Files
studiobeve.no/application/views/pages/includes/booking-form-hu.php
T
UbuntuandClaude Opus 5 13e1b1a672 Add Amarildo to the barber page and gate workers by service
New barber Amarildo Champimpi is introduced on the barber page in all three
languages, and workers can now be restricted to a subset of the services in
their category.

Amarildo cannot perform beard colouring, any waxing, or the all-in package, and
he is a barber only - but category 1 "Kozmetika" holds 19 barber AND 27 beauty
services, so the category-derived vertical wrongly made him beauty-capable.
Because the worker picker is only ever fetched AFTER services are ticked, one
per-service capability filter solves both problems: excluding him from every
beauty service removes him from that vertical entirely.

worker_services(worker_id, service_id) is an allow-list where an EMPTY set means
UNRESTRICTED. That default is deliberate: a missing migration degrades to the
previous behaviour instead of hiding every worker from the booking flow, and
existing workers keep working untouched. Ticking every box in the admin grid
stores nothing at all, so an unrestricted worker also picks up services added
later; unticking even one makes the worker restricted, and new services must
then be granted explicitly.

Enforcement is in three places. The picker offers only workers who can perform
EVERY selected service, and both booking paths re-check server-side, since the
picker is only a UI affordance - a crafted POST now gets service_not_offered/403
rather than a booking the worker cannot honour.

Fixed alongside, all found while building the above:

- getWorkersByCategorySlug() never filtered is_active, so marking a worker
  inactive had NO effect on the public booking flow. Both of its callers are
  guest-facing. The sibling fallback getActiveWorkers() had always filtered it.
- Worker profile picture uploads failed SILENTLY above PHP's upload_max_filesize.
  Both upload blocks gated on tmp_name alone, which cannot distinguish a rejected
  upload from "no file chosen" - PHP empties tmp_name in both cases - so the
  worker was saved with an empty worker_profile_img and no error shown. The
  upload error code is now read and reported, and a separate guard catches
  post_max_size overflow, where $_POST and $_FILES both arrive empty and the form
  silently did nothing at all.
- createWorker() omitted is_active from its INSERT, so the column default (1)
  always won and a worker created as inactive silently came back active.

