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);
|
||||
}
|
||||
|
||||
// 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,<br />Din bestilling er oppdatert.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$content .= '<tr><td>Dato: </td><td>'.$newBookingDate.'</td></tr>';
|
||||
$content .= '<tr><td>Tid: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||
$content .= '<tr><td>Tjenester: </td><td><table>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_no.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Administrer bestilling</a>';
|
||||
$content .= '<br /><br /> STUDIOBEVE';
|
||||
break;
|
||||
case 'hu':
|
||||
$subject = '[studiobeve] A foglalásod módosítva lett';
|
||||
$content = 'Kedves Vendégünk,<br />A foglalásod módosítva lett.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$content .= '<tr><td>Dátum: </td><td>'.$newBookingDate.'</td></tr>';
|
||||
$content .= '<tr><td>Időpont: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_hu.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Összesen: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Foglalás kezelése</a>';
|
||||
$content .= '<br /><br /> STUDIOBEVE';
|
||||
break;
|
||||
default:
|
||||
$subject = '[studiobeve] Your booking has been updated';
|
||||
$content = 'Dear Guest,<br />Your booking has been updated.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$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>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_en.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Manage booking</a>';
|
||||
$content .= '<br /><br /> 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,<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;
|
||||
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';
|
||||
break;
|
||||
default:
|
||||
$subject = '[studiobeve] Your booking has been cancelled';
|
||||
$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';
|
||||
}
|
||||
|
||||
$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,<br />Din bestilling er oppdatert.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$content .= '<tr><td>Dato: </td><td>'.$newBookingDate.'</td></tr>';
|
||||
$content .= '<tr><td>Tid: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||
$content .= '<tr><td>Tjenester: </td><td><table>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_no.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Administrer bestilling</a>';
|
||||
$content .= '<br /><br /> STUDIOBEVE';
|
||||
break;
|
||||
case 'hu':
|
||||
$subject = '[studiobeve] A foglalásod módosítva lett';
|
||||
$content = 'Kedves Vendégünk,<br />A foglalásod módosítva lett.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$content .= '<tr><td>Dátum: </td><td>'.$newBookingDate.'</td></tr>';
|
||||
$content .= '<tr><td>Időpont: </td><td>'.$newStartTime.' - '.$finishTime.'</td></tr>';
|
||||
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_hu.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Összesen: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Foglalás kezelése</a>';
|
||||
$content .= '<br /><br /> STUDIOBEVE';
|
||||
break;
|
||||
default:
|
||||
$subject = '[studiobeve] Your booking has been updated';
|
||||
$content = 'Dear Guest,<br />Your booking has been updated.<br /><br />';
|
||||
$content .= '<table>';
|
||||
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
|
||||
$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>';
|
||||
foreach($serviceArray as $s){ $content .= '<tr><td>'.$s->service_name_en.'</td><td>'.$s->service_price.' kr</td></tr>'; }
|
||||
$content .= '</table></td><tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr></table>';
|
||||
$content .= '<br /><br /><a href="'.$manageLink.'">Manage booking</a>';
|
||||
$content .= '<br /><br /> 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,<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;
|
||||
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';
|
||||
break;
|
||||
default:
|
||||
$subject = '[studiobeve] Your booking has been cancelled';
|
||||
$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';
|
||||
}
|
||||
|
||||
$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');
|
||||
|
||||
Reference in New Issue
Block a user