fix(appointment): one weekly schedule per doctor, place chosen per shift
A doctor working both at their own practice and at a clinic had to write two independent schedules and neither panel could see the other, so the clinic showed an empty form even though the doctor had configured their practice. The schedule is now a single record owned by the doctor. What varies between days is the place: the context of a shift is read from its location_id, not from the record it lives in. Booking in a context therefore sees only that context's days, so a personal-practice secretary still cannot book a clinic day. The caller's own context decides which addresses they may assign: the doctor gets every place of theirs, a clinic manager only its own, and shifts outside their reach are returned for display but preserved verbatim on save. Existing per-clinic rows are merged by migration; location_id was already stored on every shift, so no context information is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,17 @@ const DEFAULT_BOOKING_META: BookingMeta = {
|
||||
booking_mode: 'slot',
|
||||
buffer_minutes: 0,
|
||||
};
|
||||
interface WeeklyScheduleData { uuid: string; doctor_uuid: string; schedule: NewScheduleMap; meta?: BookingMeta; booking_mode_locked?: boolean; }
|
||||
interface WeeklyScheduleData {
|
||||
uuid: string;
|
||||
doctor_uuid: string;
|
||||
schedule: NewScheduleMap;
|
||||
meta?: BookingMeta;
|
||||
booking_mode_locked?: boolean;
|
||||
/** همهٔ مکانهای پزشک — مطب شخصی و هر کلینیکی که عضوش است. فقط برای برچسبزدن. */
|
||||
locations?: AddressData[];
|
||||
/** مکانهایی که همین کاربر حق دارد روی برنامه بنشاند. */
|
||||
selectable_location_ids?: number[];
|
||||
}
|
||||
|
||||
// ── Persian (Jalali) date utilities ───────────────────────────────────────
|
||||
|
||||
@@ -561,8 +571,35 @@ function NoLocationsNotice({ clinicUuid }: { clinicUuid?: string | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* شیفتی که مکانش خارج از دسترس این کاربر است.
|
||||
*
|
||||
* حذفش از فهرست، برنامه را ناقص نشان میدهد و کاربر روی همان ساعت شیفت تازه میگذارد؛
|
||||
* ویرایشپذیر کردنش هم یعنی مدیر کلینیک میتواند برنامهٔ مطب شخصی پزشک را عوض کند.
|
||||
* پس دیده میشود و دست نمیخورد.
|
||||
*/
|
||||
function ForeignSessionRow({ session, placeName }: { session: SessionConfig; placeName: string }) {
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px dashed var(--border-2)', borderRadius: 'var(--r)', background: 'var(--surface-2)',
|
||||
padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
|
||||
}}>
|
||||
<LockClosedIcon style={{ width: 15, height: 15, color: 'var(--text-3)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>
|
||||
{session.start_time} تا {session.end_time}
|
||||
</span>
|
||||
<span className="badge" style={{ flexShrink: 0 }}>{placeName}</span>
|
||||
<span className="muted" style={{ fontSize: 12 }}>خارج از دسترس شما</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly = false }: { doctorUuid: string; clinicUuid?: string | null; addresses: AddressData[]; readOnly?: boolean }) {
|
||||
const qc = useQueryClient();
|
||||
// برنامه یکی است و شیفتهای همهٔ محیطهای پزشک را دارد. مدیر کلینیک شیفت مطب شخصی
|
||||
// را میبیند ولی نباید بتواند عوضش کند، پس این دو از هم جدا نگه داشته میشوند.
|
||||
const [allLocations, setAllLocations] = useState<AddressData[]>([]);
|
||||
const [selectableIds, setSelectableIds] = useState<number[] | null>(null);
|
||||
const [scheduleMap, setScheduleMap] = useState<NewScheduleMap>(EMPTY_NEW_SCHEDULE);
|
||||
const [scheduleUuid, setScheduleUuid] = useState<string | null>(null);
|
||||
const [expandedDay, setExpandedDay] = useState<string | null>(null);
|
||||
@@ -592,12 +629,26 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
||||
setScheduleUuid(d.uuid);
|
||||
}
|
||||
if (d?.meta) setMeta({ ...DEFAULT_BOOKING_META, ...d.meta });
|
||||
if (d?.locations) setAllLocations(d.locations);
|
||||
if (d?.selectable_location_ids) setSelectableIds(d.selectable_location_ids.map(Number));
|
||||
setModeLocked(!!d?.booking_mode_locked);
|
||||
} else if (scheduleQ.error instanceof ApiError && scheduleQ.error.status === 404) {
|
||||
setScheduleMap(EMPTY_NEW_SCHEDULE); setScheduleUuid(null);
|
||||
}
|
||||
}, [scheduleQ.data, scheduleQ.error]);
|
||||
|
||||
// شیفت بدون مکان هنوز در حال ساخت است، پس دست کاربر باز میماند.
|
||||
const canEditSession = (session: SessionConfig) =>
|
||||
selectableIds === null || session.location_id === null || selectableIds.includes(session.location_id);
|
||||
|
||||
const locationLabel = (locationId: number | null) => {
|
||||
const found = (allLocations.length ? allLocations : addresses).find(a => Number(a.id) === locationId);
|
||||
if (!found) return 'مکان نامشخص';
|
||||
return found.type === 'clinic'
|
||||
? (found.clinic_name ?? found.name ?? 'کلینیک')
|
||||
: (found.name ?? 'مطب شخصی');
|
||||
};
|
||||
|
||||
const overlapDays = useMemo(() =>
|
||||
Object.fromEntries(SCHEDULE_DAYS.map(d => [d.key, hasOverlap(scheduleMap[d.key]?.sessions ?? [])]))
|
||||
, [scheduleMap]);
|
||||
@@ -891,10 +942,14 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
||||
</button>
|
||||
</div>
|
||||
) : sessions.map((session, idx) => (
|
||||
<SessionEditor key={idx} session={session} addresses={addresses}
|
||||
serviceMode={meta.booking_mode === 'service'}
|
||||
onChange={s => updateSession(day.key, idx, s)}
|
||||
onRemove={() => removeSession(day.key, idx)} />
|
||||
canEditSession(session) ? (
|
||||
<SessionEditor key={idx} session={session} addresses={addresses}
|
||||
serviceMode={meta.booking_mode === 'service'}
|
||||
onChange={s => updateSession(day.key, idx, s)}
|
||||
onRemove={() => removeSession(day.key, idx)} />
|
||||
) : (
|
||||
<ForeignSessionRow key={idx} session={session} placeName={locationLabel(session.location_id)} />
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user