feat(appointments): add support for doctors without schedules and display relevant notice

This commit is contained in:
hamed
2026-08-04 10:26:36 +03:30
parent 654c1e8303
commit 41b3109478
3 changed files with 145 additions and 16 deletions
+95 -11
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import {
PlusIcon, ChevronRightIcon, ChevronLeftIcon, ChevronDownIcon, CalendarDaysIcon,
AdjustmentsHorizontalIcon,
@@ -229,20 +229,24 @@ export default function AppointmentsPage() {
const clinicDoctorsList = clinicDoctorsQuery.data?.data?.data ?? [];
const doctors = React.useMemo(() => {
const map = new Map<string, string>();
// پزشکِ بدون برنامهٔ کاری تب نمی‌گیرد — تبش جز یک تایم‌لاین همیشه‌خالی چیزی ندارد.
// نبودِ فیلد (مسیر ادمین) یعنی «نمی‌دانیم»، پس پنهان نمی‌شود.
clinicDoctorsList
.filter(d => d.has_schedule !== false)
.forEach(d => map.set(d.uuid, d.name));
const map = new Map<string, { name: string; hasSchedule: boolean }>();
// پزشکِ بدون برنامهٔ کاری هم تب می‌گیرد: منابعِ تحت نظرش تقویم مستقلِ خودشان را
// دارند و قابل نوبت‌دهی‌اند، پس پنهان‌کردنِ پزشک آن منابع را هم از دسترس خارج
// می‌کرد. به‌جای حذف، تبش برچسب «بدون ساعت کاری» می‌گیرد.
// نبودِ فیلد (مسیر ادمین) یعنی «نمی‌دانیم»، پس برچسب نمی‌خورد.
clinicDoctorsList.forEach(d => map.set(d.uuid, { name: d.name, hasSchedule: d.has_schedule !== false }));
appointments.forEach(a => {
if (a.doctor_uuid && a.doctor_name && !map.has(a.doctor_uuid)) {
map.set(a.doctor_uuid, a.doctor_name);
map.set(a.doctor_uuid, { name: a.doctor_name, hasSchedule: true });
}
});
return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name }));
return Array.from(map.entries()).map(([uuid, v]) => ({ uuid, name: v.name, hasSchedule: v.hasSchedule }));
}, [appointments, clinicDoctorsList]);
const selectedDoctor = doctors.find(d => d.uuid === selectedDoctorUuid) ?? null;
/** پزشکِ انتخاب‌شده برنامهٔ هفتگی ندارد ⇒ تایم‌لاین جای خود را به راهنما می‌دهد. */
const doctorHasNoSchedule = selectedDoctor?.hasSchedule === false;
// سرویس‌های موجود در نوبت‌های امروز (برای فیلتر «سرویس مورد نظر...»).
const serviceOptions = React.useMemo(() => {
const map = new Map<string, string>();
@@ -291,7 +295,8 @@ export default function AppointmentsPage() {
`/api/v1/appointment-slots?doctor_uuid=${selectedDoctorUuid}&date=${selectedDate}&management=1` +
(effectiveClinicUuid ? `&clinic_uuid=${encodeURIComponent(effectiveClinicUuid)}` : '')
),
enabled: viewMode === 'timeline' && !!selectedDoctorUuid,
// برنامهٔ هفتگی ندارد ⇒ پاسخ قطعاً خالی است؛ درخواست فرستاده نمی‌شود.
enabled: viewMode === 'timeline' && !!selectedDoctorUuid && !doctorHasNoSchedule,
});
// ── روش نوبت‌دهی پزشکِ انتخاب‌شده (سرویسی/اسلاتی)
@@ -473,7 +478,11 @@ export default function AppointmentsPage() {
}}>
{showDoctorTabs && (
<DoctorTabs
doctors={doctors}
doctors={doctors.map(d => ({
uuid: d.uuid,
name: d.name,
note: d.hasSchedule ? undefined : 'بدون ساعت کاری',
}))}
selected={selectedDoctorUuid}
onSelect={selectDoctor}
showAll={isAdmin}
@@ -534,6 +543,20 @@ export default function AppointmentsPage() {
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>
برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید
</div>
) : doctorHasNoSchedule ? (
/* بدون برنامهٔ هفتگی، خودِ پزشک اسلاتی ندارد؛ اما منابعِ تحت نظرش
تقویم مستقل دارند و همچنان قابل نوبت‌دهی‌اند. */
<NoScheduleNotice
settingsHref={
isClinic || isClinicScopedSecretary
? `/admin/settings/appointment-settings?scope=doctors&doctor=${selectedDoctorUuid}`
: isDoctor || isSecretary ? '/admin/appointment-settings' : null
}
resources={supervisedResources}
canCreate={!isRepresentation && canCreateAppt}
onBookResource={setBookingResource}
onOpenResource={selectResource}
/>
) : (
<>
{serviceMode && (
@@ -613,6 +636,67 @@ export default function AppointmentsPage() {
);
}
/**
* پزشکِ بدون برنامهٔ هفتگی — به‌جای تایم‌لاینِ همیشه‌خالی.
*
* منبع، زیرمجموعهٔ پزشک است ولی تقویمش مستقل؛ پس نبودِ ساعت کاریِ پزشک نوبت‌دهیِ
* منابعش را متوقف نمی‌کند و همین‌جا میان‌بُرِ رزروشان می‌آید.
*/
function NoScheduleNotice({ settingsHref, resources, canCreate, onBookResource, onOpenResource }: {
settingsHref: string | null;
resources: ClinicResource[];
canCreate: boolean;
onBookResource: (r: ClinicResource) => void;
onOpenResource: (uuid: string) => void;
}) {
return (
<div style={{ padding: '32px 16px', textAlign: 'center' }}>
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text)' }}>ساعت کاری تنظیم نشده است</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>
برای این پزشک برنامهٔ هفتگی تعریف نشده، پس تایملاین نوبتهایش خالی است.
</div>
{settingsHref && (
<Link to={settingsHref} className="btn primary sm" style={{ display: 'inline-flex', marginTop: 14 }}>
تنظیم ساعت کاری
</Link>
)}
{resources.length > 0 && (
<div style={{
marginTop: 22, paddingTop: 18, borderTop: '1px solid var(--border)',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10,
}}>
<div style={{ fontSize: 12.5, color: 'var(--text-2)' }}>
منابع تحت نظر این پزشک تقویم مستقل دارند و همچنان قابل نوبتدهی هستند:
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center' }}>
{resources.map((r) => (
<div key={r.uuid} style={{
display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px',
border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', background: 'var(--surface-2)',
}}>
<button
type="button"
className="btn sm"
onClick={() => onOpenResource(r.uuid)}
style={{ background: 'none', border: 'none', color: 'var(--text)', fontSize: 13, padding: 0 }}
>
{r.name}
</button>
{canCreate && (
<button type="button" className="btn primary sm" onClick={() => onBookResource(r)}>
ثبت نوبت
</button>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
/** فیلتر سرویس نوار ابزار — ردیف‌های بارگذاری‌شدهٔ روز را بر اساس سرویس فیلتر می‌کند. */
function ServiceFilterSelect({ value, options, onChange }: {
value: string; options: { uuid: string; name: string }[]; onChange: (v: string) => void;