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 .= ''; - $content .= ''; - $content .= ''; - $content .= '
Arbeider: '.$worker->worker_name.'
Dato: '.$newBookingDate.'
Tid: '.$newStartTime.' - '.$finishTime.'
Tjenester: '; - foreach($serviceArray as $s){ $content .= ''; } - $content .= '
'.$s->service_name_no.''.$s->service_price.' kr
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 .= ''; - $content .= ''; - $content .= ''; - $content .= '
Dolgozó: '.$worker->worker_name.'
Dátum: '.$newBookingDate.'
Időpont: '.$newStartTime.' - '.$finishTime.'
Szolgáltatás(ok): '; - foreach($serviceArray as $s){ $content .= ''; } - $content .= '
'.$s->service_name_hu.''.$s->service_price.' kr
Ö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 .= ''; - $content .= ''; - $content .= ''; - $content .= '
Worker: '.$worker->worker_name.'
Date: '.$newBookingDate.'
Time: '.$newStartTime.' - '.$finishTime.'
Services: '; - foreach($serviceArray as $s){ $content .= ''; } - $content .= '
'.$s->service_name_en.''.$s->service_price.' kr
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 .= ''; + $content .= ''; + $content .= ''; + $content .= '
Arbeider: '.$worker->worker_name.'
Dato: '.$newBookingDate.'
Tid: '.$newStartTime.' - '.$finishTime.'
Tjenester: '; + foreach($serviceArray as $s){ $content .= ''; } + $content .= '
'.$s->service_name_no.''.$s->service_price.' kr
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 .= ''; + $content .= ''; + $content .= ''; + $content .= '
Dolgozó: '.$worker->worker_name.'
Dátum: '.$newBookingDate.'
Időpont: '.$newStartTime.' - '.$finishTime.'
Szolgáltatás(ok): '; + foreach($serviceArray as $s){ $content .= ''; } + $content .= '
'.$s->service_name_hu.''.$s->service_price.' kr
Ö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 .= ''; + $content .= ''; + $content .= ''; + $content .= '
Worker: '.$worker->worker_name.'
Date: '.$newBookingDate.'
Time: '.$newStartTime.' - '.$finishTime.'
Services: '; + foreach($serviceArray as $s){ $content .= ''; } + $content .= '
'.$s->service_name_en.''.$s->service_price.' kr
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);