feat: enhance appointment scheduling with session management and availability checks
This commit is contained in:
@@ -19,12 +19,43 @@ class SlotCalculatorService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns available slots for a doctor on a given date.
|
||||
* Returns available slots (flat array) for booking conflict checks.
|
||||
* 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}]
|
||||
* @return array[] [{start, end, start_time, end_time, location_id}]
|
||||
*/
|
||||
public function getAvailableSlots(Doctor $doctor, string $date): array
|
||||
{
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
if (empty($sessions)) return [];
|
||||
$flat = array_merge(...array_map(fn($s) => $s['slots'], $sessions));
|
||||
return $this->filterBookedSlots($doctor, $flat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sessions grouped by shift, each slot tagged with is_available.
|
||||
* Used by the schedule view to show real shift boundaries.
|
||||
*
|
||||
* @return array[] [{start_time, end_time, slots: [{start, end, start_time, end_time, location_id, is_available}]}]
|
||||
*/
|
||||
public function getAllSlotsWithAvailability(Doctor $doctor, string $date): array
|
||||
{
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
return array_map(fn(array $session) => [
|
||||
'start_time' => $session['start_time'],
|
||||
'end_time' => $session['end_time'],
|
||||
'slots' => array_map(fn(array $slot) => array_merge($slot, [
|
||||
'is_available' => !$this->appointmentRepo->isSlotTaken($doctor, $slot['start'], $slot['end']),
|
||||
]), $session['slots']),
|
||||
], $sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core: build all sessions with their slots, grouped by shift.
|
||||
*
|
||||
* @return array[] [{start_time: string, end_time: string, slots: array[]}]
|
||||
*/
|
||||
private function buildAllSessions(Doctor $doctor, string $date): array
|
||||
{
|
||||
$dayStart = (int) strtotime($date . ' 00:00:00');
|
||||
$dayEnd = $dayStart + 86400;
|
||||
@@ -38,10 +69,7 @@ class SlotCalculatorService
|
||||
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)
|
||||
);
|
||||
return $this->buildSessionsFromOverride($override->getSetting() ?? [], $dayStart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,30 +84,79 @@ class SlotCalculatorService
|
||||
$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);
|
||||
$sessionConfigs = $dayConf['sessions'] ?? [];
|
||||
$activeSessions = array_filter($sessionConfigs, 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;
|
||||
$result = [];
|
||||
$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');
|
||||
$slots = $this->buildSessionSlots($session, $dayStart);
|
||||
if (!empty($slots)) {
|
||||
$result[] = [
|
||||
'start_time' => $session['start_time'] ?? '00:00',
|
||||
'end_time' => $session['end_time'] ?? '00:00',
|
||||
'slots' => $slots,
|
||||
];
|
||||
}
|
||||
$prevEnd = $this->parseTime($session['end_time'] ?? '00:00');
|
||||
}
|
||||
|
||||
usort($allSlots, fn($a, $b) => $a['start'] - $b['start']);
|
||||
|
||||
return $this->filterBookedSlots($doctor, $allSlots);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build slots from a morning/evening session config.
|
||||
* Supports: rest breaks, patient limits.
|
||||
* Build sessions from date-override config.
|
||||
* Supports new SessionConfig format {start_time, end_time, ...} and legacy {start, end, duration}.
|
||||
*/
|
||||
private function buildSessionsFromOverride(array $slotConfigs, int $dayStart): array
|
||||
{
|
||||
$sessions = [];
|
||||
foreach ($slotConfigs as $config) {
|
||||
if (isset($config['start_time'])) {
|
||||
$slots = $this->buildSessionSlots(array_merge(['active' => true], $config), $dayStart);
|
||||
if (!empty($slots)) {
|
||||
$sessions[] = [
|
||||
'start_time' => $config['start_time'],
|
||||
'end_time' => $config['end_time'] ?? '00:00',
|
||||
'slots' => $slots,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Legacy flat format: {start, end, duration}
|
||||
$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;
|
||||
$slots = [];
|
||||
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,
|
||||
];
|
||||
}
|
||||
if (!empty($slots)) {
|
||||
$sessions[] = [
|
||||
'start_time' => $config['start'] ?? '00:00',
|
||||
'end_time' => $config['end'] ?? '00:00',
|
||||
'slots' => $slots,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build individual slot entries from a session config (weekly or override).
|
||||
* Handles rest breaks and patient limits.
|
||||
*/
|
||||
private function buildSessionSlots(array $session, int $dayStart): array
|
||||
{
|
||||
@@ -87,8 +164,8 @@ class SlotCalculatorService
|
||||
$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
|
||||
$restInt = (int)($session['rest_interval'] ?? 60) * 60;
|
||||
$restDur = (int)($session['time_to_rest'] ?? 10) * 60;
|
||||
$limit = isset($session['patient_limit']) && $session['patient_limit'] !== null
|
||||
? (int)$session['patient_limit'] : null;
|
||||
$locationId = isset($session['location_id']) ? (int)$session['location_id'] : null;
|
||||
@@ -97,13 +174,12 @@ class SlotCalculatorService
|
||||
|
||||
$slots = [];
|
||||
$currentSec = $startSec;
|
||||
$elapsedWork = 0; // seconds worked since last rest
|
||||
$elapsedWork = 0;
|
||||
$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;
|
||||
@@ -126,41 +202,6 @@ class SlotCalculatorService
|
||||
return $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build slots from date-override custom_slots.
|
||||
* Supports new SessionConfig format {start_time, end_time, duration_per_patient, ...}
|
||||
* and legacy flat format {start, end, duration} for backward compatibility.
|
||||
*/
|
||||
private function buildFlatSlots(array $slotConfigs, int $dayStart): array
|
||||
{
|
||||
$slots = [];
|
||||
foreach ($slotConfigs as $config) {
|
||||
if (isset($config['start_time'])) {
|
||||
// New SessionConfig format — reuse the same logic as weekly sessions
|
||||
$slots = array_merge($slots, $this->buildSessionSlots(
|
||||
array_merge(['active' => true], $config),
|
||||
$dayStart
|
||||
));
|
||||
} else {
|
||||
// Legacy flat format: {start, end, duration}
|
||||
$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 =>
|
||||
|
||||
Reference in New Issue
Block a user