fix(booking): aggregate public booking state across all schedules

The public doctor payload built `active`/`free_turn`/`hours_of_work` from the
personal schedule alone, so a doctor bookable only at a clinic was reported as
"نوبت‌دهی غیرفعال". Aggregate over every schedule instead: any schedule with
online booking on and an active day makes the doctor bookable, and the disabled
label only appears when all of them are off.

Three admin-panel fixes for the same class of bug:

- AppointmentsPage took the selected doctor from `dbUuid`, which is the clinic's
  uuid inside a clinic context — the slots request 404'd. Use `doctorUuid`.
- TurnsTimeline rendered any error or unknown empty_reason as "این روز شیفت کاری
  ندارد". Errors now surface as errors and unknown reasons get a neutral message;
  the day-off wording is reserved for an explicit day_off from the backend.
- Admins have no clinic context, so slots fell back to the personal schedule.
  They now pick a location from `appointment-booking-locations` and that choice
  drives the slot, service and create-appointment requests.

Adds `app:schedule:normalize-format` for legacy rows stored as a bare JSON list
covering only Saturday, which read as day-off for the rest of the week.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-18 16:25:24 +03:30
co-authored by Claude Fable 5
parent 63bf2cdd12
commit 7baa4df3d4
15 changed files with 631 additions and 69 deletions
@@ -154,10 +154,11 @@ const EMPTY_REASON_TEXT: Record<string, { title: string; hint: string }> = {
};
export default function TurnsTimeline({
slots, loading, queryKey, onView, onBook, emptyReason,
slots, loading, queryKey, onView, onBook, emptyReason, errorMessage,
}: {
slots: TimelineSlot[];
emptyReason?: string | null;
errorMessage?: string | null;
loading: boolean;
queryKey: unknown[];
onView: (a: Appointment) => void;
@@ -175,8 +176,19 @@ export default function TurnsTimeline({
}, [activeIndex]);
if (loading) return <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری...</div>;
if (errorMessage) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--danger)' }}>خطا در دریافت برنامهٔ این روز</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>{errorMessage}</div>
</div>
);
}
if (!slots.length) {
const reason = EMPTY_REASON_TEXT[emptyReason ?? ''] ?? EMPTY_REASON_TEXT.day_off;
// «شیفت کاری ندارد» فقط وقتی که backend صریحاً day_off گفته باشد؛
// دلیل ناشناخته/غایب نباید به تعطیلی تفسیر شود.
const reason = EMPTY_REASON_TEXT[emptyReason ?? '']
?? { title: 'برنامهٔ این روز در دسترس نیست', hint: 'اطلاعات برنامهٔ کاری برای این روز دریافت نشد' };
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text)' }}>{reason.title}</div>