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
+16 -6
View File
@@ -174,13 +174,23 @@ Get doctor detail for clinic owner — only doctors who are members of the authe
### Schedule Fields Notes
| Field | When schedule exists | When no schedule |
|-------|---------------------|-----------------|
| `free_turn` | نزدیک‌ترین روز/ساعت کاری از امروز (مثلاً «دوشنبه ۹:۰۰–۱۳:۰۰») | «نوبت آزادی موجود نیست» |
| `hours_of_work` | خلاصه ساعت‌های روزهای فعال با `\|` جداشده | «برنامه کاری تنظیم نشده» |
| `active` | `online_booking_enabled && has_active_sessions` | `false` — نوبت‌دهی غیرفعال |
**تجمیع همهٔ برنامه‌ها (2026-07):** این فیلدها روی **همهٔ** برنامه‌های هفتگی پزشک محاسبه
می‌شوند — برنامهٔ مطب شخصی (`clinic_id IS NULL`) به‌علاوهٔ یک برنامه به ازای هر کلینیک.
پزشکی که برنامهٔ شخصی خالی/خاموش ولی برنامهٔ کلینیکیِ فعال دارد، `active=true` می‌گیرد؛
برنامهٔ یک محیط هرگز محیط دیگر را نمی‌پوشاند.
> **نوبت‌دهی آنلاین غیرفعال:** منبعِ فعال/غیرفعال بودن نوبت‌دهی آنلاین، فیلد `meta.online_booking_enabled` در `WeeklySchedule` پزشک است. اگر `false` باشد، صرف‌نظر از سشن‌های برنامه‌ی هفتگی، `free_turn` همیشه `"نوبت‌دهی آنلاین غیرفعال است"` و `active` برابر `false` برمی‌گردد؛ `hours_of_work` در صورت وجود برنامه حفظ می‌شود. چنین پزشکی در لیست عمومی `GET /api/v1/doctors` (پیش‌فرض `active=true`) نمایش داده نمی‌شود، ولی صفحه‌ی تکی `GET /api/v1/doctor/{slug}` همچنان قابل دسترسی است.
| Field | When at least one schedule is bookable | When none |
|-------|---------------------------------------|-----------|
| `free_turn` | نزدیک‌ترین روز/ساعت کاری از امروز، بین همهٔ برنامه‌های روشن (مثلاً «دوشنبه ۹:۰۰–۱۳:۰۰») | «نوبت آزادی موجود نیست» |
| `hours_of_work` | خلاصه ساعت‌های همان برنامه‌ای که `free_turn` را داده (ساعت‌های دو محل با هم ترکیب نمی‌شوند) | «برنامه کاری تنظیم نشده» |
| `active` | `activeDoctorAppointment && (∃ schedule: online_booking_enabled && has_active_sessions)` | `false` — نوبت‌دهی غیرفعال |
> **نوبت‌دهی آنلاین غیرفعال:** اگر `meta.online_booking_enabled` در **همهٔ** برنامه‌های پزشک
> `false` باشد، `free_turn` برابر `"نوبت‌دهی آنلاین غیرفعال است"` و `active` برابر `false`
> برمی‌گردد؛ `hours_of_work` در صورت وجود برنامه حفظ می‌شود. تا وقتی حتی یک برنامه روشن و
> دارای روز فعال باشد، همان مبنا قرار می‌گیرد. چنین پزشکی (همه خاموش) در لیست عمومی
> `GET /api/v1/doctors` (پیش‌فرض `active=true`) نمایش داده نمی‌شود، ولی صفحه‌ی تکی
> `GET /api/v1/doctor/{slug}` همچنان قابل دسترسی است.
---