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
-35
View File
@@ -209,41 +209,6 @@ class Admin extends CI_Controller {
public function admin_worker_override() {
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$data['pageTitle'] = 'Egyedi napok kezelése';
$data['workers'] = $this->Service_model->getAllWorkers();
if ($this->input->post('storeOverride')) {
$this->load->database();
$insert = [
'worker_id' => $_POST['worker_id'],
'date' => $_POST['date'],
'is_day_off' => isset($_POST['is_day_off']) ? 1 : 0,
'start_time' => $_POST['start_time'],
'end_time' => $_POST['end_time']
];
$existing = $this->db->query("SELECT * FROM worker_schedule_overrides WHERE worker_id = '{$insert['worker_id']}' AND date = '{$insert['date']}'")->row();
if ($existing) {
$this->db->where('id', $existing->id)->update('worker_schedule_overrides', $insert);
} else {
$this->db->insert('worker_schedule_overrides', $insert);
}
}
if (!empty($_GET['worker_id'])) {
$this->load->database();
$worker_id = $_GET['worker_id'];
$data['selectedWorkerId'] = $worker_id;
$data['overrides'] = $this->db->query("SELECT * FROM worker_schedule_overrides WHERE worker_id = '$worker_id' ORDER BY date ASC")->result();
}
$this->load->view('admin/worker_override', $data);
}
public function manage_worker_schedule($worker_id = null) {
+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;
}
@@ -41,7 +41,6 @@ $this->load->helper('url');
<link rel="stylesheet" href="<?php echo base_url();?>assets/js/jquery/jquery-ui.css">
<script src="<?php echo base_url();?>assets/js/jquery/jquery-ui.js"></script>
<script src="https://momentjs.com/downloads/moment-with-locales.js"></script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Start cookieyes banner --> <script id="cookieyes" type="text/javascript" src="https://cdn-cookieyes.com/client_data/82ccb0b38ad63612e4c6a3fa/script.js"></script> <!-- End cookieyes banner -->
<link rel="stylesheet" type="text/css" href="<?php echo base_url();?>assets/css/style.css">
<link rel="stylesheet" type="text/css" href="<?php echo base_url();?>assets/css/responsive.css">
@@ -41,7 +41,6 @@ $this->load->helper('url');
<link rel="stylesheet" href="<?php echo base_url();?>assets/js/jquery/jquery-ui.css">
<script src="<?php echo base_url();?>assets/js/jquery/jquery-ui.js"></script>
<script src="https://momentjs.com/downloads/moment-with-locales.js"></script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Start cookieyes banner --> <script id="cookieyes" type="text/javascript" src="https://cdn-cookieyes.com/client_data/82ccb0b38ad63612e4c6a3fa/script.js"></script> <!-- End cookieyes banner -->
<link rel="stylesheet" type="text/css" href="<?php echo base_url();?>assets/css/style.css">
<link rel="stylesheet" type="text/css" href="<?php echo base_url();?>assets/css/responsive.css">
@@ -187,8 +187,7 @@
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Back"/>
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
<div class="g-recaptcha" data-callback="recaptcha_callback" data-sitekey="6Ld_sS0pAAAAANQrY3ziDGPcsjMOdM9L9WIgAzxd"></div>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Send" disabled/>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Send"/>
</div>
</div>
@@ -425,7 +424,8 @@
$('#bookingStep'+stepNumber).show();
if(stepNumber == 4){
if(stepNumber == 4){
recaptcha_callback();
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
$('#stepCircle3').addClass('activeStep');
@@ -472,7 +472,8 @@
else if(stepNumber == 1){
$RequiredFieldSelected = 1;
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
$RequiredFieldSelected = 1;
}
else if(stepNumber == 3){
@@ -483,7 +484,8 @@
});
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
if($('#booking_date').val() != '' && $('#booking_time').val() != ''){
$RequiredFieldSelected = 1;
}
@@ -503,7 +505,3 @@
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
async defer>
</script>
@@ -188,8 +188,7 @@
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Vissza"/>
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
<div class="g-recaptcha" data-callback="recaptcha_callback" data-sitekey="6Ld_sS0pAAAAANQrY3ziDGPcsjMOdM9L9WIgAzxd"></div>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Elküld" disabled/>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Elküld"/>
</div>
</div>
</form>
@@ -424,7 +423,8 @@
$('#bookingStep'+stepNumber).show();
if(stepNumber == 4){
if(stepNumber == 4){
recaptcha_callback();
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
$('#stepCircle3').addClass('activeStep');
@@ -471,7 +471,8 @@
else if(stepNumber == 1){
$RequiredFieldSelected = 1;
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
$RequiredFieldSelected = 1;
}
else if(stepNumber == 3){
@@ -482,7 +483,8 @@
});
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
if($('#booking_date').val() != '' && $('#booking_time').val() != ''){
$RequiredFieldSelected = 1;
}
@@ -502,7 +504,3 @@
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
async defer>
</script>
@@ -188,8 +188,7 @@ if (is_array($services) && count($services) > 0) {
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Tilbake"/>
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
<div class="g-recaptcha" data-callback="recaptcha_callback" data-sitekey="6Ld_sS0pAAAAANQrY3ziDGPcsjMOdM9L9WIgAzxd"></div>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Sende" disabled/>
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Sende"/>
</div>
</div>
</form>
@@ -424,7 +423,8 @@ if (is_array($services) && count($services) > 0) {
$('#bookingStep'+stepNumber).show();
if(stepNumber == 4){
if(stepNumber == 4){
recaptcha_callback();
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
$('#stepCircle3').addClass('activeStep');
@@ -471,7 +471,8 @@ if (is_array($services) && count($services) > 0) {
else if(stepNumber == 1){
$RequiredFieldSelected = 1;
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
$RequiredFieldSelected = 1;
}
else if(stepNumber == 3){
@@ -482,7 +483,8 @@ if (is_array($services) && count($services) > 0) {
});
}
else if(stepNumber == 4){
else if(stepNumber == 4){
recaptcha_callback();
if($('#booking_date').val() != '' && $('#booking_time').val() != ''){
$RequiredFieldSelected = 1;
}
@@ -502,7 +504,3 @@ if (is_array($services) && count($services) > 0) {
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
async defer>
</script>