diff --git a/docs/api/appointment.md b/docs/api/appointment.md index c4a4d78e..70a4047f 100644 --- a/docs/api/appointment.md +++ b/docs/api/appointment.md @@ -737,6 +737,9 @@ location — picking one and hiding the rest removes real capacity from the doct "clinic_uuid": null, "booking_mode": "slot", "buffer_minutes": 0, + "opening_hours": [ + { "day": "Saturday", "opens": "09:00", "closes": "13:00" } + ], "services": [], "next_available_at": 1755000000 }, @@ -748,6 +751,10 @@ location — picking one and hiding the rest removes real capacity from the doct "clinic_uuid": "41e325c4-e825-4067-8438-5d828ecaee09", "booking_mode": "service", "buffer_minutes": 10, + "opening_hours": [ + { "day": "Saturday", "opens": "09:00", "closes": "13:00" }, + { "day": "Sunday", "opens": "16:00", "closes": "20:00" } + ], "services": [ { "uuid": "…", "name": "ویزیت", "duration_minutes": 20, "price_rials": 500000, "service_section": { "uuid": "…", "name": "عمومی" } } @@ -764,6 +771,7 @@ location — picking one and hiding the rest removes real capacity from the doct | `location_uuid` | `string\|null` | the `DoctorAddress` uuid; `null` when the context has no address yet | | `type` | `"personal" \| "clinic"` | | | `booking_mode` | `"slot" \| "service"` | per-context — the same doctor can differ between locations | +| `opening_hours` | `array` | active weekly shifts of that context, flattened; `day` is the English weekday name so it maps straight onto schema.org `openingHoursSpecification` | | `services` | `array` | populated only in `service` mode, scoped to that context's owner | | `next_available_at` | `int\|null` | Unix timestamp of the earliest free slot within 30 days | @@ -773,5 +781,11 @@ selection; locations with no capacity sort last. Deep links should carry the cho **Status codes:** `200`, `404 ERR_VALIDATION_002` (doctor not found). -**Consumer:** `nobat724_front` — the doctor page must render one booking block per entry and pass the -matching `clinic_uuid` into the slot and booking calls. +`opening_hours` lists one entry per active shift, so a day with a morning and an evening shift +appears twice. Days with no active shift are absent. Times are local `HH:MM` strings, and the +per-shift `location_id` is not repeated here — every shift in an entry already belongs to that +location's context. + +**Consumer:** `nobat724_front` — the doctor page must render one booking block per entry, pass the +matching `clinic_uuid` into the slot and booking calls, and feed `opening_hours` into the +`openingHoursSpecification` of each `MedicalClinic` in the Physician JSON-LD. diff --git a/src/Appointment/Controller/AppointmentController.php b/src/Appointment/Controller/AppointmentController.php index a8fac72e..505a4e46 100644 --- a/src/Appointment/Controller/AppointmentController.php +++ b/src/Appointment/Controller/AppointmentController.php @@ -288,6 +288,7 @@ class AppointmentController extends BaseController 'clinic_uuid' => $clinic?->getUuid(), 'booking_mode' => $meta['booking_mode'], 'buffer_minutes' => (int) $meta['buffer_minutes'], + 'opening_hours' => $this->openingHours($schedule), 'services' => $meta['booking_mode'] === WeeklySchedule::MODE_SERVICE ? $this->bookableServices($doctor, $clinic) : [], @@ -718,6 +719,44 @@ class AppointmentController extends BaseController }, $this->itemRepo->findBookableByEntity($type, $id)); } + /** + * شیفت‌های فعال هفته به‌صورت تخت، با نام انگلیسی روز — آمادهٔ نگاشت به + * openingHoursSpecification در schema.org. کلیدهای برنامه 0..6 هستند و 0 شنبه است. + * + * @return array + */ + private function openingHours(WeeklySchedule $schedule): array + { + $hours = []; + + foreach ($schedule->getDaySchedule() as $dayIndex => $day) { + $dayName = WeeklySchedule::DAYS[(int) $dayIndex] ?? null; + if ($dayName === null) { + continue; + } + + foreach (($day['sessions'] ?? []) as $session) { + if (!($session['active'] ?? false)) { + continue; + } + + $opens = $session['start_time'] ?? null; + $closes = $session['end_time'] ?? null; + if ($opens === null || $closes === null) { + continue; + } + + $hours[] = [ + 'day' => ucfirst($dayName), + 'opens' => $opens, + 'closes' => $closes, + ]; + } + } + + return $hours; + } + /** زودترین اسلات آزاد در ۳۰ روز آینده، یا null اگر ظرفیتی نباشد. */ private function nextAvailableAt(Doctor $doctor, ?Clinic $clinic): ?int {