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:
hamed
2026-07-18 13:51:10 +03:30
co-authored by Claude Opus 4.8
parent 02566beacf
commit ac6bdef9ef
2 changed files with 55 additions and 2 deletions
@@ -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
{