feat: Enhance appointment management by decoupling online booking toggle for admin context

- Introduced management mode for appointment slots, allowing doctors, admins, and clinic managers to view and book slots regardless of the online booking status.
- Updated SlotCalculatorService to accept a management context parameter, bypassing online booking restrictions.
- Modified appointment-related endpoints to handle management context and ensure proper authorization checks.
- Added tests to verify that management users can access slots even when online booking is disabled, while public users are still restricted.
- Improved documentation for API endpoints to reflect new management parameters and behaviors.
This commit is contained in:
hamed
2026-07-22 16:43:56 +03:30
parent 5507b42fd8
commit ed516c81a8
16 changed files with 658 additions and 83 deletions
@@ -158,8 +158,9 @@ class AppointmentController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت تاریخ نادرست است (Y-m-d)', 422, 'date');
}
$clinic = $this->bookingClinic($doctor, $request->query->get('clinic_uuid'));
$sessions = $this->slotCalculator->getAllSlotsWithAvailability($doctor, $date, $clinic);
$clinic = $this->bookingClinic($doctor, $request->query->get('clinic_uuid'));
$forManagement = $this->isManagementContext($request, $doctor, $clinic);
$sessions = $this->slotCalculator->getAllSlotsWithAvailability($doctor, $date, $clinic, $forManagement);
return $this->success([
'doctor_uuid' => $doctorUuid,
@@ -168,7 +169,7 @@ class AppointmentController extends BaseController
'sessions' => $sessions,
// خالی‌بودن دلایل مختلفی دارد؛ کلاینت نباید همه را «تعطیل» بنامد.
'empty_reason' => $sessions === []
? $this->slotCalculator->explainEmptyDay($doctor, $date, $clinic)
? $this->slotCalculator->explainEmptyDay($doctor, $date, $clinic, $forManagement)
: null,
]);
}
@@ -234,7 +235,13 @@ class AppointmentController extends BaseController
'total_duration_minutes' => $totalMinutes,
'buffer_minutes' => (int) $meta['buffer_minutes'],
'clinic_uuid' => $clinic?->getUuid(),
'start_times' => $this->slotCalculator->getServiceStartTimes($doctor, $date, $totalMinutes, $clinic),
'start_times' => $this->slotCalculator->getServiceStartTimes(
$doctor,
$date,
$totalMinutes,
$clinic,
$this->isManagementContext($request, $doctor, $clinic),
),
]);
}
@@ -306,8 +313,9 @@ class AppointmentController extends BaseController
continue;
}
$meta = $schedule->getMeta();
$address = $byId[$hours[0]['location_id']] ?? $addresses[0];
$meta = $schedule->getMeta();
$address = $byId[$hours[0]['location_id']] ?? $addresses[0];
$forManagement = $this->isManagementContext($request, $doctor, $clinic);
$locations[] = [
'location_uuid' => $address->getUuid(),
@@ -321,10 +329,10 @@ class AppointmentController extends BaseController
'services' => $meta['booking_mode'] === WeeklySchedule::MODE_SERVICE
? $this->bookableServices($doctor, $clinic)
: [],
'next_available_at' => $this->slotCalculator->findNextAvailableStart($doctor, $clinic),
'next_available_at' => $this->slotCalculator->findNextAvailableStart($doctor, $clinic, 30, $forManagement),
'available_on_date' => $date === ''
? null
: $this->slotCalculator->getAvailableSlots($doctor, $date, $clinic) !== [],
: $this->slotCalculator->getAvailableSlots($doctor, $date, $clinic, $forManagement) !== [],
];
}
@@ -352,14 +360,15 @@ class AppointmentController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سال یا ماه نامعتبر است', 422, 'month');
}
$clinic = $this->bookingClinic($doctor, $request->query->get('clinic_uuid'));
$daysInMonth = (int) date('t', (int) strtotime(sprintf('%04d-%02d-01', $year, $month)));
$clinic = $this->bookingClinic($doctor, $request->query->get('clinic_uuid'));
$forManagement = $this->isManagementContext($request, $doctor, $clinic);
$daysInMonth = (int) date('t', (int) strtotime(sprintf('%04d-%02d-01', $year, $month)));
$disabled = [];
$enabled = [];
for ($day = 1; $day <= $daysInMonth; $day++) {
$date = sprintf('%04d-%02d-%02d', $year, $month, $day);
if ($this->slotCalculator->hasAnyAvailability($doctor, $date, $clinic)) {
if ($this->slotCalculator->hasAnyAvailability($doctor, $date, $clinic, $forManagement)) {
$enabled[] = $date;
} else {
$disabled[] = $date;
@@ -747,6 +756,25 @@ class AppointmentController extends BaseController
return $this->bookingContext->resolve($doctor, $clinicUuid);
}
/**
* آیا این درخواستِ اسلات از پنل مدیریت است (پزشک/منشی/ادمینِ دارای دسترسی)؟
* اندپوینت‌های اسلات عمومی‌اند؛ فقط با management=1 + کاربرِ احرازشده و مجاز،
* توگلِ نوبت‌دهی آنلاین دور زده می‌شود. در غیر این‌صورت مثل رزرو عمومی رفتار می‌شود
* (fail-safe عمومی) تا اسلاتِ خاموش به بازدیدکنندهٔ سایت نشت نکند.
*/
private function isManagementContext(Request $request, Doctor $doctor, ?Clinic $clinic): bool
{
if ($request->query->get('management') !== '1') {
return false;
}
$user = $this->getUser();
if (!$user instanceof User) {
return false;
}
return $this->accessChecker->canManageContext($user, $doctor, $clinic);
}
private function assertServicesMatchContext(array $serviceUuids, Doctor $doctor, ?Clinic $clinic): ?JsonResponse
{
[$type, $id] = $clinic !== null