Migration - the table MUST be created before this code is deployed, because the
picker query subselects it whenever service ids are passed:

    CREATE TABLE worker_services (
      worker_id  INT NOT NULL,
      service_id INT NOT NULL,
      PRIMARY KEY (worker_id, service_id),
      KEY idx_worker_services_worker (worker_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Already applied on test, dev and prod. Server-side, upload_max_filesize/
post_max_size were raised to 8M/12M on all three environments (php.ini on test,
.user.ini on the shared-host dev and prod docroots) - not carried by this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYGSbK1erKv7VG1pvdPEjG
2026-08-24 12:07:07 +00:00

556 lines
23 KiB
PHP
Executable File

<div class="bookingContainer">
<div class="bookingTitle">Foglalási folyamat</div>
<ul class="steps-indicator steps-3">
<li>
<div class="stepTitleContainer">
<div class="stepLine"></div>
<div class="stepCircle activeStep" id="stepCircle1"></div>
<div class="stepTitle">LÉPÉS 1</div>
</div>
</li>
<li>
<div class="stepTitleContainer">
<div class="stepLine"></div>
<div class="stepCircle" id="stepCircle2"></div>
<div class="stepTitle">LÉPÉS 2</div>
</div>
</li>
<li>
<div class="stepTitleContainer">
<div class="stepCircle" id="stepCircle3"></div>
<div class="stepTitle">LÉPÉS 3</div>
</div>
<li>
</ul>
<div class="totalPanelContainer">
<div id="totalPanel">
<div class="serviceBookingDateContainer">
<div id="serviceBookingDate"></div>
<div id="serviceBookingTime"></div>
</div>
<div class="serviceBookingWorkerContainer">
</div>
<div class="serviceContent">
</div>
<div class="totalSelectedTime"></div>
<div class="totalPrice"></div>
</div>
<div id="totalPanelBtnContainer">
<div id="totalPanelBtn"><i class="fas fa-caret-right"></i></div>
</div>
</div>
<form method="post" action="<?php echo SITEURL;?>booking-process">
<div class="BookingStepContainer" id="bookingStep1">
<div class="totalTime"><input type="time" id="servicelength" name="servicelength" value="00:00"></div>
<div class="bookingTable">
<div class="panel panel-primary">
<div class="panel-heading">Szolgáltatások</div>
</div>
</div>
<div class="serviceTable">
<div class="serviceHeader">
<div class="serviceCol serviceNameCol">Szolgáltatás neve</div>
<div class="serviceCol"></div>
<div class="serviceCol servicePriceCol">Ára</div>
<div class="serviceCol serviceInfoCol"></div>
<div class="serviceCol serviceCheckCol"></div>
</div>
<?php
function canon_cat($s) {
$s = html_entity_decode((string)$s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$s = strip_tags($s); // removes "<p class=" junk etc.
return trim($s); // removes trailing/leading spaces
}
if (is_array($services) && count($services) > 0) {
// Build groups in first-seen order
$groups = []; // label => [items]
$order = []; // labels in order of first appearance
foreach ($services as $serviceItem) {
$label = canon_cat($serviceItem->service_category ?? '');
if ($label === '') $label = 'Other';
if (!isset($groups[$label])) {
$groups[$label] = [];
$order[] = $label;
}
$groups[$label][] = $serviceItem;
}
// Render
foreach ($order as $label) {
?>
<div class="serviceRow">
<div class="serviceCol" style="width: 100%">
<h4><?php echo $label; ?></h4>
</div>
</div>
<?php
foreach ($groups[$label] as $serviceItem) {
?>
<div class="serviceRow">
<div class="serviceCol serviceNameCol" id="serviceName_<?php echo $serviceItem->service_id; ?>" data-service-name="<?php echo htmlspecialchars($serviceItem->service_name, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo vertical_service_label(isset($vertical) ? $vertical : NULL, $serviceItem); ?>
</div>
<div class="serviceCol">
<input type="time" class="serviceTime" id="time_<?php echo $serviceItem->service_id; ?>" value="<?php echo $serviceItem->service_time; ?>"/>
</div>
<div class="serviceCol servicePriceCol" id="servicePrice_<?php echo $serviceItem->service_id; ?>">
<?php echo $serviceItem->service_price; ?>
</div>
<div class="serviceCol serviceInfoCol">
<div class="serviceInfoIcon">
<i class="fas fa-info-circle"></i>
<div class="serviceInfoLabel"><?php echo $serviceItem->service_description; ?></div>
</div>
</div>
<div class="serviceCol serviceCheckCol">
<input type="hidden" name="service_<?php echo $serviceItem->service_id; ?>" value="0"/>
<input type="checkbox"
class="service <?php echo $serviceItem->serv_cat_slug; ?>"
id="<?php echo $serviceItem->service_id; ?>"
name="service_<?php echo $serviceItem->service_id; ?>"
value="1"/>
</div>
</div>
<?php
}
}
}
?>
</div>
<div class="bookingButtonContainer">
<input type="button" class="rightButton bookingButton" onclick="goToStep(2);" value="Következő"/>
</div>
</div>
<div class="BookingStepContainer" id="bookingStep2">
<input type="hidden" name="worker_id" id="worker_id" value="">
<input type="hidden" name="subpage" id="subpage" value="<?php echo $subpage;?>">
<input type="hidden" name="lang" id="lang" value="<?php echo $selectedLang;?>">
<div class="bookingHeader">Válassz dolgozót</div>
<div class="workerTable" id="workerTable">
</div>
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(1);" value="Vissza"/>
<input type="button" class="rightButton bookingButton" onclick="goToStep(3);" value="Következő"/>
</div>
</div>
<div class="BookingStepContainer" id="bookingStep3">
<div class="bookingCalendar">
<div id="datepicker"></div>
</div>
<div class="bookingResultsWrapper">
<div class="availableTimesContainerTitle"></div>
<div class="availableTimesContainer" style="padding:5px;">
<input type="hidden" name="booking_date" id="booking_date" value="<?php echo date('Y-m-d');?>">
<input type="hidden" name="booking_start_time" id="booking_time" value="">
<div class="availableTimes">
</div>
</div>
</div>
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(2);" value="Vissza"/>
<input type="button" class="rightButton bookingButton" id="isTimeSelectedBtn" onclick="goToStep(4);" value="Tovább" disabled/>
</div>
</div>
<div class="BookingStepContainer" id="bookingStep4">
<div class="formRow">
<label class="bookingEmailTitle">Név:</label>
<input type="text" class="bookingName" name="guest_name" value="" required>
</div>
<div class="formRow">
<label class="bookingEmailTitle">E-mail:</label>
<input type="email" class="bookingEmail" name="guest_email" value="" required>
</div>
<div class="formRow">
<label class="bookingPhoneTitle">Telefon:</label>
<input type="text" class="bookingPhone" name="guest_phone" value="" required>
</div>
<div class="bookingButtonContainer">
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Vissza"/>
<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"/>
</div>
</div>
</form>
</div>
<script>
$(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(){
if($('#booking_time').val() != ''){
$('#isTimeSelectedBtn').prop('disabled', false);
}
});
$('#totalPanelBtn').click(function(){
if($('#totalPanel').is(':visible')){
$('#totalPanelBtn').html('<i class="fas fa-caret-left"></i>');
$('#totalPanel').hide();
$('#totalPanelBtn').css('border-top-left-radius','5px');
$('#totalPanelBtn').css('border-bottom-left-radius','5px');
}
else{
$('#totalPanelBtn').html('<i class="fas fa-caret-right"></i>');
$('#totalPanel').show();
$('#totalPanelBtn').css('border-top-left-radius','0px');
$('#totalPanelBtn').css('border-bottom-left-radius','0px');
}
});
<?php if ( ! empty($vertical['single_service_booking'])): ?>
/* Single-service verticals (massage): a guest books one treatment at a
time, so selecting one clears and disables the rest. Clicking the
selected one again releases everything - the escape hatch for a
mis-click. Bound BEFORE the totals handler below, so that handler
always sees the corrected state. */
$('.service').click(function(){
if($(this).is(':checked')){
$('.service').not(this)
.prop('checked', false)
.prop('disabled', true)
.closest('.serviceRow').addClass('serviceRowDisabled');
}
else{
$('.service').prop('disabled', false);
$('.serviceRow').removeClass('serviceRowDisabled');
}
});
<?php endif; ?>
$('.service').click(function(){
var totalPrice = 0;
var selectedServices = '<div class="selectedServiceTable">';
selectedServices += '<div class="totalServiceRow serviceHeader"><div class="selectedServiceCol totalSelectedServiceName">Service name</div><div class="selectedServiceCol totalSelectedServicePrice">Price</div><div class="selectedServiceCol serviceInfoCol"></div><div class="selectedServiceCol serviceCheckCol"></div></div>';
var classIsChecked = 0;
$('#servicelength').val('00:00');
$('.service').each(function(i, obj) {
if($(this).is(':checked')){
classIsChecked = 1;
var selected_id = $(this).attr('id');
selectedPriceElement = document.getElementById('servicePrice_'+selected_id).innerHTML;
totalPrice = totalPrice + parseInt(selectedPriceElement);
var nameNode = document.getElementById('serviceName_'+selected_id);
// Prefer the full name: the visible label may have had its
// category prefix stripped for readability.
selectedNameElement = nameNode.getAttribute('data-service-name') || nameNode.innerHTML;
selectedServices = selectedServices + '<div class="totalServiceRow"><div class="selectedServiceCol totalSelectedServiceName">'+selectedNameElement+'</div><div class="selectedServiceCol totalSelectedServicePrice">'+selectedPriceElement+' kr</div></div>';
var selectedTotalTime = $('#servicelength').val();
const selectedServiceDatetime = new Date('<?php echo date('Y-m-d');?> ' + $('#time_'+selected_id).val());
var subTotalTimeHours = moment.utc(selectedTotalTime,'HH:mm').add(selectedServiceDatetime.getHours(),'hour').format('HH:mm');
var subTotalTimeMinutes = moment.utc(subTotalTimeHours,'HH:mm').add(selectedServiceDatetime.getMinutes(),'minutes').format('HH:mm');
//console.log(subTotalTimeMinutes);
$('#servicelength').val(subTotalTimeMinutes);
}
});
selectedServices +='</div></div></div>';
$('.serviceContent').html(selectedServices);
$('.totalPrice').html('<span class="totalTitle">Total:</span> <span>' + totalPrice + '</span><span class="totalTitle"> kr</span>');
$('.totalSelectedTime').html('<span class="totalTitle">Total time:</span> <span>' + $('#servicelength').val() + '</span>');
if(classIsChecked){
$('#totalPanelBtn').show();
$('#totalPanel').show();
}
else{
$('#totalPanelBtn').hide();
$('#totalPanel').hide();
}
});
$(".workers").each(function() {
var worker = $(this).attr('id');
worker_id = worker.split("_");
setWorker(worker_id[1])
});
$('.service').click(function(){
if($('.service').is(':checked')){
var selectedClasses = $(this).attr('class');
var selectedclassesArray = selectedClasses.split(" ");
$('.service').each(function(i, obj) {
var actualElement = $(this).attr('class');
var actualElementArray = actualElement.split(" ");
if(selectedclassesArray[1] !== actualElementArray[1]){
$(this).attr("disabled", true);
}
});
getAvailableWorkersByServiceCategorySlug(selectedclassesArray[1]);
}
else{
$('.service').attr("disabled", false);
}
});
});
function recaptcha_callback(){
selectedDate = $('#booking_date').val();
selectedTime = $('#booking_time').val();
var response = 0;
$.ajax({
url: '<?php echo base_url();?>ajax',
type: 'POST',
data: {
action:'isTimeAvailable',
worker_id:$('#worker_id').val(),
servicelength:$('#servicelength').val(),
selectedDate:selectedDate,
selectedTime:selectedTime
},
error: function() {
},
//dataType: 'json',
success: function(data) {
},
}).done(function(result){
if(result == '1'){
$('#sendBooking').prop("disabled", false);
}
else{
alert('A választott időpont már foglalt, kérem válasszon másik időpontot!');
}
});
}
function getAvailableWorkersByServiceCategorySlug(categorySlug){
// Only workers who can perform EVERY ticked service may be offered.
// The checkbox id IS the service_id (see the service list markup).
var selectedServiceIds = $('.service:checked').map(function(){
return this.id;
}).get();
$.ajax({
url: '<?php echo base_url();?>ajax',
type: 'POST',
data: {
action:'getAvailableWorkersByServiceCategory',
serv_cat_slug:categorySlug,
service_ids:selectedServiceIds
},
error: function() {
},
dataType: 'json',
success: function(data) {
},
}).done(function(result){
$('#workerTable').html(result.workerListShow);
if(result.workers && result.workers.length > 0){
setWorker(result.workers[0].worker_id);
}
else{
$('#workerTable').html('<div class="noWorkerMessage">Nincs elérhető munkatárs ehhez a szolgáltatáshoz.</div>');
setWorker('');
}
});
}
$(function(){
$("#datepicker").datepicker({
firstDay: 1,
defaultDate: new Date(),
dateFormat: 'yy-mm-dd',
onSelect: function(dateText) {
$('#booking_date').val(this.value);
getAvaliableTimesOfTheDay(this.value);
//console.log("Selected date: " + dateText + "; input's current value: " + this.value);
}
});
});
function getAvaliableTimesOfTheDay(selectedDate){
$('.bookingResultsWrapper').show();
$('#serviceBookingDate').html(selectedDate);
$('#serviceBookingDate').show();
//console.log(selectedDate);
$.ajax({
url: '<?php echo base_url();?>ajax',
type: 'POST',
data: {
action:'getAvailableTimes',
worker_id:$('#worker_id').val(),
servicelength:$('#servicelength').val(),
selectedDate:selectedDate
},
error: function() {
},
//dataType: 'json',
success: function(data) {
},
}).done(function(result){
if(result != 0){
$('.availableTimes').html(result);
}
else{
$('.availableTimes').html('There is not available time on this day!');
}
});
}
function setSelectedTime(time, id){
$('#booking_time').val(time).trigger('change');
$('#serviceBookingTime').html(time);
$('#serviceBookingTime').show();
$('.availableBookingTime').each(function(i, obj) {
$(this).removeClass('selectedTime');
});
//$('#bookingStep4').show();
$('#bookingTime_'+id).addClass('selectedTime');
}
function goToStep(stepNumber){
if(canGotoNextStep(stepNumber)){
$('.stepCircle').each(function(i, obj) {
$(this).removeClass('activeStep');
});
//$('#stepCircle'+stepNumber).addClass('activeStep');
$('.BookingStepContainer').each(function(i, obj) {
$(this).hide();
});
$('#bookingStep'+stepNumber).show();
if(stepNumber == 4){
recaptcha_callback();
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
$('#stepCircle3').addClass('activeStep');
}
else if(stepNumber == 3){
if($("#datepicker").val() != ''){
getAvaliableTimesOfTheDay($("#datepicker").val());
}
$('.bookingSummaryContainer').show();
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
$('#stepCircle3').addClass('activeStep');
}
else{
if(stepNumber == 1){
$('#stepCircle1').addClass('activeStep');
}
if(stepNumber == 2){
$('#stepCircle1').addClass('activeStep');
$('#stepCircle2').addClass('activeStep');
}
$('.bookingSummaryContainer').hide();
$('.bookingResultsWrapper').hide();
}
$('html').scrollTop(0);
}
else{
alert('Please fill the required fields!');
}
}
function canGotoNextStep(stepNumber){
$RequiredFieldSelected = 0;
if(stepNumber == 2){
$('.service').each(function(i, obj) {
if($(this).is(':checked')){
$RequiredFieldSelected = 1;
}
});
}
else if(stepNumber == 1){
$RequiredFieldSelected = 1;
}
else if(stepNumber == 4){
recaptcha_callback();
$RequiredFieldSelected = 1;
}
else if(stepNumber == 3){
$('.workers').each(function(i, obj) {
if($(this).is(':checked')){
$RequiredFieldSelected = 1;
}
});
}
else if(stepNumber == 4){
recaptcha_callback();
if($('#booking_date').val() != '' && $('#booking_time').val() != ''){
$RequiredFieldSelected = 1;
}
}
return $RequiredFieldSelected;
}
function setWorker(worker_id){
// csak az aktuális worker ID-t állítjuk be a formban
$('#worker_id').val(worker_id);
// ne jelenjen meg worker név/kép a jobb oldali panelen
$('.serviceBookingWorkerContainer').hide().html('');
}
</script>