diff --git a/application/controllers/Pages.php b/application/controllers/Pages.php
index 6054b82..efcd721 100755
--- a/application/controllers/Pages.php
+++ b/application/controllers/Pages.php
@@ -460,6 +460,9 @@ class Pages extends CI_Controller {
$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
$gcalServiceNamesNtfy = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
$this->_ntfy(
@@ -703,339 +706,401 @@ class Pages extends CI_Controller {
}
}
-
- public function manage_booking($lang, $token){
- $this->load->helper('url');
- $this->load->model('Service_model');
-
- $booking = $this->Service_model->getBookingByToken($token);
- if(!$booking){
- show_404();
- return;
- }
-
- // Determine booking type from services
- $serviceIds = unserialize($booking->service_ids);
- $subpage = 'barber';
- if(is_array($serviceIds) && !empty($serviceIds)){
- $firstService = $this->Service_model->getServiceById($serviceIds[0]);
- if($firstService){
- $subpage = $firstService->service_type;
- }
- }
-
- $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
- $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
-
- $data['token'] = $token;
- $data['booking'] = $booking;
- $data['selectedLang'] = $lang;
- $data['subpage'] = $subpage;
- $data['pageTitle'] = '';
- $data['status'] = 'ok';
-
- if($appointmentDT < $now){
- $data['status'] = 'expired';
- } elseif(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
- $data['status'] = 'cutoff';
- }
-
- $data['services'] = $this->Service_model->getAllServiceByServiceType($subpage, $lang);
- $data['workers'] = $this->Service_model->getActiveWorkers($subpage);
- $data['selectedServiceIds'] = is_array($serviceIds) ? $serviceIds : array();
- $data['worker'] = $this->Service_model->getWorkerById($booking->worker_id);
-
- $this->load->view('pages/manage-booking', $data);
- }
-
- public function manage_booking_process(){
- $this->load->helper('url');
- $this->load->model('Service_model');
-
- if(!isset($_POST['token']) || !isset($_POST['sendBooking'])){
- header('Location:'.SITEURL);
- return;
- }
-
- $token = $_POST['token'];
- $booking = $this->Service_model->getBookingByToken($token);
- if(!$booking){
- show_404();
- return;
- }
-
- $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
- $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
- if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Modification cutoff has passed.']));
- return;
- }
-
- // Build selected service array and total length
- $selectedServiceArray = array();
- $totalLengthSeconds = 0;
- foreach($_POST as $key => $value){
- if(strpos($key, 'service_') === 0 && $value == '1'){
- $serviceId = str_replace('service_', '', $key);
- $selectedServiceArray[] = $serviceId;
- $service = $this->Service_model->getServiceById($serviceId);
- if($service){
- $parts = explode(':', $service->service_time);
- $totalLengthSeconds += ($parts[0] * 3600) + ($parts[1] * 60);
- }
- }
- }
- $servicelength = gmdate('H:i:s', $totalLengthSeconds);
-
- $newBookingDate = $_POST['booking_date'];
- $newStartTime = $_POST['booking_start_time'];
- $newWorkerId = $_POST['worker_id'];
- $lang = $_POST['lang'];
- $subpage = $_POST['subpage'];
-
- // Schedule check
- $weekday = date('w', strtotime($newBookingDate));
- $schedule = $this->Service_model->getWorkerScheduleByDay($newWorkerId, $weekday);
- if(!$schedule){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Worker is not available on this day.']));
- return;
- }
- if($newStartTime < $schedule->start_time || $newStartTime >= $schedule->end_time){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
- return;
- }
- if(!$this->Service_model->isWorkerAvailableThisWeek($newWorkerId, $newBookingDate)){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Worker only works every second week.']));
- return;
- }
-
- // Slot availability (temporarily exclude current booking)
- $this->Service_model->updateBooking($booking->booking_id, ['booking_date' => '1970-01-01']);
- $available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength);
- $this->Service_model->updateBooking($booking->booking_id, ['booking_date' => $booking->booking_date]);
-
- if(!is_array($available) || !in_array($newStartTime, $available)){
- $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.']));
- return;
- }
-
- // 3-month limit
- $today = new DateTime();
- $maxDate = (clone $today)->modify('+3 months');
- $selectedDate = new DateTime($newBookingDate);
- if($selectedDate > $maxDate){
- $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.']));
- return;
- }
-
- // Calculate finish time
- $bookingStart = new DateTime($newStartTime);
- $bookingLength = new DateTime($servicelength);
- $bookingEnd = clone $bookingStart;
- $bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('H')).'H'));
- $bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('i')).'M'));
- $closingTime = new DateTime('18:00');
- if($bookingEnd > $closingTime){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Selected time exceeds business hours.']));
- return;
- }
- $finishTime = $bookingEnd->format('H:i:s');
-
- // Update booking
- $this->Service_model->updateBooking($booking->booking_id, array(
- 'worker_id' => $newWorkerId,
- 'booking_date' => $newBookingDate,
- 'booking_start_time' => $newStartTime,
- 'booking_finish_time' => $finishTime,
- 'service_ids' => serialize($selectedServiceArray),
- ));
-
- $worker = $this->Service_model->getWorkerById($newWorkerId);
- $serviceArray = array();
- foreach($selectedServiceArray as $serviceItem){
- $sel = $this->Service_model->getServiceById($serviceItem);
- if(is_object($sel)) $serviceArray[] = $sel;
- }
-
- // Google Calendar: delete old events, create new ones
- $this->load->library('GoogleCalendar');
- $this->config->load('google_calendar');
- $evelinCalId = $this->config->item('gcal_evelin_calendar_id');
- $oldWorker = $this->Service_model->getWorkerById($booking->worker_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);
- }
- if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
- $this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
- }
- $gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
- $gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $booking->guest_name;
- $gcalDescription = 'Guest: ' . $booking->guest_name . "\nPhone: " . $booking->guest_phone . "\nEmail: " . $booking->guest_email;
- $gcalStart = $newBookingDate . 'T' . $newStartTime;
- $gcalEnd = $newBookingDate . 'T' . $finishTime;
- $newWorkerEventId = null;
- $newOwnerEventId = null;
- if(!empty($worker->google_calendar_id)){
- $newWorkerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
- }
- if(!empty($evelinCalId)){
- $newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
- }
- $this->Service_model->updateBookingCalEvents($booking->booking_id, $newWorkerEventId, $newOwnerEventId);
-
- // ntfy push notification
- $this->_ntfy(
- 'Modified booking: ' . $booking->guest_name,
- 'Worker: ' . $worker->worker_name . "\n" .
- 'Date: ' . $newBookingDate . ' ' . $newStartTime . ' - ' . $finishTime . "\n" .
- 'Services: ' . $gcalServiceNames,
- isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
- );
-
- $manageLink = SITEURL.$lang.'/manage-booking/'.$token;
- $totalServicePrice = 0;
- foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
-
- switch($lang){
- case 'no':
- $subject = '[studiobeve] Din bestilling er oppdatert';
- $content = 'Kjære gjest,
Din bestilling er oppdatert.
';
- $content .= '
';
- $content .= '| Arbeider: | '.$worker->worker_name.' |
';
- $content .= '| Dato: | '.$newBookingDate.' |
';
- $content .= '| Tid: | '.$newStartTime.' - '.$finishTime.' |
';
- $content .= '| Tjenester: | ';
- foreach($serviceArray as $s){ $content .= '| '.$s->service_name_no.' | '.$s->service_price.' kr | '; }
- $content .= ' |
| Totalpris: | '.$totalServicePrice.' kr |
';
- $content .= '
Administrer bestilling';
- $content .= '
STUDIOBEVE';
- break;
- case 'hu':
- $subject = '[studiobeve] A foglalásod módosítva lett';
- $content = 'Kedves Vendégünk,
A foglalásod módosítva lett.
';
- $content .= '';
- $content .= '| Dolgozó: | '.$worker->worker_name.' |
';
- $content .= '| Dátum: | '.$newBookingDate.' |
';
- $content .= '| Időpont: | '.$newStartTime.' - '.$finishTime.' |
';
- $content .= '| Szolgáltatás(ok): | ';
- foreach($serviceArray as $s){ $content .= '| '.$s->service_name_hu.' | '.$s->service_price.' kr | '; }
- $content .= ' |
| Összesen: | '.$totalServicePrice.' kr |
';
- $content .= '
Foglalás kezelése';
- $content .= '
STUDIOBEVE';
- break;
- default:
- $subject = '[studiobeve] Your booking has been updated';
- $content = 'Dear Guest,
Your booking has been updated.
';
- $content .= '';
- $content .= '| Worker: | '.$worker->worker_name.' |
';
- $content .= '| Date: | '.$newBookingDate.' |
';
- $content .= '| Time: | '.$newStartTime.' - '.$finishTime.' |
';
- $content .= '| Services: | ';
- foreach($serviceArray as $s){ $content .= '| '.$s->service_name_en.' | '.$s->service_price.' kr | '; }
- $content .= ' |
| Total price: | '.$totalServicePrice.' kr |
';
- $content .= '
Manage booking';
- $content .= '
STUDIOBEVE';
- }
-
- $emailArray = array(array(
- 'addressee_email' => $booking->guest_email,
- 'addressee_name' => $booking->guest_name,
- 'subject' => $subject,
- 'content' => $content,
- 'attachments' => array()
- ));
- $this->load->model('User_model');
- $this->User_model->sendEmail($emailArray, $lang, '');
-
- header('Location:'.SITEURL.$lang.'/booking-finished/'.$subpage);
- }
-
- public function manage_booking_cancel(){
- $this->load->helper('url');
- $this->load->model('Service_model');
- $this->load->model('User_model');
-
- if(!isset($_POST['token'])){
- header('Location:'.SITEURL);
- return;
- }
-
- $token = $_POST['token'];
- $lang = isset($_POST['lang']) ? $_POST['lang'] : 'en';
- $booking = $this->Service_model->getBookingByToken($token);
- if(!$booking){
- show_404();
- return;
- }
-
- $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
- $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
- if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
- $this->output->set_status_header(403)->set_content_type('application/json')
- ->set_output(json_encode(['error' => 'Cancellation cutoff has passed.']));
- return;
- }
-
- $worker = $this->Service_model->getWorkerById($booking->worker_id);
-
- switch($lang){
- case 'no':
- $subject = '[studiobeve] Din bestilling er avbestilt';
- $content = 'Kjære gjest,
Din reservasjon den '.$booking->booking_date.' kl. '.date('H:i', strtotime($booking->booking_start_time)).' med '.$worker->worker_name.' er avbestilt.
STUDIOBEVE';
- break;
- case 'hu':
- $subject = '[studiobeve] A foglalásod törölve lett';
- $content = 'Kedves Vendégünk,
A '.$booking->booking_date.' '.$booking->booking_start_time.'-os foglalásod '.$worker->worker_name.'-nál törölve lett.
STUDIOBEVE';
- break;
- default:
- $subject = '[studiobeve] Your booking has been cancelled';
- $content = 'Dear Guest,
Your booking on '.$booking->booking_date.' at '.date('H:i', strtotime($booking->booking_start_time)).' with '.$worker->worker_name.' has been cancelled.
STUDIOBEVE';
- }
-
- $emailArray = array(array(
- 'addressee_email' => $booking->guest_email,
- 'addressee_name' => $booking->guest_name,
- 'subject' => $subject,
- 'content' => $content,
- 'attachments' => array()
- ));
- $this->User_model->sendEmail($emailArray, $lang, '');
-
- // Google Calendar: delete events on cancellation
- $this->load->library('GoogleCalendar');
- $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);
- }
- $evelinCalId = $this->config->item('gcal_evelin_calendar_id');
- if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
- $this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
- }
-
- // ntfy push notification
- $this->_ntfy(
- 'Cancelled booking: ' . $booking->guest_name,
- 'Worker: ' . $worker->worker_name . "\n" .
- 'Date: ' . $booking->booking_date . ' ' . $booking->booking_start_time,
- isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
- );
-
- $this->Service_model->deleteBooking($booking->booking_id);
-
- $data['selectedLang'] = $lang;
- $data['booking'] = $booking;
- $data['worker'] = $worker;
- $data['pageTitle'] = '';
- $this->load->view('pages/manage-booking-cancelled', $data);
- }
-
+
+ public function manage_booking($lang, $token){
+ $this->load->helper('url');
+ $this->load->model('Service_model');
+
+ $booking = $this->Service_model->getBookingByToken($token);
+ if(!$booking){
+ show_404();
+ return;
+ }
+
+ // Determine booking type from services
+ $serviceIds = unserialize($booking->service_ids);
+ $subpage = 'barber';
+ if(is_array($serviceIds) && !empty($serviceIds)){
+ $firstService = $this->Service_model->getServiceById($serviceIds[0]);
+ if($firstService){
+ $subpage = $firstService->service_type;
+ }
+ }
+
+ $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
+ $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
+
+ $data['token'] = $token;
+ $data['booking'] = $booking;
+ $data['selectedLang'] = $lang;
+ $data['subpage'] = $subpage;
+ $data['pageTitle'] = '';
+ $data['status'] = 'ok';
+
+ if($appointmentDT < $now){
+ $data['status'] = 'expired';
+ } elseif(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
+ $data['status'] = 'cutoff';
+ }
+
+ $data['services'] = $this->Service_model->getAllServiceByServiceType($subpage, $lang);
+ $data['workers'] = $this->Service_model->getActiveWorkers($subpage);
+ $data['selectedServiceIds'] = is_array($serviceIds) ? $serviceIds : array();
+ $data['worker'] = $this->Service_model->getWorkerById($booking->worker_id);
+
+ $this->load->view('pages/manage-booking', $data);
+ }
+
+ public function manage_booking_process(){
+ $this->load->helper('url');
+ $this->load->model('Service_model');
+
+ if(!isset($_POST['token']) || !isset($_POST['sendBooking'])){
+ header('Location:'.SITEURL);
+ return;
+ }
+
+ $token = $_POST['token'];
+ $booking = $this->Service_model->getBookingByToken($token);
+ if(!$booking){
+ show_404();
+ return;
+ }
+
+ $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
+ $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
+ if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Modification cutoff has passed.']));
+ return;
+ }
+
+ // Build selected service array and total length
+ $selectedServiceArray = array();
+ $totalLengthSeconds = 0;
+ foreach($_POST as $key => $value){
+ if(strpos($key, 'service_') === 0 && $value == '1'){
+ $serviceId = str_replace('service_', '', $key);
+ $selectedServiceArray[] = $serviceId;
+ $service = $this->Service_model->getServiceById($serviceId);
+ if($service){
+ $parts = explode(':', $service->service_time);
+ $totalLengthSeconds += ($parts[0] * 3600) + ($parts[1] * 60);
+ }
+ }
+ }
+ $servicelength = gmdate('H:i:s', $totalLengthSeconds);
+
+ $newBookingDate = $_POST['booking_date'];
+ $newStartTime = $_POST['booking_start_time'];
+ $newWorkerId = $_POST['worker_id'];
+ $lang = $_POST['lang'];
+ $subpage = $_POST['subpage'];
+
+ // Schedule check
+ $weekday = date('w', strtotime($newBookingDate));
+ $schedule = $this->Service_model->getWorkerScheduleByDay($newWorkerId, $weekday);
+ if(!$schedule){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Worker is not available on this day.']));
+ return;
+ }
+ if($newStartTime < $schedule->start_time || $newStartTime >= $schedule->end_time){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
+ return;
+ }
+ if(!$this->Service_model->isWorkerAvailableThisWeek($newWorkerId, $newBookingDate)){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Worker only works every second week.']));
+ return;
+ }
+
+ // Slot availability (temporarily exclude current booking)
+ $this->Service_model->updateBooking($booking->booking_id, ['booking_date' => '1970-01-01']);
+ $available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength);
+ $this->Service_model->updateBooking($booking->booking_id, ['booking_date' => $booking->booking_date]);
+
+ if(!is_array($available) || !in_array($newStartTime, $available)){
+ $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.']));
+ return;
+ }
+
+ // 3-month limit
+ $today = new DateTime();
+ $maxDate = (clone $today)->modify('+3 months');
+ $selectedDate = new DateTime($newBookingDate);
+ if($selectedDate > $maxDate){
+ $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.']));
+ return;
+ }
+
+ // Calculate finish time
+ $bookingStart = new DateTime($newStartTime);
+ $bookingLength = new DateTime($servicelength);
+ $bookingEnd = clone $bookingStart;
+ $bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('H')).'H'));
+ $bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('i')).'M'));
+ $closingTime = new DateTime('18:00');
+ if($bookingEnd > $closingTime){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Selected time exceeds business hours.']));
+ return;
+ }
+ $finishTime = $bookingEnd->format('H:i:s');
+
+ // Update booking
+ $this->Service_model->updateBooking($booking->booking_id, array(
+ 'worker_id' => $newWorkerId,
+ 'booking_date' => $newBookingDate,
+ 'booking_start_time' => $newStartTime,
+ 'booking_finish_time' => $finishTime,
+ 'service_ids' => serialize($selectedServiceArray),
+ ));
+
+ $worker = $this->Service_model->getWorkerById($newWorkerId);
+ $serviceArray = array();
+ foreach($selectedServiceArray as $serviceItem){
+ $sel = $this->Service_model->getServiceById($serviceItem);
+ if(is_object($sel)) $serviceArray[] = $sel;
+ }
+
+ // Google Calendar: delete old events, create new ones
+ $this->load->library('GoogleCalendar');
+ $this->config->load('google_calendar');
+ $evelinCalId = $this->config->item('gcal_evelin_calendar_id');
+ $oldWorker = $this->Service_model->getWorkerById($booking->worker_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);
+ }
+ if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
+ $this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
+ }
+ $gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
+ $gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $booking->guest_name;
+ $gcalDescription = 'Guest: ' . $booking->guest_name . "\nPhone: " . $booking->guest_phone . "\nEmail: " . $booking->guest_email;
+ $gcalStart = $newBookingDate . 'T' . $newStartTime;
+ $gcalEnd = $newBookingDate . 'T' . $finishTime;
+ $newWorkerEventId = null;
+ $newOwnerEventId = null;
+ if(!empty($worker->google_calendar_id)){
+ $newWorkerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
+ }
+ if(!empty($evelinCalId)){
+ $newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
+ }
+ $this->Service_model->updateBookingCalEvents($booking->booking_id, $newWorkerEventId, $newOwnerEventId);
+
+ // Sync lunch break calendar event (new date, and old date if it changed)
+ $this->_syncLunchCalendarEvent($worker, $newBookingDate);
+ if ($booking->booking_date !== $newBookingDate) {
+ $this->_syncLunchCalendarEvent($worker, $booking->booking_date);
+ }
+
+ // ntfy push notification
+ $this->_ntfy(
+ 'Modified booking: ' . $booking->guest_name,
+ 'Worker: ' . $worker->worker_name . "\n" .
+ 'Date: ' . $newBookingDate . ' ' . $newStartTime . ' - ' . $finishTime . "\n" .
+ 'Services: ' . $gcalServiceNames,
+ isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
+ );
+
+ $manageLink = SITEURL.$lang.'/manage-booking/'.$token;
+ $totalServicePrice = 0;
+ foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
+
+ switch($lang){
+ case 'no':
+ $subject = '[studiobeve] Din bestilling er oppdatert';
+ $content = 'Kjære gjest,
Din bestilling er oppdatert.
';
+ $content .= '';
+ $content .= '| Arbeider: | '.$worker->worker_name.' |
';
+ $content .= '| Dato: | '.$newBookingDate.' |
';
+ $content .= '| Tid: | '.$newStartTime.' - '.$finishTime.' |
';
+ $content .= '| Tjenester: | ';
+ foreach($serviceArray as $s){ $content .= '| '.$s->service_name_no.' | '.$s->service_price.' kr | '; }
+ $content .= ' |
| Totalpris: | '.$totalServicePrice.' kr |
';
+ $content .= '
Administrer bestilling';
+ $content .= '
STUDIOBEVE';
+ break;
+ case 'hu':
+ $subject = '[studiobeve] A foglalásod módosítva lett';
+ $content = 'Kedves Vendégünk,
A foglalásod módosítva lett.
';
+ $content .= '';
+ $content .= '| Dolgozó: | '.$worker->worker_name.' |
';
+ $content .= '| Dátum: | '.$newBookingDate.' |
';
+ $content .= '| Időpont: | '.$newStartTime.' - '.$finishTime.' |
';
+ $content .= '| Szolgáltatás(ok): | ';
+ foreach($serviceArray as $s){ $content .= '| '.$s->service_name_hu.' | '.$s->service_price.' kr | '; }
+ $content .= ' |
| Összesen: | '.$totalServicePrice.' kr |
';
+ $content .= '
Foglalás kezelése';
+ $content .= '
STUDIOBEVE';
+ break;
+ default:
+ $subject = '[studiobeve] Your booking has been updated';
+ $content = 'Dear Guest,
Your booking has been updated.
';
+ $content .= '';
+ $content .= '| Worker: | '.$worker->worker_name.' |
';
+ $content .= '| Date: | '.$newBookingDate.' |
';
+ $content .= '| Time: | '.$newStartTime.' - '.$finishTime.' |
';
+ $content .= '| Services: | ';
+ foreach($serviceArray as $s){ $content .= '| '.$s->service_name_en.' | '.$s->service_price.' kr | '; }
+ $content .= ' |
| Total price: | '.$totalServicePrice.' kr |
';
+ $content .= '
Manage booking';
+ $content .= '
STUDIOBEVE';
+ }
+
+ $emailArray = array(array(
+ 'addressee_email' => $booking->guest_email,
+ 'addressee_name' => $booking->guest_name,
+ 'subject' => $subject,
+ 'content' => $content,
+ 'attachments' => array()
+ ));
+ $this->load->model('User_model');
+ $this->User_model->sendEmail($emailArray, $lang, '');
+
+ header('Location:'.SITEURL.$lang.'/booking-finished/'.$subpage);
+ }
+
+ public function manage_booking_cancel(){
+ $this->load->helper('url');
+ $this->load->model('Service_model');
+ $this->load->model('User_model');
+
+ if(!isset($_POST['token'])){
+ header('Location:'.SITEURL);
+ return;
+ }
+
+ $token = $_POST['token'];
+ $lang = isset($_POST['lang']) ? $_POST['lang'] : 'en';
+ $booking = $this->Service_model->getBookingByToken($token);
+ if(!$booking){
+ show_404();
+ return;
+ }
+
+ $now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
+ $appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
+ if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
+ $this->output->set_status_header(403)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'Cancellation cutoff has passed.']));
+ return;
+ }
+
+ $worker = $this->Service_model->getWorkerById($booking->worker_id);
+
+ switch($lang){
+ case 'no':
+ $subject = '[studiobeve] Din bestilling er avbestilt';
+ $content = 'Kjære gjest,
Din reservasjon den '.$booking->booking_date.' kl. '.date('H:i', strtotime($booking->booking_start_time)).' med '.$worker->worker_name.' er avbestilt.
STUDIOBEVE';
+ break;
+ case 'hu':
+ $subject = '[studiobeve] A foglalásod törölve lett';
+ $content = 'Kedves Vendégünk,
A '.$booking->booking_date.' '.$booking->booking_start_time.'-os foglalásod '.$worker->worker_name.'-nál törölve lett.
STUDIOBEVE';
+ break;
+ default:
+ $subject = '[studiobeve] Your booking has been cancelled';
+ $content = 'Dear Guest,
Your booking on '.$booking->booking_date.' at '.date('H:i', strtotime($booking->booking_start_time)).' with '.$worker->worker_name.' has been cancelled.
STUDIOBEVE';
+ }
+
+ $emailArray = array(array(
+ 'addressee_email' => $booking->guest_email,
+ 'addressee_name' => $booking->guest_name,
+ 'subject' => $subject,
+ 'content' => $content,
+ 'attachments' => array()
+ ));
+ $this->User_model->sendEmail($emailArray, $lang, '');
+
+ // Google Calendar: delete events on cancellation
+ $this->load->library('GoogleCalendar');
+ $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);
+ }
+ $evelinCalId = $this->config->item('gcal_evelin_calendar_id');
+ if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
+ $this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
+ }
+
+ // ntfy push notification
+ $this->_ntfy(
+ 'Cancelled booking: ' . $booking->guest_name,
+ 'Worker: ' . $worker->worker_name . "\n" .
+ 'Date: ' . $booking->booking_date . ' ' . $booking->booking_start_time,
+ isset($worker->ntfy_topic) ? $worker->ntfy_topic : null
+ );
+
+ $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){
$this->config->load('google_calendar');
$evelinTopic = $this->config->item('ntfy_topic');
diff --git a/application/models/Service_model.php b/application/models/Service_model.php
index 0b2d277..9c37b3e 100755
--- a/application/models/Service_model.php
+++ b/application/models/Service_model.php
@@ -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'));
}
- // --- Lunch break: find free 30-min slot closest to preferred time within worker's lunch window ---
- $lunchBlockStart = null;
- $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();
+ // --- Lunch break setup ---
+ // Pre-fetch all bookings for the day so we can do hypothetical checks per slot
+ $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();
- 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("
SELECT booking_start_time, booking_finish_time FROM bookings
WHERE booking_date = '$selectedDate' AND worker_id = '$worker_id'
")->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');
@@ -562,23 +515,33 @@ class Service_model extends CI_Model {
$endTimeStr = $slotEnd->format('H:i:s');
$bookingQuery = $this->db->query("
- SELECT * FROM bookings
- WHERE booking_date = '{$selectedDate}'
- AND worker_id = '{$worker_id}'
- AND (
- (booking_start_time < '{$endTimeStr}' AND booking_finish_time > '{$startTimeStr}')
- )
+ SELECT * FROM bookings
+ WHERE booking_date = '{$selectedDate}'
+ AND worker_id = '{$worker_id}'
+ AND booking_start_time < '{$endTimeStr}' AND booking_finish_time > '{$startTimeStr}'
");
- $overlapsLunch = $lunchBlockStart && ($current < $lunchBlockEnd) && ($slotEnd > $lunchBlockStart);
- if ($bookingQuery->num_rows() == 0 && !$overlapsLunch) {
- $availableTimeArray[] = $startTimeStr;
+ if ($bookingQuery->num_rows() > 0) continue; // already booked
+
+ 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', 'Weekday: ' . (isset($weekday) ? $weekday : 'N/A'));
log_message('debug', 'Worker ID: ' . $worker_id);
- log_message('debug', 'Query: ' . $this->db->last_query());
}
return $availableTimeArray;
@@ -669,6 +632,36 @@ class Service_model extends CI_Model {
$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) {
$this->load->database();
$yearMonth = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);