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); } }