feat: add service-based booking mode to appointment scheduling
- Introduced a new booking mode in WeeklySchedule to support service-based appointments. - Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times. - Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side. - Implemented validation to ensure at least one bookable service exists for doctors in service mode. - Added new API endpoint to retrieve available appointment slots based on selected services. - Updated MyAppointmentsController to accept service items during appointment creation. - Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling. - Created migration to add bookable column to service_items table. - Added tests for service-based slot calculations and validation logic.
This commit is contained in:
@@ -80,6 +80,74 @@ class SlotCalculatorService
|
||||
return !empty($this->buildAllSessions($doctor, $date));
|
||||
}
|
||||
|
||||
/**
|
||||
* حالت نوبتدهی سرویسی: زمانهای شروعِ ممکن برای نوبتی به طول $durationMinutes
|
||||
* در یک روز. برخلاف اسلاتِ ثابت، فضای خالی داخل هر session را با توجه به مدت
|
||||
* سرویس (+ بافر) پُر میکند: از ابتدای window شروع، بازههای اشغالشده را رد
|
||||
* میکند و اولین جای پیوستهٔ کافی را برمیگرداند، سپس نوبتهای بعدی را پشتسرهم
|
||||
* (با فاصلهٔ بافر) میچیند.
|
||||
*
|
||||
* زمان پایانِ ذخیرهشدهٔ نوبت = start + duration (بدون بافر)؛ بافر فقط فاصلهٔ
|
||||
* بین دو نوبت است، پس candidate بعدی از start + duration + buffer شروع میشود.
|
||||
*
|
||||
* @return array<array{start:int,end:int,start_time:string,end_time:string,location_id:?int}>
|
||||
*/
|
||||
public function getServiceStartTimes(Doctor $doctor, string $date, int $durationMinutes): array
|
||||
{
|
||||
if ($durationMinutes <= 0) return [];
|
||||
|
||||
$buffer = (int)($this->getBookingMeta($doctor)['buffer_minutes'] ?? 0);
|
||||
$durSec = $durationMinutes * 60;
|
||||
$needSec = $durSec + $buffer * 60; // فضای لازم شامل بافر
|
||||
|
||||
$sessions = $this->buildAllSessions($doctor, $date); // window/holiday/override/booking-window رعایت میشود
|
||||
if (empty($sessions)) return [];
|
||||
|
||||
$dayStart = (int) strtotime($date . ' 00:00:00');
|
||||
$busy = $this->appointmentRepo->findBusyIntervals($doctor, $dayStart, $dayStart + 86400);
|
||||
$now = time();
|
||||
|
||||
$result = [];
|
||||
foreach ($sessions as $session) {
|
||||
$winStart = $dayStart + $this->parseTime($session['start_time'] ?? '00:00');
|
||||
$winEnd = $dayStart + $this->parseTime($session['end_time'] ?? '00:00');
|
||||
$locationId = $session['slots'][0]['location_id'] ?? null;
|
||||
|
||||
$t = max($winStart, $now);
|
||||
while ($t + $durSec <= $winEnd) {
|
||||
$end = $t + $durSec;
|
||||
$conflict = $this->firstOverlap($t, $t + $needSec, $busy);
|
||||
if ($conflict !== null) {
|
||||
$t = $conflict; // به انتهای بازهٔ اشغالشدهٔ متداخل بپر
|
||||
continue;
|
||||
}
|
||||
$result[] = [
|
||||
'start' => $t,
|
||||
'end' => $end,
|
||||
'start_time' => date('H:i', $t),
|
||||
'end_time' => date('H:i', $end),
|
||||
'location_id' => $locationId !== null ? (int) $locationId : null,
|
||||
];
|
||||
$t += $needSec; // نوبت بعدی پس از این نوبت + بافر
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* انتهای اولین بازهٔ اشغالشدهای که با [$start, $end) تداخل دارد، یا null.
|
||||
* @param array<array{start:int,end:int}> $busy
|
||||
*/
|
||||
private function firstOverlap(int $start, int $end, array $busy): ?int
|
||||
{
|
||||
foreach ($busy as $b) {
|
||||
if ($b['start'] < $end && $b['end'] > $start) {
|
||||
return $b['end'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Booking is allowed only when online booking is enabled and the date is
|
||||
* today..(today + window). Past dates are always rejected.
|
||||
|
||||
Reference in New Issue
Block a user