- Removed unused DAY_MAP constant. - Enhanced getAvailableSlots method to prioritize holiday checks and date overrides. - Simplified session handling by filtering and sorting active sessions. - Introduced buildSessionSlots method to handle session slot creation with support for rest breaks and patient limits. - Updated buildFlatSlots method for backward compatibility with date overrides. - Improved filterBookedSlots method for clarity and conciseness.
169 lines
6.4 KiB
PHP
169 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Appointment\Service;
|
|
|
|
use App\Appointment\Repository\AppointmentRepository;
|
|
use App\Appointment\Repository\DateOverrideRepository;
|
|
use App\Appointment\Repository\HolidayRepository;
|
|
use App\Appointment\Repository\WeeklyScheduleRepository;
|
|
use App\Doctor\Entity\Doctor;
|
|
|
|
|
|
class SlotCalculatorService
|
|
{
|
|
public function __construct(
|
|
private readonly WeeklyScheduleRepository $scheduleRepo,
|
|
private readonly DateOverrideRepository $overrideRepo,
|
|
private readonly HolidayRepository $holidayRepo,
|
|
private readonly AppointmentRepository $appointmentRepo,
|
|
) {}
|
|
|
|
/**
|
|
* Returns available slots for a doctor on a given date.
|
|
* Day index convention: 0=Saturday(شنبه), 1=Sunday, ..., 6=Friday(جمعه)
|
|
*
|
|
* @return array[] [{start: int, end: int, start_time: string, end_time: string, location_id: int|null}]
|
|
*/
|
|
public function getAvailableSlots(Doctor $doctor, string $date): array
|
|
{
|
|
$dayStart = (int) strtotime($date . ' 00:00:00');
|
|
$dayEnd = $dayStart + 86400;
|
|
|
|
// 1. Blocked by holiday
|
|
if (!empty($this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1))) {
|
|
return [];
|
|
}
|
|
|
|
// 2. Date override takes precedence over weekly schedule
|
|
foreach ($this->overrideRepo->findByDoctor($doctor) as $override) {
|
|
if (date('Y-m-d', $override->getDate()) === $date) {
|
|
if (!$override->isActive()) return [];
|
|
return $this->filterBookedSlots(
|
|
$doctor,
|
|
$this->buildFlatSlots($override->getSetting() ?? [], $dayStart)
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3. Weekly schedule
|
|
$schedule = $this->scheduleRepo->findByDoctor($doctor);
|
|
if ($schedule === null) return [];
|
|
|
|
// Convert PHP date('w') (0=Sunday) to Iranian week index (0=Saturday)
|
|
$phpDow = (int) date('w', $dayStart);
|
|
$dayKey = (string)(($phpDow + 1) % 7);
|
|
|
|
$dayConf = $schedule->getSetting()[$dayKey] ?? null;
|
|
if ($dayConf === null) return [];
|
|
|
|
$sessions = $dayConf['sessions'] ?? [];
|
|
// Sort active sessions by start_time, skip overlapping ones
|
|
$activeSessions = array_filter($sessions, fn($s) => $s['active'] ?? false);
|
|
usort($activeSessions, fn($a, $b) =>
|
|
$this->parseTime($a['start_time'] ?? '00:00') <=> $this->parseTime($b['start_time'] ?? '00:00')
|
|
);
|
|
|
|
$allSlots = [];
|
|
$prevEnd = 0;
|
|
foreach ($activeSessions as $session) {
|
|
$sessionStart = $this->parseTime($session['start_time'] ?? '00:00');
|
|
if ($sessionStart < $prevEnd) continue; // skip overlapping session
|
|
$allSlots = array_merge($allSlots, $this->buildSessionSlots($session, $dayStart));
|
|
$prevEnd = $this->parseTime($session['end_time'] ?? '00:00');
|
|
}
|
|
|
|
usort($allSlots, fn($a, $b) => $a['start'] - $b['start']);
|
|
|
|
return $this->filterBookedSlots($doctor, $allSlots);
|
|
}
|
|
|
|
/**
|
|
* Build slots from a morning/evening session config.
|
|
* Supports: rest breaks, patient limits.
|
|
*/
|
|
private function buildSessionSlots(array $session, int $dayStart): array
|
|
{
|
|
$startSec = $this->parseTime($session['start_time'] ?? '00:00');
|
|
$endSec = $this->parseTime($session['end_time'] ?? '00:00');
|
|
$dur = (int)($session['duration_per_patient'] ?? 20) * 60;
|
|
$hasRest = (bool)($session['has_rest'] ?? false);
|
|
$restInt = (int)($session['rest_interval'] ?? 60) * 60; // convert min → sec
|
|
$restDur = (int)($session['time_to_rest'] ?? 10) * 60; // convert min → sec
|
|
$limit = isset($session['patient_limit']) && $session['patient_limit'] !== null
|
|
? (int)$session['patient_limit'] : null;
|
|
$locationId = isset($session['location_id']) ? (int)$session['location_id'] : null;
|
|
|
|
if ($dur <= 0 || $endSec <= $startSec) return [];
|
|
|
|
$slots = [];
|
|
$currentSec = $startSec;
|
|
$elapsedWork = 0; // seconds worked since last rest
|
|
$patientCount = 0;
|
|
|
|
while ($currentSec + $dur <= $endSec) {
|
|
if ($limit !== null && $patientCount >= $limit) break;
|
|
|
|
// Insert rest break if needed
|
|
if ($hasRest && $restInt > 0 && $elapsedWork > 0 && $elapsedWork >= $restInt) {
|
|
$currentSec += $restDur;
|
|
$elapsedWork = 0;
|
|
continue;
|
|
}
|
|
|
|
$slots[] = [
|
|
'start' => $dayStart + $currentSec,
|
|
'end' => $dayStart + $currentSec + $dur,
|
|
'start_time' => gmdate('H:i', $currentSec),
|
|
'end_time' => gmdate('H:i', $currentSec + $dur),
|
|
'location_id' => $locationId,
|
|
];
|
|
|
|
$currentSec += $dur;
|
|
$elapsedWork += $dur;
|
|
$patientCount += 1;
|
|
}
|
|
|
|
return $slots;
|
|
}
|
|
|
|
/**
|
|
* Build slots from flat date-override format: [{start, end, duration}]
|
|
* Kept for backward-compatibility with DateOverride.custom_slots.
|
|
*/
|
|
private function buildFlatSlots(array $slotConfigs, int $dayStart): array
|
|
{
|
|
$slots = [];
|
|
foreach ($slotConfigs as $config) {
|
|
$startSec = $this->parseTime($config['start'] ?? '00:00');
|
|
$endSec = $this->parseTime($config['end'] ?? '00:00');
|
|
$duration = (int)($config['duration'] ?? 30) * 60;
|
|
|
|
if ($duration <= 0 || $endSec <= $startSec) continue;
|
|
|
|
for ($t = $startSec; $t + $duration <= $endSec; $t += $duration) {
|
|
$slots[] = [
|
|
'start' => $dayStart + $t,
|
|
'end' => $dayStart + $t + $duration,
|
|
'start_time' => gmdate('H:i', $t),
|
|
'end_time' => gmdate('H:i', $t + $duration),
|
|
'location_id' => null,
|
|
];
|
|
}
|
|
}
|
|
return $slots;
|
|
}
|
|
|
|
private function filterBookedSlots(Doctor $doctor, array $slots): array
|
|
{
|
|
return array_values(array_filter($slots, fn(array $slot): bool =>
|
|
!$this->appointmentRepo->isSlotTaken($doctor, $slot['start'], $slot['end'])
|
|
));
|
|
}
|
|
|
|
private function parseTime(string $time): int
|
|
{
|
|
[$h, $m] = explode(':', $time, 2) + [0, 0];
|
|
return ((int)$h * 3600) + ((int)$m * 60);
|
|
}
|
|
}
|