feat(appointments): add support for doctors without schedules and display relevant notice
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user