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
+8 -10
View File
@@ -123,8 +123,7 @@ class DoctorController extends BaseController
$this->userRepo->save($user);
}
$schedule = $this->scheduleRepo->findByDoctor($doctor);
return $this->success(['data' => $doctor->toDetailArray($schedule)], 201);
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor))], 201);
}
#[OA\Get(
@@ -176,8 +175,8 @@ class DoctorController extends BaseController
? ['id' => $rep->getId(), 'uuid' => $rep->getUuid(), 'full_name' => $rep->getFullName()]
: null;
$schedule = $this->scheduleRepo->findByDoctor($doctor);
return $this->success(['data' => array_merge($doctor->toDetailArray($schedule), [
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules), [
'clinics' => $clinicData,
'representation' => $representation,
])]);
@@ -201,8 +200,8 @@ class DoctorController extends BaseController
return $this->error(ErrorCodes::ERR_AUTH_006, 'این پزشک عضو کلینیک شما نیست', 403);
}
$schedule = $this->scheduleRepo->findByDoctor($doctor);
return $this->success(['data' => array_merge($doctor->toDetailArray($schedule), ['clinics' => [[
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules), ['clinics' => [[
'id' => (string) $clinic->getId(),
'uuid' => $clinic->getUuid(),
'name' => $clinic->getName(),
@@ -258,11 +257,11 @@ class DoctorController extends BaseController
$scheduleMap = [];
foreach ($this->scheduleRepo->findByDoctors($result['items']) as $schedule) {
$scheduleMap[$schedule->getDoctor()->getId()] = $schedule;
$scheduleMap[$schedule->getDoctor()->getId()][] = $schedule;
}
return $this->paginated(
array_map(fn(Doctor $d) => $d->toListArray($scheduleMap[$d->getId()] ?? null), $result['items']),
array_map(fn(Doctor $d) => $d->toListArray($scheduleMap[$d->getId()] ?? []), $result['items']),
$result['total'],
$result['page'],
$result['limit']
@@ -348,8 +347,7 @@ class DoctorController extends BaseController
$this->hydrateDoctor($doctor, $data);
$this->doctorRepo->save($doctor);
$schedule = $this->scheduleRepo->findByDoctor($doctor);
return $this->success(['data' => $doctor->toDetailArray($schedule)]);
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor))]);
}
#[OA\Delete(