feat(appointment): expose weekly opening hours per booking location
The booking-locations endpoint described where a doctor can be booked but not when, so the public site had no way to emit openingHoursSpecification and had to fall back to availableService alone. Each location now carries opening_hours: its active weekly shifts flattened, with the English weekday name so the consumer can map it onto schema.org directly. A day with two shifts appears twice; days with no active shift are omitted. Hours are per context, so the personal practice and each clinic report their own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -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.
|
||||
|
||||
@@ -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<int, array{day: string, opens: string, closes: string}>
|
||||
*/
|
||||
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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user