Fix timezone bug causing slots past closing time; remove lunch fallback outside window

- Add Europe/Oslo timezone to all DateTime constructors in getAvailableTimes() and
  computeLunchBreak() to prevent UTC vs local time mismatch that allowed booking
  slots 1 hour past the worker's end time on same-day bookings
- Remove fallback loop in computeLunchBreak() that pushed the lunch break outside
  the configured window; lunch break is now strictly enforced within the interval

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 11:52:30 +01:00
co-authored by Claude Sonnet 4.6
parent f30d8d1a07
commit 307f17faa6
8 changed files with 36 additions and 83 deletions
+14 -18
View File
@@ -439,17 +439,17 @@ class Service_model extends CI_Model {
if ($override->is_day_off) return [];
$dayStartTime = new DateTime($selectedDate.' '.$override->start_time);
$finishTime = new DateTime($selectedDate.' '.$override->end_time);
$dayStartTime = new DateTime($selectedDate.' '.$override->start_time, new DateTimeZone('Europe/Oslo'));
$finishTime = new DateTime($selectedDate.' '.$override->end_time, new DateTimeZone('Europe/Oslo'));
} else {
$weekday = date('w', strtotime($selectedDate)); // 0 = Sunday, 6 = Saturday
$weekNumber = date('W', strtotime($selectedDate));
$isOddWeek = $weekNumber % 2 !== 0;
$scheduleQuery = $this->db->query("
SELECT * FROM worker_schedule
WHERE worker_id = '{$worker_id}'
AND weekday = '{$weekday}'
SELECT * FROM worker_schedule
WHERE worker_id = '{$worker_id}'
AND weekday = '{$weekday}'
AND (
is_alternate_week = 0
OR (is_alternate_week = 1 AND " . ($isOddWeek ? "1" : "0") . ")
@@ -461,8 +461,8 @@ class Service_model extends CI_Model {
}
$schedule = $scheduleQuery->row();
$dayStartTime = new DateTime($selectedDate . ' ' . $schedule->start_time);
$finishTime = new DateTime($selectedDate . ' ' . $schedule->end_time);
$dayStartTime = new DateTime($selectedDate . ' ' . $schedule->start_time, new DateTimeZone('Europe/Oslo'));
$finishTime = new DateTime($selectedDate . ' ' . $schedule->end_time, new DateTimeZone('Europe/Oslo'));
}
// --- ÚJ: ma foglalva csak (következő 15 perces blokk + 1 óra) UTÁN legyen időpont ---
@@ -685,8 +685,9 @@ class Service_model extends CI_Model {
$lunchDuration = new DateInterval('PT30M');
$lunchStep = new DateInterval('PT15M');
$winStartDt = new DateTime($date . ' ' . $winStart);
$winEndDt = new DateTime($date . ' ' . $winEnd);
$tz = new DateTimeZone('Europe/Oslo');
$winStartDt = new DateTime($date . ' ' . $winStart, $tz);
$winEndDt = new DateTime($date . ' ' . $winEnd, $tz);
// Clamp window to actual shift hours
if ($shiftStart && $winStartDt < $shiftStart) $winStartDt = clone $shiftStart;
@@ -695,10 +696,11 @@ class Service_model extends CI_Model {
$scanEnd = (clone $winEndDt)->sub($lunchDuration);
$isFree = function($candidate) use ($lunchDuration, $dayBookings, $date) {
$tz = new DateTimeZone('Europe/Oslo');
$candEnd = (clone $candidate)->add($lunchDuration);
foreach ($dayBookings as $bk) {
$bkS = new DateTime($date . ' ' . $bk->booking_start_time);
$bkE = new DateTime($date . ' ' . $bk->booking_finish_time);
$bkS = new DateTime($date . ' ' . $bk->booking_start_time, $tz);
$bkE = new DateTime($date . ' ' . $bk->booking_finish_time, $tz);
if ($bkS < $candEnd && $bkE > $candidate) return false;
}
return true;
@@ -713,7 +715,7 @@ class Service_model extends CI_Model {
if (!empty($freeSlots)) {
// Pick the slot closest to preferred time (or start of window if no preference)
$prefDt = $preferredTime
? new DateTime($date . ' ' . $preferredTime)
? new DateTime($date . ' ' . $preferredTime, new DateTimeZone('Europe/Oslo'))
: clone $winStartDt;
$best = null;
$bestDiff = PHP_INT_MAX;
@@ -725,12 +727,6 @@ class Service_model extends CI_Model {
return ['start' => $best->format('H:i'), 'end' => $bestEnd->format('H:i')];
}
// Fallback: search after the window
for ($c = clone $winEndDt; ; $c->add($lunchStep)) {
$candEnd = (clone $c)->add($lunchDuration);
if ($candEnd > $shiftEnd) break;
if ($isFree($c)) return ['start' => $c->format('H:i'), 'end' => $candEnd->format('H:i')];
}
return null;
}