Apply the worker capability filter to the manage-booking flow
The per-worker service allow-list was only enforced on the new-booking form. On the guest manage-booking page a worker could still be offered for services they do not perform: - manage_booking() branched on $firstService, which was never assigned - the resolve loop above it used $resolvedService - so the guarded branch was dead and the worker list fell through to getActiveWorkers(), filtered by vertical only. Assigning it activates both the category filter and the capability check, and with it the $isOtherCategory checkbox disabling in the view. - The page's own getAvailableWorkersByServiceCategorySlug() never sent service_ids, so re-picking services asked for a category-only worker list. It now mirrors the booking form, including the empty-result message. - setWorker() clears any time already chosen: changing worker or services left a stale selection with the submit button still enabled. manage_booking_process() did re-check capability server-side, but every rejection there answered with raw JSON, which this non-AJAX form renders as an unstyled blob in the guest's browser. All eight rejections, plus the cancellation cutoff, now go through _booking_error(), which takes an optional back URL so the guest returns to their own manage page rather than an empty booking form. Adds the cutoff_passed message in all three languages and makes the error title and button label reflect which flow failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYGSbK1erKv7VG1pvdPEjG
This commit is contained in:
@@ -315,7 +315,7 @@ class Pages extends CI_Controller {
|
||||
* form is a normal full-page POST, so whatever this outputs is what the
|
||||
* customer actually sees in their browser.
|
||||
*/
|
||||
private function _booking_error($errorCode, $statusCode = 409){
|
||||
private function _booking_error($errorCode, $statusCode = 409, $backUrl = ''){
|
||||
$this->load->helper('url');
|
||||
|
||||
$lang = vertical_lang(isset($_POST['lang']) ? $_POST['lang'] : '');
|
||||
@@ -332,7 +332,10 @@ class Pages extends CI_Controller {
|
||||
'selectedLang' => $lang,
|
||||
'subpage' => $vertical['slug'],
|
||||
'vertical' => $vertical,
|
||||
'bookingErrorCode' => $errorCode
|
||||
'bookingErrorCode' => $errorCode,
|
||||
// Manage-booking errors must return the guest to their own manage page,
|
||||
// not to a fresh booking form. Empty = the new-booking default.
|
||||
'bookingBackUrl' => $backUrl
|
||||
);
|
||||
|
||||
$this->output->set_status_header($statusCode);
|
||||
@@ -854,10 +857,14 @@ class Pages extends CI_Controller {
|
||||
// wrong vertical's branding.
|
||||
$serviceIds = unserialize($booking->service_ids);
|
||||
$subpage = 'barber';
|
||||
$firstService = NULL;
|
||||
if(is_array($serviceIds)){
|
||||
foreach($serviceIds as $serviceIdItem){
|
||||
$resolvedService = $this->Service_model->getServiceById($serviceIdItem);
|
||||
if($resolvedService && $resolvedService->service_type !== ''){
|
||||
// Kept for the worker filter below: the category (and therefore the
|
||||
// capability check) is derived from this same resolved service.
|
||||
$firstService = $resolvedService;
|
||||
$subpage = $resolvedService->service_type;
|
||||
break;
|
||||
}
|
||||
@@ -893,7 +900,7 @@ class Pages extends CI_Controller {
|
||||
// Filter workers to those qualified for the booking's service category,
|
||||
// so the guest can't switch to a worker who doesn't perform these services.
|
||||
$categorySlug = '';
|
||||
if(isset($firstService) && $firstService){
|
||||
if($firstService){
|
||||
$category = $this->Service_model->getServiceCategoryById($firstService->service_category_id);
|
||||
if($category){
|
||||
$categorySlug = $category->serv_cat_slug;
|
||||
@@ -928,11 +935,17 @@ class Pages extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
// This form is a normal POST, not AJAX, so every rejection below is rendered
|
||||
// as the shared booking error page. Returning raw JSON showed the guest an
|
||||
// unstyled {"error":...} blob after they had filled the whole form in.
|
||||
// Use the stored token, not the posted one: it is echoed straight into the
|
||||
// error page's back link.
|
||||
$manageBackUrl = SITEURL.vertical_lang(isset($_POST['lang']) ? $_POST['lang'] : '').'/manage-booking/'.$booking->manage_token;
|
||||
|
||||
$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.']));
|
||||
$this->_booking_error('cutoff_passed', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -963,8 +976,7 @@ class Pages extends CI_Controller {
|
||||
if(!empty($selectedServiceArray) && $submittedWorker){
|
||||
$firstSelectedService = $this->Service_model->getServiceById($selectedServiceArray[0]);
|
||||
if($firstSelectedService && $firstSelectedService->service_category_id != $submittedWorker->service_category_id){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Selected worker does not perform the chosen services.']));
|
||||
$this->_booking_error('category_mismatch', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -973,26 +985,22 @@ class Pages extends CI_Controller {
|
||||
// category check proves only that the services belong to the worker's
|
||||
// category, not that this worker performs them.
|
||||
if(!$this->Service_model->workerCanPerformServices($newWorkerId, $selectedServiceArray)){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Selected worker does not perform the chosen services.']));
|
||||
$this->_booking_error('service_not_offered', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule check
|
||||
$schedule = $this->Service_model->getWorkerScheduleForDate($newWorkerId, $newBookingDate);
|
||||
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.']));
|
||||
$this->_booking_error('worker_unavailable', 403, $manageBackUrl);
|
||||
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.']));
|
||||
$this->_booking_error('outside_schedule', 403, $manageBackUrl);
|
||||
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.']));
|
||||
$this->_booking_error('alternate_week', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1000,8 +1008,7 @@ class Pages extends CI_Controller {
|
||||
$available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength, $booking->booking_id);
|
||||
|
||||
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.']));
|
||||
$this->_booking_error('slot_taken', 409, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1010,8 +1017,7 @@ class Pages extends CI_Controller {
|
||||
$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.']));
|
||||
$this->_booking_error('too_far', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1023,8 +1029,7 @@ class Pages extends CI_Controller {
|
||||
$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.']));
|
||||
$this->_booking_error('after_hours', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
$finishTime = $bookingEnd->format('H:i:s');
|
||||
@@ -1186,8 +1191,7 @@ class Pages extends CI_Controller {
|
||||
$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.']));
|
||||
$this->_booking_error('cutoff_passed', 403, SITEURL.vertical_lang($lang).'/manage-booking/'.$booking->manage_token);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user