Fix lunch break: float-to-gap logic + Google Calendar sync
- Lunch slot no longer pre-blocked; a slot is only unavailable if booking it would eliminate the last possible 30-min break window - Added preferred lunch time per worker (closest-to-preferred slot wins) - Lunch break only applies for shifts >= 6 hours - Google Calendar: lunch event created/updated/deleted on every booking create, modify, or cancel via _syncLunchCalendarEvent() - New table worker_lunch_gcal_events tracks lunch event IDs per worker/date - New model methods: getBookingsForWorkerDay, getLunchGcalEventId, upsertLunchGcalEventId, deleteLunchGcalEventRecord, getBookingsForWorkerMonth, computeLunchBreak Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+398
-333
@@ -460,6 +460,9 @@ class Pages extends CI_Controller {
|
|||||||
$this->Service_model->updateBookingCalEvents($newBooking->booking_id, $workerEventId, $ownerEventId);
|
$this->Service_model->updateBookingCalEvents($newBooking->booking_id, $workerEventId, $ownerEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync lunch break calendar event for this worker+date
|
||||||
|
$this->_syncLunchCalendarEvent($worker, $bookingArray['booking_date']);
|
||||||
|
|
||||||
// ntfy push notification
|
// ntfy push notification
|
||||||
$gcalServiceNamesNtfy = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
$gcalServiceNamesNtfy = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
||||||
$this->_ntfy(
|
$this->_ntfy(
|
||||||
@@ -703,339 +706,401 @@ class Pages extends CI_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function manage_booking($lang, $token){
|
public function manage_booking($lang, $token){
|
||||||
$this->load->helper('url');
|
$this->load->helper('url');
|
||||||
$this->load->model('Service_model');
|
$this->load->model('Service_model');
|
||||||
|
|
||||||
$booking = $this->Service_model->getBookingByToken($token);
|
$booking = $this->Service_model->getBookingByToken($token);
|
||||||
if(!$booking){
|
if(!$booking){
|
||||||
show_404();
|
show_404();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine booking type from services
|
// Determine booking type from services
|
||||||
$serviceIds = unserialize($booking->service_ids);
|
$serviceIds = unserialize($booking->service_ids);
|
||||||
$subpage = 'barber';
|
$subpage = 'barber';
|
||||||
if(is_array($serviceIds) && !empty($serviceIds)){
|
if(is_array($serviceIds) && !empty($serviceIds)){
|
||||||
$firstService = $this->Service_model->getServiceById($serviceIds[0]);
|
$firstService = $this->Service_model->getServiceById($serviceIds[0]);
|
||||||
if($firstService){
|
if($firstService){
|
||||||
$subpage = $firstService->service_type;
|
$subpage = $firstService->service_type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
||||||
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
||||||
|
|
||||||
$data['token'] = $token;
|
$data['token'] = $token;
|
||||||
$data['booking'] = $booking;
|
$data['booking'] = $booking;
|
||||||
$data['selectedLang'] = $lang;
|
$data['selectedLang'] = $lang;
|
||||||
$data['subpage'] = $subpage;
|
$data['subpage'] = $subpage;
|
||||||
$data['pageTitle'] = '';
|
$data['pageTitle'] = '';
|
||||||
$data['status'] = 'ok';
|
$data['status'] = 'ok';
|
||||||
|
|
||||||
if($appointmentDT < $now){
|
if($appointmentDT < $now){
|
||||||
$data['status'] = 'expired';
|
$data['status'] = 'expired';
|
||||||
} elseif(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
} elseif(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
||||||
$data['status'] = 'cutoff';
|
$data['status'] = 'cutoff';
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['services'] = $this->Service_model->getAllServiceByServiceType($subpage, $lang);
|
$data['services'] = $this->Service_model->getAllServiceByServiceType($subpage, $lang);
|
||||||
$data['workers'] = $this->Service_model->getActiveWorkers($subpage);
|
$data['workers'] = $this->Service_model->getActiveWorkers($subpage);
|
||||||
$data['selectedServiceIds'] = is_array($serviceIds) ? $serviceIds : array();
|
$data['selectedServiceIds'] = is_array($serviceIds) ? $serviceIds : array();
|
||||||
$data['worker'] = $this->Service_model->getWorkerById($booking->worker_id);
|
$data['worker'] = $this->Service_model->getWorkerById($booking->worker_id);
|
||||||
|
|
||||||
$this->load->view('pages/manage-booking', $data);
|
$this->load->view('pages/manage-booking', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function manage_booking_process(){
|
public function manage_booking_process(){
|
||||||
$this->load->helper('url');
|
$this->load->helper('url');
|
||||||
$this->load->model('Service_model');
|
$this->load->model('Service_model');
|
||||||
|
|
||||||
if(!isset($_POST['token']) || !isset($_POST['sendBooking'])){
|
if(!isset($_POST['token']) || !isset($_POST['sendBooking'])){
|
||||||
header('Location:'.SITEURL);
|
header('Location:'.SITEURL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $_POST['token'];
|
$token = $_POST['token'];
|
||||||
$booking = $this->Service_model->getBookingByToken($token);
|
$booking = $this->Service_model->getBookingByToken($token);
|
||||||
if(!$booking){
|
if(!$booking){
|
||||||
show_404();
|
show_404();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
||||||
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
||||||
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Modification cutoff has passed.']));
|
->set_output(json_encode(['error' => 'Modification cutoff has passed.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build selected service array and total length
|
// Build selected service array and total length
|
||||||
$selectedServiceArray = array();
|
$selectedServiceArray = array();
|
||||||
$totalLengthSeconds = 0;
|
$totalLengthSeconds = 0;
|
||||||
foreach($_POST as $key => $value){
|
foreach($_POST as $key => $value){
|
||||||
if(strpos($key, 'service_') === 0 && $value == '1'){
|
if(strpos($key, 'service_') === 0 && $value == '1'){
|
||||||
$serviceId = str_replace('service_', '', $key);
|
$serviceId = str_replace('service_', '', $key);
|
||||||
$selectedServiceArray[] = $serviceId;
|
$selectedServiceArray[] = $serviceId;
|
||||||
$service = $this->Service_model->getServiceById($serviceId);
|
$service = $this->Service_model->getServiceById($serviceId);
|
||||||
if($service){
|
if($service){
|
||||||
$parts = explode(':', $service->service_time);
|
$parts = explode(':', $service->service_time);
|
||||||
$totalLengthSeconds += ($parts[0] * 3600) + ($parts[1] * 60);
|
$totalLengthSeconds += ($parts[0] * 3600) + ($parts[1] * 60);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$servicelength = gmdate('H:i:s', $totalLengthSeconds);
|
$servicelength = gmdate('H:i:s', $totalLengthSeconds);
|
||||||
|
|
||||||
$newBookingDate = $_POST['booking_date'];
|
$newBookingDate = $_POST['booking_date'];
|
||||||
$newStartTime = $_POST['booking_start_time'];
|
$newStartTime = $_POST['booking_start_time'];
|
||||||
$newWorkerId = $_POST['worker_id'];
|
$newWorkerId = $_POST['worker_id'];
|
||||||
$lang = $_POST['lang'];
|
$lang = $_POST['lang'];
|
||||||
$subpage = $_POST['subpage'];
|
$subpage = $_POST['subpage'];
|
||||||
|
|
||||||
// Schedule check
|
// Schedule check
|
||||||
$weekday = date('w', strtotime($newBookingDate));
|
$weekday = date('w', strtotime($newBookingDate));
|
||||||
$schedule = $this->Service_model->getWorkerScheduleByDay($newWorkerId, $weekday);
|
$schedule = $this->Service_model->getWorkerScheduleByDay($newWorkerId, $weekday);
|
||||||
if(!$schedule){
|
if(!$schedule){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Worker is not available on this day.']));
|
->set_output(json_encode(['error' => 'Worker is not available on this day.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if($newStartTime < $schedule->start_time || $newStartTime >= $schedule->end_time){
|
if($newStartTime < $schedule->start_time || $newStartTime >= $schedule->end_time){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
|
->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!$this->Service_model->isWorkerAvailableThisWeek($newWorkerId, $newBookingDate)){
|
if(!$this->Service_model->isWorkerAvailableThisWeek($newWorkerId, $newBookingDate)){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Worker only works every second week.']));
|
->set_output(json_encode(['error' => 'Worker only works every second week.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slot availability (temporarily exclude current booking)
|
// Slot availability (temporarily exclude current booking)
|
||||||
$this->Service_model->updateBooking($booking->booking_id, ['booking_date' => '1970-01-01']);
|
$this->Service_model->updateBooking($booking->booking_id, ['booking_date' => '1970-01-01']);
|
||||||
$available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength);
|
$available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength);
|
||||||
$this->Service_model->updateBooking($booking->booking_id, ['booking_date' => $booking->booking_date]);
|
$this->Service_model->updateBooking($booking->booking_id, ['booking_date' => $booking->booking_date]);
|
||||||
|
|
||||||
if(!is_array($available) || !in_array($newStartTime, $available)){
|
if(!is_array($available) || !in_array($newStartTime, $available)){
|
||||||
$this->output->set_status_header(409)->set_content_type('application/json')
|
$this->output->set_status_header(409)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Conflict. Timeslot is taken or does not fit the service.']));
|
->set_output(json_encode(['error' => 'Conflict. Timeslot is taken or does not fit the service.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3-month limit
|
// 3-month limit
|
||||||
$today = new DateTime();
|
$today = new DateTime();
|
||||||
$maxDate = (clone $today)->modify('+3 months');
|
$maxDate = (clone $today)->modify('+3 months');
|
||||||
$selectedDate = new DateTime($newBookingDate);
|
$selectedDate = new DateTime($newBookingDate);
|
||||||
if($selectedDate > $maxDate){
|
if($selectedDate > $maxDate){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Bookings can only be made up to 3 months in advance.']));
|
->set_output(json_encode(['error' => 'Bookings can only be made up to 3 months in advance.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate finish time
|
// Calculate finish time
|
||||||
$bookingStart = new DateTime($newStartTime);
|
$bookingStart = new DateTime($newStartTime);
|
||||||
$bookingLength = new DateTime($servicelength);
|
$bookingLength = new DateTime($servicelength);
|
||||||
$bookingEnd = clone $bookingStart;
|
$bookingEnd = clone $bookingStart;
|
||||||
$bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('H')).'H'));
|
$bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('H')).'H'));
|
||||||
$bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('i')).'M'));
|
$bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('i')).'M'));
|
||||||
$closingTime = new DateTime('18:00');
|
$closingTime = new DateTime('18:00');
|
||||||
if($bookingEnd > $closingTime){
|
if($bookingEnd > $closingTime){
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
->set_output(json_encode(['error' => 'Selected time exceeds business hours.']));
|
->set_output(json_encode(['error' => 'Selected time exceeds business hours.']));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$finishTime = $bookingEnd->format('H:i:s');
|
$finishTime = $bookingEnd->format('H:i:s');
|
||||||
|
|
||||||
// Update booking
|
// Update booking
|
||||||
$this->Service_model->updateBooking($booking->booking_id, array(
|
$this->Service_model->updateBooking($booking->booking_id, array(
|
||||||
'worker_id' => $newWorkerId,
|
'worker_id' => $newWorkerId,
|
||||||
'booking_date' => $newBookingDate,
|
'booking_date' => $newBookingDate,
|
||||||
'booking_start_time' => $newStartTime,
|
'booking_start_time' => $newStartTime,
|
||||||
'booking_finish_time' => $finishTime,
|
'booking_finish_time' => $finishTime,
|
||||||
'service_ids' => serialize($selectedServiceArray),
|
'service_ids' => serialize($selectedServiceArray),
|
||||||
));
|
));
|
||||||
|
|
||||||
$worker = $this->Service_model->getWorkerById($newWorkerId);
|
$worker = $this->Service_model->getWorkerById($newWorkerId);
|
||||||
$serviceArray = array();
|
$serviceArray = array();
|
||||||
foreach($selectedServiceArray as $serviceItem){
|
foreach($selectedServiceArray as $serviceItem){
|
||||||
$sel = $this->Service_model->getServiceById($serviceItem);
|
$sel = $this->Service_model->getServiceById($serviceItem);
|
||||||
if(is_object($sel)) $serviceArray[] = $sel;
|
if(is_object($sel)) $serviceArray[] = $sel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Calendar: delete old events, create new ones
|
// Google Calendar: delete old events, create new ones
|
||||||
$this->load->library('GoogleCalendar');
|
$this->load->library('GoogleCalendar');
|
||||||
$this->config->load('google_calendar');
|
$this->config->load('google_calendar');
|
||||||
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
||||||
$oldWorker = $this->Service_model->getWorkerById($booking->worker_id);
|
$oldWorker = $this->Service_model->getWorkerById($booking->worker_id);
|
||||||
if(!empty($booking->gcal_event_id_worker) && !empty($oldWorker->google_calendar_id)){
|
if(!empty($booking->gcal_event_id_worker) && !empty($oldWorker->google_calendar_id)){
|
||||||
$this->googlecalendar->deleteEvent($oldWorker->google_calendar_id, $booking->gcal_event_id_worker);
|
$this->googlecalendar->deleteEvent($oldWorker->google_calendar_id, $booking->gcal_event_id_worker);
|
||||||
}
|
}
|
||||||
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
||||||
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
||||||
}
|
}
|
||||||
$gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
$gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
||||||
$gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $booking->guest_name;
|
$gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $booking->guest_name;
|
||||||
$gcalDescription = 'Guest: ' . $booking->guest_name . "\nPhone: " . $booking->guest_phone . "\nEmail: " . $booking->guest_email;
|
$gcalDescription = 'Guest: ' . $booking->guest_name . "\nPhone: " . $booking->guest_phone . "\nEmail: " . $booking->guest_email;
|
||||||
$gcalStart = $newBookingDate . 'T' . $newStartTime;
|
$gcalStart = $newBookingDate . 'T' . $newStartTime;
|
||||||
$gcalEnd = $newBookingDate . 'T' . $finishTime;
|
$gcalEnd = $newBookingDate . 'T' . $finishTime;
|
||||||
$newWorkerEventId = null;
|
$newWorkerEventId = null;
|
||||||
$newOwnerEventId = null;
|
$newOwnerEventId = null;
|
||||||
if(!empty($worker->google_calendar_id)){
|
if(!empty($worker->google_calendar_id)){
|
||||||
$newWorkerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
$newWorkerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
}
|
}
|
||||||
if(!empty($evelinCalId)){
|
if(!empty($evelinCalId)){
|
||||||
$newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
$newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
}
|
}
|
||||||
$this->Service_model->updateBookingCalEvents($booking->booking_id, $newWorkerEventId, $newOwnerEventId);
|
$this->Service_model->updateBookingCalEvents($booking->booking_id, $newWorkerEventId, $newOwnerEventId);
|
||||||
|
|
||||||
// ntfy push notification
|
// Sync lunch break calendar event (new date, and old date if it changed)
|
||||||
$this->_ntfy(
|
$this->_syncLunchCalendarEvent($worker, $newBookingDate);
|
||||||
'Modified booking: ' . $booking->guest_name,
|
if ($booking->booking_date !== $newBookingDate) {
|
||||||
'Worker: ' . $worker->worker_name . "\n" .
|
$this->_syncLunchCalendarEvent($worker, $booking->booking_date);
|
||||||
'Date: ' . $newBookingDate . ' ' . $newStartTime . ' - ' . $finishTime . "\n" .
|
}
|
||||||
'Services: ' . $gcalServiceNames,
|
|
||||||
isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
|
// ntfy push notification
|
||||||
);
|
$this->_ntfy(
|
||||||
|
'Modified booking: ' . $booking->guest_name,
|
||||||
$manageLink = SITEURL.$lang.'/manage-booking/'.$token;
|
'Worker: ' . $worker->worker_name . "\n" .
|
||||||
$totalServicePrice = 0;
|
'Date: ' . $newBookingDate . ' ' . $newStartTime . ' - ' . $finishTime . "\n" .
|
||||||
foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
|
'Services: ' . $gcalServiceNames,
|
||||||
|
isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
|
||||||
switch($lang){
|
);
|
||||||
case 'no':
|
|
||||||
$subject = '[studiobeve] Din bestilling er oppdatert';
|
$manageLink = SITEURL.$lang.'/manage-booking/'.$token;
|
||||||
$content = 'Kjære gjest,<br />Din bestilling er oppdatert.<br /><br />';
|
$totalServicePrice = 0;
|
||||||
$content .= '<table>';
|
foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
|
||||||
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
|
|
||||||
$content .= '<tr><td>Dato: </td><td>'.$newBookingDate.'</td></tr>';
|
switch($lang){
|
||||||
$content .= '<tr><td>Tid: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
case 'no':
|
||||||
$content .= '<tr><td>Tjenester: </td><td><table>';
|
$subject = '[studiobeve] Din bestilling er oppdatert';
|
||||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_no.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
$content = 'Kjære gjest,<br />Din bestilling er oppdatert.<br /><br />';
|
||||||
$content .= '</table></td><tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
$content .= '<table>';
|
||||||
$content .= '<br /><br /><a href="'.$manageLink.'">Administrer bestilling</a>';
|
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||||
$content .= '<br /><br /> STUDIOBEVE';
|
$content .= '<tr><td>Dato: </td><td>'.$newBookingDate.'</td></tr>';
|
||||||
break;
|
$content .= '<tr><td>Tid: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||||
case 'hu':
|
$content .= '<tr><td>Tjenester: </td><td><table>';
|
||||||
$subject = '[studiobeve] A foglalásod módosítva lett';
|
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_no.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||||
$content = 'Kedves Vendégünk,<br />A foglalásod módosítva lett.<br /><br />';
|
$content .= '</table></td><tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||||
$content .= '<table>';
|
$content .= '<br /><br /><a href="'.$manageLink.'">Administrer bestilling</a>';
|
||||||
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
|
$content .= '<br /><br /> STUDIOBEVE';
|
||||||
$content .= '<tr><td>Dátum: </td><td>'.$newBookingDate.'</td></tr>';
|
break;
|
||||||
$content .= '<tr><td>Időpont: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
case 'hu':
|
||||||
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
|
$subject = '[studiobeve] A foglalásod módosítva lett';
|
||||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_hu.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
$content = 'Kedves Vendégünk,<br />A foglalásod módosítva lett.<br /><br />';
|
||||||
$content .= '</table></td><tr><td>Összesen: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
$content .= '<table>';
|
||||||
$content .= '<br /><br /><a href="'.$manageLink.'">Foglalás kezelése</a>';
|
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||||
$content .= '<br /><br /> STUDIOBEVE';
|
$content .= '<tr><td>Dátum: </td><td>'.$newBookingDate.'</td></tr>';
|
||||||
break;
|
$content .= '<tr><td>Időpont: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||||
default:
|
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
|
||||||
$subject = '[studiobeve] Your booking has been updated';
|
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_hu.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||||
$content = 'Dear Guest,<br />Your booking has been updated.<br /><br />';
|
$content .= '</table></td><tr><td>Összesen: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||||
$content .= '<table>';
|
$content .= '<br /><br /><a href="'.$manageLink.'">Foglalás kezelése</a>';
|
||||||
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
|
$content .= '<br /><br /> STUDIOBEVE';
|
||||||
$content .= '<tr><td>Date: </td><td>'.$newBookingDate.'</td></tr>';
|
break;
|
||||||
$content .= '<tr><td>Time: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
default:
|
||||||
$content .= '<tr><td>Services: </td><td><table>';
|
$subject = '[studiobeve] Your booking has been updated';
|
||||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_en.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
$content = 'Dear Guest,<br />Your booking has been updated.<br /><br />';
|
||||||
$content .= '</table></td><tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
$content .= '<table>';
|
||||||
$content .= '<br /><br /><a href="'.$manageLink.'">Manage booking</a>';
|
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||||
$content .= '<br /><br /> STUDIOBEVE';
|
$content .= '<tr><td>Date: </td><td>'.$newBookingDate.'</td></tr>';
|
||||||
}
|
$content .= '<tr><td>Time: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||||
|
$content .= '<tr><td>Services: </td><td><table>';
|
||||||
$emailArray = array(array(
|
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_en.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||||
'addressee_email' => $booking->guest_email,
|
$content .= '</table></td><tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||||
'addressee_name' => $booking->guest_name,
|
$content .= '<br /><br /><a href="'.$manageLink.'">Manage booking</a>';
|
||||||
'subject' => $subject,
|
$content .= '<br /><br /> STUDIOBEVE';
|
||||||
'content' => $content,
|
}
|
||||||
'attachments' => array()
|
|
||||||
));
|
$emailArray = array(array(
|
||||||
$this->load->model('User_model');
|
'addressee_email' => $booking->guest_email,
|
||||||
$this->User_model->sendEmail($emailArray, $lang, '');
|
'addressee_name' => $booking->guest_name,
|
||||||
|
'subject' => $subject,
|
||||||
header('Location:'.SITEURL.$lang.'/booking-finished/'.$subpage);
|
'content' => $content,
|
||||||
}
|
'attachments' => array()
|
||||||
|
));
|
||||||
public function manage_booking_cancel(){
|
$this->load->model('User_model');
|
||||||
$this->load->helper('url');
|
$this->User_model->sendEmail($emailArray, $lang, '');
|
||||||
$this->load->model('Service_model');
|
|
||||||
$this->load->model('User_model');
|
header('Location:'.SITEURL.$lang.'/booking-finished/'.$subpage);
|
||||||
|
}
|
||||||
if(!isset($_POST['token'])){
|
|
||||||
header('Location:'.SITEURL);
|
public function manage_booking_cancel(){
|
||||||
return;
|
$this->load->helper('url');
|
||||||
}
|
$this->load->model('Service_model');
|
||||||
|
$this->load->model('User_model');
|
||||||
$token = $_POST['token'];
|
|
||||||
$lang = isset($_POST['lang']) ? $_POST['lang'] : 'en';
|
if(!isset($_POST['token'])){
|
||||||
$booking = $this->Service_model->getBookingByToken($token);
|
header('Location:'.SITEURL);
|
||||||
if(!$booking){
|
return;
|
||||||
show_404();
|
}
|
||||||
return;
|
|
||||||
}
|
$token = $_POST['token'];
|
||||||
|
$lang = isset($_POST['lang']) ? $_POST['lang'] : 'en';
|
||||||
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
$booking = $this->Service_model->getBookingByToken($token);
|
||||||
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
if(!$booking){
|
||||||
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
show_404();
|
||||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
return;
|
||||||
->set_output(json_encode(['error' => 'Cancellation cutoff has passed.']));
|
}
|
||||||
return;
|
|
||||||
}
|
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
||||||
|
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
||||||
$worker = $this->Service_model->getWorkerById($booking->worker_id);
|
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
||||||
|
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||||
switch($lang){
|
->set_output(json_encode(['error' => 'Cancellation cutoff has passed.']));
|
||||||
case 'no':
|
return;
|
||||||
$subject = '[studiobeve] Din bestilling er avbestilt';
|
}
|
||||||
$content = 'Kjære gjest,<br />Din reservasjon den '.$booking->booking_date.' kl. '.date('H:i', strtotime($booking->booking_start_time)).' med '.$worker->worker_name.' er avbestilt.<br /><br /> STUDIOBEVE';
|
|
||||||
break;
|
$worker = $this->Service_model->getWorkerById($booking->worker_id);
|
||||||
case 'hu':
|
|
||||||
$subject = '[studiobeve] A foglalásod törölve lett';
|
switch($lang){
|
||||||
$content = 'Kedves Vendégünk,<br />A '.$booking->booking_date.' '.$booking->booking_start_time.'-os foglalásod '.$worker->worker_name.'-nál törölve lett.<br /><br /> STUDIOBEVE';
|
case 'no':
|
||||||
break;
|
$subject = '[studiobeve] Din bestilling er avbestilt';
|
||||||
default:
|
$content = 'Kjære gjest,<br />Din reservasjon den '.$booking->booking_date.' kl. '.date('H:i', strtotime($booking->booking_start_time)).' med '.$worker->worker_name.' er avbestilt.<br /><br /> STUDIOBEVE';
|
||||||
$subject = '[studiobeve] Your booking has been cancelled';
|
break;
|
||||||
$content = 'Dear Guest,<br />Your booking on '.$booking->booking_date.' at '.date('H:i', strtotime($booking->booking_start_time)).' with '.$worker->worker_name.' has been cancelled.<br /><br /> STUDIOBEVE';
|
case 'hu':
|
||||||
}
|
$subject = '[studiobeve] A foglalásod törölve lett';
|
||||||
|
$content = 'Kedves Vendégünk,<br />A '.$booking->booking_date.' '.$booking->booking_start_time.'-os foglalásod '.$worker->worker_name.'-nál törölve lett.<br /><br /> STUDIOBEVE';
|
||||||
$emailArray = array(array(
|
break;
|
||||||
'addressee_email' => $booking->guest_email,
|
default:
|
||||||
'addressee_name' => $booking->guest_name,
|
$subject = '[studiobeve] Your booking has been cancelled';
|
||||||
'subject' => $subject,
|
$content = 'Dear Guest,<br />Your booking on '.$booking->booking_date.' at '.date('H:i', strtotime($booking->booking_start_time)).' with '.$worker->worker_name.' has been cancelled.<br /><br /> STUDIOBEVE';
|
||||||
'content' => $content,
|
}
|
||||||
'attachments' => array()
|
|
||||||
));
|
$emailArray = array(array(
|
||||||
$this->User_model->sendEmail($emailArray, $lang, '');
|
'addressee_email' => $booking->guest_email,
|
||||||
|
'addressee_name' => $booking->guest_name,
|
||||||
// Google Calendar: delete events on cancellation
|
'subject' => $subject,
|
||||||
$this->load->library('GoogleCalendar');
|
'content' => $content,
|
||||||
$this->config->load('google_calendar');
|
'attachments' => array()
|
||||||
if(!empty($booking->gcal_event_id_worker) && !empty($worker->google_calendar_id)){
|
));
|
||||||
$this->googlecalendar->deleteEvent($worker->google_calendar_id, $booking->gcal_event_id_worker);
|
$this->User_model->sendEmail($emailArray, $lang, '');
|
||||||
}
|
|
||||||
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
// Google Calendar: delete events on cancellation
|
||||||
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
$this->load->library('GoogleCalendar');
|
||||||
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
$this->config->load('google_calendar');
|
||||||
}
|
if(!empty($booking->gcal_event_id_worker) && !empty($worker->google_calendar_id)){
|
||||||
|
$this->googlecalendar->deleteEvent($worker->google_calendar_id, $booking->gcal_event_id_worker);
|
||||||
// ntfy push notification
|
}
|
||||||
$this->_ntfy(
|
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
||||||
'Cancelled booking: ' . $booking->guest_name,
|
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
||||||
'Worker: ' . $worker->worker_name . "\n" .
|
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
||||||
'Date: ' . $booking->booking_date . ' ' . $booking->booking_start_time,
|
}
|
||||||
isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
|
|
||||||
);
|
// ntfy push notification
|
||||||
|
$this->_ntfy(
|
||||||
$this->Service_model->deleteBooking($booking->booking_id);
|
'Cancelled booking: ' . $booking->guest_name,
|
||||||
|
'Worker: ' . $worker->worker_name . "\n" .
|
||||||
$data['selectedLang'] = $lang;
|
'Date: ' . $booking->booking_date . ' ' . $booking->booking_start_time,
|
||||||
$data['booking'] = $booking;
|
isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
|
||||||
$data['worker'] = $worker;
|
);
|
||||||
$data['pageTitle'] = '';
|
|
||||||
$this->load->view('pages/manage-booking-cancelled', $data);
|
$this->Service_model->deleteBooking($booking->booking_id);
|
||||||
}
|
|
||||||
|
// Sync lunch break calendar event now that the booking is gone
|
||||||
|
$this->_syncLunchCalendarEvent($worker, $booking->booking_date);
|
||||||
|
|
||||||
|
$data['selectedLang'] = $lang;
|
||||||
|
$data['booking'] = $booking;
|
||||||
|
$data['worker'] = $worker;
|
||||||
|
$data['pageTitle'] = '';
|
||||||
|
$this->load->view('pages/manage-booking-cancelled', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _syncLunchCalendarEvent($worker, $date) {
|
||||||
|
if (empty($worker->google_calendar_id) || empty($worker->lunch_window_start) || empty($worker->lunch_window_end)) return;
|
||||||
|
|
||||||
|
// Determine shift hours for this date
|
||||||
|
$override = $this->Service_model->getWorkerScheduleOverrideByDate($worker->worker_id, $date);
|
||||||
|
if ($override && $override->is_day_off) {
|
||||||
|
$this->_deleteLunchGcalEvent($worker, $date);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($override && $override->start_time && $override->end_time) {
|
||||||
|
$shiftStart = new DateTime($date . ' ' . $override->start_time);
|
||||||
|
$shiftEnd = new DateTime($date . ' ' . $override->end_time);
|
||||||
|
} else {
|
||||||
|
$phpWeekday = (int)date('w', strtotime($date));
|
||||||
|
$schedule = $this->Service_model->getWorkerScheduleByDay($worker->worker_id, $phpWeekday);
|
||||||
|
if (!$schedule) { $this->_deleteLunchGcalEvent($worker, $date); return; }
|
||||||
|
$shiftStart = new DateTime($date . ' ' . $schedule->start_time);
|
||||||
|
$shiftEnd = new DateTime($date . ' ' . $schedule->end_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dayBookings = $this->Service_model->getBookingsForWorkerDay($worker->worker_id, $date);
|
||||||
|
$lunch = $this->Service_model->computeLunchBreak(
|
||||||
|
$worker->lunch_window_start, $worker->lunch_window_end,
|
||||||
|
$date, $dayBookings, $shiftEnd, $shiftStart,
|
||||||
|
isset($worker->lunch_preferred_time) ? $worker->lunch_preferred_time : null
|
||||||
|
);
|
||||||
|
|
||||||
|
$existingEventId = $this->Service_model->getLunchGcalEventId($worker->worker_id, $date);
|
||||||
|
if ($lunch) {
|
||||||
|
$lunchStart = $date . 'T' . $lunch['start'] . ':00';
|
||||||
|
$lunchEnd = $date . 'T' . $lunch['end'] . ':00';
|
||||||
|
$title = 'Ebédszünet — ' . $worker->worker_name;
|
||||||
|
$desc = 'Automatikusan számított ebédszünet.';
|
||||||
|
if ($existingEventId) {
|
||||||
|
$this->googlecalendar->updateEvent($worker->google_calendar_id, $existingEventId, $title, $desc, $lunchStart, $lunchEnd);
|
||||||
|
} else {
|
||||||
|
$newId = $this->googlecalendar->createEvent($worker->google_calendar_id, $title, $desc, $lunchStart, $lunchEnd);
|
||||||
|
if ($newId) $this->Service_model->upsertLunchGcalEventId($worker->worker_id, $date, $newId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->_deleteLunchGcalEvent($worker, $date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _deleteLunchGcalEvent($worker, $date) {
|
||||||
|
if (empty($worker->google_calendar_id)) return;
|
||||||
|
$eventId = $this->Service_model->getLunchGcalEventId($worker->worker_id, $date);
|
||||||
|
if ($eventId) {
|
||||||
|
$this->googlecalendar->deleteEvent($worker->google_calendar_id, $eventId);
|
||||||
|
$this->Service_model->deleteLunchGcalEventRecord($worker->worker_id, $date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function _ntfy($title, $message, $workerTopic = null){
|
private function _ntfy($title, $message, $workerTopic = null){
|
||||||
$this->config->load('google_calendar');
|
$this->config->load('google_calendar');
|
||||||
$evelinTopic = $this->config->item('ntfy_topic');
|
$evelinTopic = $this->config->item('ntfy_topic');
|
||||||
|
|||||||
@@ -488,65 +488,18 @@ class Service_model extends CI_Model {
|
|||||||
log_message('error', '⏳ Min start (next 15min +1h, Europe/Oslo): ' . $dayStartTime->format('Y-m-d H:i:s'));
|
log_message('error', '⏳ Min start (next 15min +1h, Europe/Oslo): ' . $dayStartTime->format('Y-m-d H:i:s'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Lunch break: find free 30-min slot closest to preferred time within worker's lunch window ---
|
// --- Lunch break setup ---
|
||||||
$lunchBlockStart = null;
|
// Pre-fetch all bookings for the day so we can do hypothetical checks per slot
|
||||||
$lunchBlockEnd = null;
|
$workerRow = $this->db->query("SELECT lunch_window_start, lunch_window_end, lunch_preferred_time FROM workers WHERE worker_id = '$worker_id' LIMIT 1")->row();
|
||||||
|
|
||||||
$workerRow = $this->db->query("SELECT lunch_window_start, lunch_window_end, lunch_preferred_time FROM workers WHERE worker_id = '$worker_id' LIMIT 1")->row();
|
|
||||||
$shiftSeconds = $finishTime->getTimestamp() - $dayStartTime->getTimestamp();
|
$shiftSeconds = $finishTime->getTimestamp() - $dayStartTime->getTimestamp();
|
||||||
if ($workerRow && $workerRow->lunch_window_start && $workerRow->lunch_window_end && $shiftSeconds >= 6 * 3600) {
|
$lunchRequired = $workerRow && $workerRow->lunch_window_start && $workerRow->lunch_window_end && $shiftSeconds >= 6 * 3600;
|
||||||
|
|
||||||
|
$allBookings = [];
|
||||||
|
if ($lunchRequired) {
|
||||||
$allBookings = $this->db->query("
|
$allBookings = $this->db->query("
|
||||||
SELECT booking_start_time, booking_finish_time FROM bookings
|
SELECT booking_start_time, booking_finish_time FROM bookings
|
||||||
WHERE booking_date = '$selectedDate' AND worker_id = '$worker_id'
|
WHERE booking_date = '$selectedDate' AND worker_id = '$worker_id'
|
||||||
")->result();
|
")->result();
|
||||||
|
|
||||||
$lunchWinStart = new DateTime($selectedDate . ' ' . $workerRow->lunch_window_start);
|
|
||||||
$lunchWinEnd = new DateTime($selectedDate . ' ' . $workerRow->lunch_window_end);
|
|
||||||
$lunchDuration = new DateInterval('PT30M');
|
|
||||||
$lunchStep = new DateInterval('PT15M');
|
|
||||||
|
|
||||||
// Clamp window to actual shift hours
|
|
||||||
if ($lunchWinStart < $dayStartTime) $lunchWinStart = clone $dayStartTime;
|
|
||||||
if ($lunchWinEnd > $finishTime) $lunchWinEnd = clone $finishTime;
|
|
||||||
|
|
||||||
$scanEnd = (clone $lunchWinEnd)->sub($lunchDuration);
|
|
||||||
|
|
||||||
$isFree = function($c) use ($lunchDuration, $allBookings, $selectedDate) {
|
|
||||||
$cEnd = (clone $c)->add($lunchDuration);
|
|
||||||
foreach ($allBookings as $bk) {
|
|
||||||
$bkS = new DateTime($selectedDate . ' ' . $bk->booking_start_time);
|
|
||||||
$bkE = new DateTime($selectedDate . ' ' . $bk->booking_finish_time);
|
|
||||||
if ($bkS < $cEnd && $bkE > $c) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Collect free slots inside window, pick closest to preferred time
|
|
||||||
$freeSlots = [];
|
|
||||||
for ($c = clone $lunchWinStart; $c <= $scanEnd; $c->add($lunchStep)) {
|
|
||||||
if ($isFree($c)) $freeSlots[] = clone $c;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($freeSlots)) {
|
|
||||||
$prefDt = $workerRow->lunch_preferred_time
|
|
||||||
? new DateTime($selectedDate . ' ' . $workerRow->lunch_preferred_time)
|
|
||||||
: clone $lunchWinStart;
|
|
||||||
$best = null;
|
|
||||||
$bestDiff = PHP_INT_MAX;
|
|
||||||
foreach ($freeSlots as $slot) {
|
|
||||||
$diff = abs($slot->getTimestamp() - $prefDt->getTimestamp());
|
|
||||||
if ($diff < $bestDiff) { $bestDiff = $diff; $best = $slot; }
|
|
||||||
}
|
|
||||||
$lunchBlockStart = $best;
|
|
||||||
$lunchBlockEnd = (clone $best)->add($lunchDuration);
|
|
||||||
} else {
|
|
||||||
// Fallback: search after the window
|
|
||||||
for ($c = clone $lunchWinEnd; ; $c->add($lunchStep)) {
|
|
||||||
$candEnd = (clone $c)->add($lunchDuration);
|
|
||||||
if ($candEnd > $finishTime) break;
|
|
||||||
if ($isFree($c)) { $lunchBlockStart = clone $c; $lunchBlockEnd = $candEnd; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$interval = new DateInterval('PT15M');
|
$interval = new DateInterval('PT15M');
|
||||||
@@ -562,23 +515,33 @@ class Service_model extends CI_Model {
|
|||||||
$endTimeStr = $slotEnd->format('H:i:s');
|
$endTimeStr = $slotEnd->format('H:i:s');
|
||||||
|
|
||||||
$bookingQuery = $this->db->query("
|
$bookingQuery = $this->db->query("
|
||||||
SELECT * FROM bookings
|
SELECT * FROM bookings
|
||||||
WHERE booking_date = '{$selectedDate}'
|
WHERE booking_date = '{$selectedDate}'
|
||||||
AND worker_id = '{$worker_id}'
|
AND worker_id = '{$worker_id}'
|
||||||
AND (
|
AND booking_start_time < '{$endTimeStr}' AND booking_finish_time > '{$startTimeStr}'
|
||||||
(booking_start_time < '{$endTimeStr}' AND booking_finish_time > '{$startTimeStr}')
|
|
||||||
)
|
|
||||||
");
|
");
|
||||||
|
|
||||||
$overlapsLunch = $lunchBlockStart && ($current < $lunchBlockEnd) && ($slotEnd > $lunchBlockStart);
|
if ($bookingQuery->num_rows() > 0) continue; // already booked
|
||||||
if ($bookingQuery->num_rows() == 0 && !$overlapsLunch) {
|
|
||||||
$availableTimeArray[] = $startTimeStr;
|
if ($lunchRequired) {
|
||||||
|
// Check: if we book this slot, can a 30-min lunch break still fit somewhere?
|
||||||
|
$hypo = array_merge((array)$allBookings, [(object)[
|
||||||
|
'booking_start_time' => $startTimeStr,
|
||||||
|
'booking_finish_time' => $endTimeStr,
|
||||||
|
]]);
|
||||||
|
$canStillBreak = $this->computeLunchBreak(
|
||||||
|
$workerRow->lunch_window_start, $workerRow->lunch_window_end,
|
||||||
|
$selectedDate, $hypo, $finishTime, $dayStartTime,
|
||||||
|
$workerRow->lunch_preferred_time
|
||||||
|
) !== null;
|
||||||
|
if (!$canStillBreak) continue; // booking this slot would eliminate the only break opportunity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$availableTimeArray[] = $startTimeStr;
|
||||||
|
|
||||||
log_message('debug', 'Selected Date: ' . $selectedDate);
|
log_message('debug', 'Selected Date: ' . $selectedDate);
|
||||||
log_message('debug', 'Weekday: ' . (isset($weekday) ? $weekday : 'N/A'));
|
log_message('debug', 'Weekday: ' . (isset($weekday) ? $weekday : 'N/A'));
|
||||||
log_message('debug', 'Worker ID: ' . $worker_id);
|
log_message('debug', 'Worker ID: ' . $worker_id);
|
||||||
log_message('debug', 'Query: ' . $this->db->last_query());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $availableTimeArray;
|
return $availableTimeArray;
|
||||||
@@ -669,6 +632,36 @@ class Service_model extends CI_Model {
|
|||||||
$this->db->insert('worker_schedule_overrides', $override);
|
$this->db->insert('worker_schedule_overrides', $override);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBookingsForWorkerDay($worker_id, $date) {
|
||||||
|
$this->load->database();
|
||||||
|
return $this->db->query("
|
||||||
|
SELECT booking_start_time, booking_finish_time FROM bookings
|
||||||
|
WHERE worker_id = ? AND booking_date = ?
|
||||||
|
ORDER BY booking_start_time
|
||||||
|
", [$worker_id, $date])->result();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLunchGcalEventId($worker_id, $date) {
|
||||||
|
$this->load->database();
|
||||||
|
$row = $this->db->query("SELECT gcal_event_id FROM worker_lunch_gcal_events WHERE worker_id = ? AND date = ? LIMIT 1", [$worker_id, $date])->row();
|
||||||
|
return $row ? $row->gcal_event_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upsertLunchGcalEventId($worker_id, $date, $event_id) {
|
||||||
|
$this->load->database();
|
||||||
|
$existing = $this->getLunchGcalEventId($worker_id, $date);
|
||||||
|
if ($existing) {
|
||||||
|
$this->db->where('worker_id', $worker_id)->where('date', $date)->update('worker_lunch_gcal_events', ['gcal_event_id' => $event_id]);
|
||||||
|
} else {
|
||||||
|
$this->db->insert('worker_lunch_gcal_events', ['worker_id' => $worker_id, 'date' => $date, 'gcal_event_id' => $event_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteLunchGcalEventRecord($worker_id, $date) {
|
||||||
|
$this->load->database();
|
||||||
|
$this->db->where('worker_id', $worker_id)->where('date', $date)->delete('worker_lunch_gcal_events');
|
||||||
|
}
|
||||||
|
|
||||||
public function getBookingsForWorkerMonth($worker_id, $year, $month) {
|
public function getBookingsForWorkerMonth($worker_id, $year, $month) {
|
||||||
$this->load->database();
|
$this->load->database();
|
||||||
$yearMonth = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
|
$yearMonth = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||||
|
|||||||
Reference in New Issue
Block a user