Fix "Conflict. Timeslot is taken" raw-JSON error on booking
Customers intermittently hit a full-screen raw JSON error when booking:
{"error":"Conflict. Timeslot is taken or does not fit the service."}
booking_process() re-validates the chosen slot at submit time and returned
409/403 raw JSON. Because the public booking form is a full-page POST, that
JSON filled the whole screen.
The trigger is a double submit. After inserting the booking, booking_process()
synchronously runs two Google Calendar createEvent calls, a lunch sync, an ntfy
push and an SMTP confirmation e-mail before redirecting - several seconds - and
the submit button was never disabled. On mobile the guest taps "Send" again; the
second request arrives after the first has committed, so the slot reads as taken.
Evidence: 168 duplicate booking pairs exist in prod (same guest, slot and worker,
consecutive booking ids, including runs of four). All are from 2025, none from
2026 - the 409 guard added around May 2025 converted those silent duplicates
into today's visible error.
Prevent the double submit:
- disable the submit button and relabel it on first submit, ignore later ones
- add a hidden sendBooking field, since disabling a submit button can drop its
name/value from the POST and booking_process() bails to the homepage without it
Handle it gracefully when it still happens:
- new _booking_error() renders a localised page in the right skin instead of raw
JSON, replacing all six JSON responses in booking_process()
- new booking-error views for barber/beauty in no/en/hu, each with a message per
error case and a link back to booking
- new Service_model::getBookingBySlotAndGuest(); if the guest's own booking for
that exact slot already exists the submit is a duplicate rather than a real
conflict, so finish normally instead of erroring. Guarded on a non-empty
e-mail, as admin block bookings are stored with an empty guest_email.
No schema change. Verified on test, dev and prod: friendly page in all three
languages and both skins, double submit redirects to booking-finished without
creating a duplicate row, and no raw JSON in any response.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJso3iGT7TkW5tm4RSBohs
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
06966a898a
commit
0fd7f7a7a5
@@ -276,6 +276,34 @@ class Pages extends CI_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a guest-facing error page instead of raw JSON. The public booking
|
||||||
|
* 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){
|
||||||
|
$this->load->helper('url');
|
||||||
|
|
||||||
|
$lang = isset($_POST['lang']) && in_array($_POST['lang'], array('no', 'en', 'hu')) ? $_POST['lang'] : 'en';
|
||||||
|
$subpage = isset($_POST['subpage']) && $_POST['subpage'] != '' ? $_POST['subpage'] : 'barber';
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'pageTitle' => '',
|
||||||
|
'selectedLang' => $lang,
|
||||||
|
'subpage' => $subpage,
|
||||||
|
'bookingErrorCode' => $errorCode
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->output->set_status_header($statusCode);
|
||||||
|
|
||||||
|
if($subpage == 'beauty'){
|
||||||
|
$this->load->view('pages/beauty-booking-error', $data);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->load->view('pages/barber-booking-error', $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function booking_process(){
|
public function booking_process(){
|
||||||
$this->load->helper('url');
|
$this->load->helper('url');
|
||||||
$this->load->model('User_model');
|
$this->load->model('User_model');
|
||||||
@@ -355,26 +383,17 @@ class Pages extends CI_Controller {
|
|||||||
$schedule = $this->Service_model->getWorkerScheduleByDay($bookingArray['worker_id'], $weekday);
|
$schedule = $this->Service_model->getWorkerScheduleByDay($bookingArray['worker_id'], $weekday);
|
||||||
|
|
||||||
if (!$schedule) {
|
if (!$schedule) {
|
||||||
$this->output
|
$this->_booking_error('worker_unavailable', 403);
|
||||||
->set_status_header(403)
|
|
||||||
->set_content_type('application/json')
|
|
||||||
->set_output(json_encode(['error' => 'Worker is not available on this day.']));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($bookingTime < $schedule->start_time || $bookingTime >= $schedule->end_time) {
|
if ($bookingTime < $schedule->start_time || $bookingTime >= $schedule->end_time) {
|
||||||
$this->output
|
$this->_booking_error('outside_schedule', 403);
|
||||||
->set_status_header(403)
|
|
||||||
->set_content_type('application/json')
|
|
||||||
->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->Service_model->isWorkerAvailableThisWeek($bookingArray['worker_id'], $bookingDate)) {
|
if (!$this->Service_model->isWorkerAvailableThisWeek($bookingArray['worker_id'], $bookingDate)) {
|
||||||
$this->output
|
$this->_booking_error('alternate_week', 403);
|
||||||
->set_status_header(403)
|
|
||||||
->set_content_type('application/json')
|
|
||||||
->set_output(json_encode(['error' => 'Worker only works every second week.']));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,12 +405,27 @@ class Pages extends CI_Controller {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!is_array($available) || !in_array($bookingArray['booking_start_time'], $available)) {
|
if (!is_array($available) || !in_array($bookingArray['booking_start_time'], $available)) {
|
||||||
$this->output
|
// The guest may have submitted twice: the booking POST stays open for
|
||||||
->set_status_header(409)
|
// several seconds while Google Calendar and the confirmation e-mail
|
||||||
->set_content_type('application/json')
|
// run, so an impatient second tap arrives after the first already
|
||||||
->set_output(json_encode([
|
// saved. If their own booking for this exact slot exists, that is a
|
||||||
'error' => 'Conflict. Timeslot is taken or does not fit the service.'
|
// duplicate submit rather than a real conflict - finish normally.
|
||||||
]));
|
// Guard on a non-empty e-mail: admin-created block bookings are stored
|
||||||
|
// with an empty guest_email, and must never be mistaken for the guest's
|
||||||
|
// own duplicate submit.
|
||||||
|
$guestEmail = trim($bookingArray['guest_email']);
|
||||||
|
$ownBooking = $guestEmail !== '' ? $this->Service_model->getBookingBySlotAndGuest(
|
||||||
|
$bookingArray['worker_id'],
|
||||||
|
$bookingArray['booking_date'],
|
||||||
|
$bookingArray['booking_start_time'],
|
||||||
|
$guestEmail
|
||||||
|
) : false;
|
||||||
|
if($ownBooking){
|
||||||
|
header('Location:'.SITEURL.$_POST['lang'].'/booking-finished/'.$_POST['subpage']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_booking_error('slot_taken', 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,12 +441,7 @@ class Pages extends CI_Controller {
|
|||||||
$closingTime = new DateTime('18:00');
|
$closingTime = new DateTime('18:00');
|
||||||
|
|
||||||
if ($bookingEnd > $closingTime) {
|
if ($bookingEnd > $closingTime) {
|
||||||
$this->output
|
$this->_booking_error('after_hours', 403);
|
||||||
->set_status_header(403)
|
|
||||||
->set_content_type('application/json')
|
|
||||||
->set_output(json_encode([
|
|
||||||
'error' => 'Selected time exceeds business hours.'
|
|
||||||
]));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,12 +450,7 @@ class Pages extends CI_Controller {
|
|||||||
$selectedDate = new DateTime($bookingArray['booking_date']);
|
$selectedDate = new DateTime($bookingArray['booking_date']);
|
||||||
|
|
||||||
if ($selectedDate > $maxDate) {
|
if ($selectedDate > $maxDate) {
|
||||||
$this->output
|
$this->_booking_error('too_far', 403);
|
||||||
->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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -596,6 +596,12 @@ class Service_model extends CI_Model {
|
|||||||
return $query->num_rows() > 0 ? $query->result()[0] : false;
|
return $query->num_rows() > 0 ? $query->result()[0] : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBookingBySlotAndGuest($worker_id, $booking_date, $booking_start_time, $guest_email){
|
||||||
|
$this->load->database();
|
||||||
|
$query = $this->db->query('SELECT * FROM bookings WHERE worker_id = ? AND booking_date = ? AND booking_start_time = ? AND guest_email = ? ORDER BY booking_id DESC LIMIT 1', array($worker_id, $booking_date, $booking_start_time, $guest_email));
|
||||||
|
return $query->num_rows() > 0 ? $query->row() : false;
|
||||||
|
}
|
||||||
|
|
||||||
public function updateBookingCalEvents($booking_id, $worker_event_id, $owner_event_id){
|
public function updateBookingCalEvents($booking_id, $worker_event_id, $owner_event_id){
|
||||||
$this->load->database();
|
$this->load->database();
|
||||||
$this->db->query("UPDATE bookings SET gcal_event_id_worker = ?, gcal_event_id_owner = ? WHERE booking_id = ?", array($worker_event_id, $owner_event_id, $booking_id));
|
$this->db->query("UPDATE bookings SET gcal_event_id_worker = ?, gcal_event_id_owner = ? WHERE booking_id = ?", array($worker_event_id, $owner_event_id, $booking_id));
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
include(getcwd().'/application/views/includes/barber-skeleton-top.php');
|
||||||
|
$langArray = array('en', 'hu', 'no');
|
||||||
|
if(in_array($selectedLang, $langArray)){
|
||||||
|
include(getcwd().'/application/views/pages/includes/booking-error-form-'.$selectedLang.'.php');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
include(getcwd().'/application/views/pages/includes/booking-error-form-en.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
include(getcwd().'/application/views/includes/barber-skeleton-bottom.php');
|
||||||
|
?>
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
include(getcwd().'/application/views/includes/beauty-skeleton-top.php');
|
||||||
|
$langArray = array('en', 'hu', 'no');
|
||||||
|
if(in_array($selectedLang, $langArray)){
|
||||||
|
include(getcwd().'/application/views/pages/includes/booking-error-form-'.$selectedLang.'.php');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
include(getcwd().'/application/views/pages/includes/booking-error-form-en.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
include(getcwd().'/application/views/includes/beauty-skeleton-bottom.php');
|
||||||
|
?>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
$bookingErrorMessages = array(
|
||||||
|
'slot_taken' => 'This time slot was taken while you were filling in the form. Please choose another time.',
|
||||||
|
'worker_unavailable' => 'The selected staff member is not working on this day. Please choose another day.',
|
||||||
|
'outside_schedule' => 'The selected time is outside the staff member\'s working hours. Please choose another time.',
|
||||||
|
'alternate_week' => 'The selected staff member only works every second week. Please choose another day.',
|
||||||
|
'after_hours' => 'The treatment would not finish before closing time. Please choose an earlier time.',
|
||||||
|
'too_far' => 'Bookings can only be made up to 3 months in advance.',
|
||||||
|
'default' => 'Something went wrong with your booking. Please try again.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$shownError = isset($bookingErrorCode) && isset($bookingErrorMessages[$bookingErrorCode])
|
||||||
|
? $bookingErrorMessages[$bookingErrorCode]
|
||||||
|
: $bookingErrorMessages['default'];
|
||||||
|
|
||||||
|
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||||
|
$backUrl = SITEURL.'en/booking/'.$backSubpage;
|
||||||
|
?>
|
||||||
|
<div class="bookingContainer" style="min-height:500px;">
|
||||||
|
<div class="bookingThankYouTitle">This time is no longer available</div>
|
||||||
|
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||||
|
<div style="text-align:center; margin-top:30px;">
|
||||||
|
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Back to booking</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
$bookingErrorMessages = array(
|
||||||
|
'slot_taken' => 'Ezt az időpontot sajnos lefoglalták, amíg az űrlapot kitöltötte. Kérjük, válasszon másik időpontot.',
|
||||||
|
'worker_unavailable' => 'A kiválasztott kolléga ezen a napon nem dolgozik. Kérjük, válasszon másik napot.',
|
||||||
|
'outside_schedule' => 'A kiválasztott időpont a kolléga munkaidején kívül esik. Kérjük, válasszon másik időpontot.',
|
||||||
|
'alternate_week' => 'A kiválasztott kolléga csak kéthetente dolgozik. Kérjük, válasszon másik napot.',
|
||||||
|
'after_hours' => 'A kezelés nem érne véget zárásig. Kérjük, válasszon korábbi időpontot.',
|
||||||
|
'too_far' => 'Foglalás legfeljebb 3 hónappal előre adható le.',
|
||||||
|
'default' => 'Hiba történt a foglalás során. Kérjük, próbálja újra.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$shownError = isset($bookingErrorCode) && isset($bookingErrorMessages[$bookingErrorCode])
|
||||||
|
? $bookingErrorMessages[$bookingErrorCode]
|
||||||
|
: $bookingErrorMessages['default'];
|
||||||
|
|
||||||
|
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||||
|
$backUrl = SITEURL.'hu/booking/'.$backSubpage;
|
||||||
|
?>
|
||||||
|
<div class="bookingContainer" style="min-height:500px;">
|
||||||
|
<div class="bookingThankYouTitle">Ez az időpont már nem foglalható</div>
|
||||||
|
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||||
|
<div style="text-align:center; margin-top:30px;">
|
||||||
|
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Vissza a foglaláshoz</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
$bookingErrorMessages = array(
|
||||||
|
'slot_taken' => 'Dette tidspunktet ble dessverre opptatt mens du fylte ut skjemaet. Vennligst velg et annet tidspunkt.',
|
||||||
|
'worker_unavailable' => 'Den valgte medarbeideren jobber ikke denne dagen. Vennligst velg en annen dag.',
|
||||||
|
'outside_schedule' => 'Det valgte tidspunktet er utenfor medarbeiderens arbeidstid. Vennligst velg et annet tidspunkt.',
|
||||||
|
'alternate_week' => 'Den valgte medarbeideren jobber kun annenhver uke. Vennligst velg en annen dag.',
|
||||||
|
'after_hours' => 'Behandlingen rekker ikke å bli ferdig innen stengetid. Vennligst velg et tidligere tidspunkt.',
|
||||||
|
'too_far' => 'Du kan kun bestille time inntil 3 måneder frem i tid.',
|
||||||
|
'default' => 'Noe gikk galt med bestillingen. Vennligst prøv igjen.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$shownError = isset($bookingErrorCode) && isset($bookingErrorMessages[$bookingErrorCode])
|
||||||
|
? $bookingErrorMessages[$bookingErrorCode]
|
||||||
|
: $bookingErrorMessages['default'];
|
||||||
|
|
||||||
|
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||||
|
$backUrl = SITEURL.'no/booking/'.$backSubpage;
|
||||||
|
?>
|
||||||
|
<div class="bookingContainer" style="min-height:500px;">
|
||||||
|
<div class="bookingThankYouTitle">Tidspunktet er ikke lenger tilgjengelig</div>
|
||||||
|
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||||
|
<div style="text-align:center; margin-top:30px;">
|
||||||
|
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Tilbake til bestilling</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -187,6 +187,7 @@
|
|||||||
<div class="bookingButtonContainer">
|
<div class="bookingButtonContainer">
|
||||||
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Back"/>
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Back"/>
|
||||||
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
||||||
|
<input type="hidden" name="sendBooking" value="1">
|
||||||
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Send"/>
|
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Send"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -200,6 +201,19 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
|
||||||
|
// Prevent double submission. The booking POST stays open for several
|
||||||
|
// seconds (Google Calendar + confirmation e-mail), so an impatient
|
||||||
|
// second tap used to hit the server as a separate booking attempt.
|
||||||
|
var bookingSubmitInProgress = false;
|
||||||
|
$('#sendBooking').closest('form').on('submit', function(e){
|
||||||
|
if(bookingSubmitInProgress){
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bookingSubmitInProgress = true;
|
||||||
|
$('#sendBooking').prop('disabled', true).val('Sending...');
|
||||||
|
});
|
||||||
|
|
||||||
$('#booking_time').change(function(){
|
$('#booking_time').change(function(){
|
||||||
if($('#booking_time').val() != ''){
|
if($('#booking_time').val() != ''){
|
||||||
$('#isTimeSelectedBtn').prop('disabled', false);
|
$('#isTimeSelectedBtn').prop('disabled', false);
|
||||||
|
|||||||
@@ -188,6 +188,7 @@
|
|||||||
<div class="bookingButtonContainer">
|
<div class="bookingButtonContainer">
|
||||||
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Vissza"/>
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Vissza"/>
|
||||||
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
||||||
|
<input type="hidden" name="sendBooking" value="1">
|
||||||
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Elküld"/>
|
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Elküld"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,6 +200,19 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
|
||||||
|
// Prevent double submission. The booking POST stays open for several
|
||||||
|
// seconds (Google Calendar + confirmation e-mail), so an impatient
|
||||||
|
// second tap used to hit the server as a separate booking attempt.
|
||||||
|
var bookingSubmitInProgress = false;
|
||||||
|
$('#sendBooking').closest('form').on('submit', function(e){
|
||||||
|
if(bookingSubmitInProgress){
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bookingSubmitInProgress = true;
|
||||||
|
$('#sendBooking').prop('disabled', true).val('Küldés...');
|
||||||
|
});
|
||||||
|
|
||||||
$('#booking_time').change(function(){
|
$('#booking_time').change(function(){
|
||||||
if($('#booking_time').val() != ''){
|
if($('#booking_time').val() != ''){
|
||||||
$('#isTimeSelectedBtn').prop('disabled', false);
|
$('#isTimeSelectedBtn').prop('disabled', false);
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ if (is_array($services) && count($services) > 0) {
|
|||||||
<div class="bookingButtonContainer">
|
<div class="bookingButtonContainer">
|
||||||
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Tilbake"/>
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Tilbake"/>
|
||||||
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
||||||
|
<input type="hidden" name="sendBooking" value="1">
|
||||||
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Sende"/>
|
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Sende"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,6 +200,19 @@ if (is_array($services) && count($services) > 0) {
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
|
||||||
|
// Prevent double submission. The booking POST stays open for several
|
||||||
|
// seconds (Google Calendar + confirmation e-mail), so an impatient
|
||||||
|
// second tap used to hit the server as a separate booking attempt.
|
||||||
|
var bookingSubmitInProgress = false;
|
||||||
|
$('#sendBooking').closest('form').on('submit', function(e){
|
||||||
|
if(bookingSubmitInProgress){
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bookingSubmitInProgress = true;
|
||||||
|
$('#sendBooking').prop('disabled', true).val('Sender...');
|
||||||
|
});
|
||||||
|
|
||||||
$('#booking_time').change(function(){
|
$('#booking_time').change(function(){
|
||||||
if($('#booking_time').val() != ''){
|
if($('#booking_time').val() != ''){
|
||||||
$('#isTimeSelectedBtn').prop('disabled', false);
|
$('#isTimeSelectedBtn').prop('disabled', false);
|
||||||
|
|||||||
Reference in New Issue
Block a user