feat(admin): service-aware time picking on the appointment edit page

In service mode the page now mounts the existing ServiceSlotPicker and hides the
three free-form time inputs plus the single-service select: a 45-minute service
could previously be shortened to 20 and the next patient would sit on top of it.
Hidden rather than disabled — a disabled field reads as "you must do something
here".

Saving splits in two: the service-aware endpoint takes the time and services
(the client sends no duration), then the usual PATCH carries deposit, insurance,
status and note without slot_start/slot_end/version, since the reschedule already
advanced the optimistic-lock version.

Booking mode is read from the appointment's own schedule via an explicit
clinic_uuid, not from the panel's current environment: a doctor can be slot-based
in their office and service-based in a clinic. That required exposing clinic_uuid
in Appointment::toArray(), which was missing.

appointment-service-slots accepts exclude_appointment_uuid, gated on canManage of
that appointment — an ungated parameter would let anyone fabricate availability.

ServiceSlotPicker gained two optional props; its existing callers pass neither and
are unaffected. Its reset-on-doctor-change effect now skips the first run, which
would otherwise wipe the initial selection.

Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-30 14:57:21 +03:30
co-authored by Claude Opus 5
parent 0051205bf2
commit 50759bf663
6 changed files with 402 additions and 59 deletions
@@ -219,6 +219,24 @@ class AppointmentController extends BaseController
(array) $request->query->all('durations'),
);
// جابه‌جایی: بازهٔ خودِ نوبتِ در حال ویرایش نباید اشغال حساب شود، وگرنه زمان
// فعلی‌اش هرگز در فهرست نمی‌آید. فقط برای کسی که همان نوبت را مدیریت می‌کند —
// این پارامتر آزاد نیست، چون در غیر این‌صورت هر کسی می‌توانست با uuid دلخواه
// ظرفیتِ ساختگی ببیند.
$excludeId = null;
$excludeUuid = trim((string) $request->query->get('exclude_appointment_uuid', ''));
if ($excludeUuid !== '') {
$excluded = $this->appointmentRepo->findByUuid($excludeUuid);
$actor = $this->getUser();
if ($excluded === null || !$actor instanceof User || !$this->canManage($excluded, $actor)) {
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403, 'exclude_appointment_uuid');
}
if ($excluded->getDoctor()->getId() !== $doctor->getId()) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نوبت انتخابی به این پزشک تعلق ندارد', 422, 'exclude_appointment_uuid');
}
$excludeId = $excluded->getId();
}
return $this->success([
'doctor_uuid' => $doctorUuid,
'date' => $date,
@@ -231,6 +249,7 @@ class AppointmentController extends BaseController
$duration->totalMinutes,
$clinic,
$this->isManagementContext($request, $doctor, $clinic),
$excludeId,
),
]);
}
+4
View File
@@ -467,6 +467,10 @@ class Appointment
],
'address' => $firstAddress?->toArray(),
'address_id' => $this->addressId,
// محلِ نوبت‌دهی این نوبت. null = مطب شخصی. کلاینت بدون این نمی‌داند روش
// نوبت‌دهی را از کدام برنامه بپرسد: یک پزشک می‌تواند در مطب اسلاتی و در
// کلینیک سرویسی باشد و محیط جاریِ پنل لزوماً محیط این نوبت نیست.
'clinic_uuid' => $this->clinic?->getUuid(),
'user' => [
'uuid' => $this->user->getUuid(),
'mobile' => $this->user->getMobileNumber(),