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
This commit is contained in:
Ubuntu
2026-08-24 12:07:07 +00:00
co-authored by Claude Opus 5
parent 4c823f92ca
commit 13e1b1a672
16 changed files with 638 additions and 13 deletions
@@ -100,6 +100,21 @@
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
</div>
</div>
<div class="dolgozo-nev">
<hr>
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
</div>
<div class="bemutatkozas-dolgozo no-gallery">
<div class="bem-dolgozo-bal">
<p>Amarildo here, originally from Albania. I have more than six years of experience as a barber, and throughout my career I have worked in both Greece and England. I have been living in Norway for the past three years, and I am now happy to join the Studio Beve team.</p>
<p>I specialise in both modern and classic haircuts, as well as skin fades and taper fades. Precision and attention to detail are very important to me, and I always strive to achieve the best possible result for every client.</p>
<p>Since I started working as a barber, my profession has become a true passion of mine. I continuously develop my skills, learn new techniques, and keep myself up to date with the latest trends and styles.</p>
<p>For me, barbering is not simply about giving someone a haircut. It is important to create a style that truly suits each individual and to make sure every client leaves my chair feeling confident and satisfied.</p>
<p>I am excited to bring the experience I have gained over the years, my dedication to the profession, and my creativity to Studio Beve, and I look forward to welcoming you to my chair soon.</p>
</div>
</div>
</div>
</div>
</section>
@@ -76,6 +76,21 @@
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
</div>
</div>
<div class="dolgozo-nev">
<hr>
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
</div>
<div class="bemutatkozas-dolgozo no-gallery">
<div class="bem-dolgozo-bal">
<p>Amarildo vagyok, és Albániából érkeztem. Több mint hatéves tapasztalattal rendelkezem barberként, pályafutásom során Görögországban és Angliában is dolgoztam. Három éve élek Norvégiában, és most örömmel csatlakozom a Studio Beve csapatához.</p>
<p>Modern és klasszikus hajvágásokra, valamint skin fade és taper fade technikákra specializálódtam. A munkámban fontos számomra a precizitás és a részletekre való odafigyelés, és mindig arra törekszem, hogy minden vendégem számára a lehető legjobb eredményt érjem el.</p>
<p>Amióta barberként dolgozom, a szakmám valódi szenvedélyemmé vált. Folyamatosan fejlesztem a tudásomat, új technikákat tanulok, és igyekszem naprakész maradni a legújabb trendekkel és stílusokkal.</p>
<p>Számomra a barbering nem egyszerűen egy hajvágás elkészítéséről szól. Fontos, hogy olyan stílust alakítsak ki, amely valóban illik az adott vendéghez, és hogy mindenki magabiztosan és elégedetten távozzon a székemből.</p>
<p>Örömmel hozom a Studio Bevébe az elmúlt években megszerzett tapasztalataimat, a szakmám iránti elkötelezettségemet és a kreativitásomat, és várom, hogy hamarosan téged is üdvözölhesselek a székemben.</p>
</div>
</div>
</div>
</div>
</section>
@@ -77,6 +77,21 @@
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
</div>
</div>
<div class="dolgozo-nev">
<hr>
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
</div>
<div class="bemutatkozas-dolgozo no-gallery">
<div class="bem-dolgozo-bal">
<p>Jeg heter Amarildo og kommer opprinnelig fra Albania. Jeg har mer enn seks års erfaring som barber, og gjennom karrieren min har jeg jobbet både i Hellas og England. Jeg har bodd i Norge de siste tre årene, og nå er jeg glad for å bli en del av teamet hos Studio Beve.</p>
<p>Jeg spesialiserer meg på både moderne og klassiske hårklipper, samt skin fade og taper fade. Presisjon og sans for detaljer er svært viktig for meg, og jeg streber alltid etter å oppnå best mulig resultat for hver enkelt kunde.</p>
<p>Siden jeg begynte å jobbe som barber, har yrket blitt en ekte lidenskap for meg. Jeg utvikler meg kontinuerlig, lærer nye teknikker og holder meg oppdatert på de nyeste trendene og stilene.</p>
<p>For meg handler barberfaget ikke bare om å klippe håret. Det er viktig å skape en stil som virkelig passer den enkelte, og sørge for at hver kunde forlater stolen med en følelse av selvtillit og tilfredshet.</p>
<p>Jeg gleder meg til å ta med meg erfaringen jeg har opparbeidet gjennom årene, engasjementet mitt for faget og kreativiteten min til Studio Beve, og jeg ser frem til å ønske deg velkommen i stolen min.</p>
</div>
</div>
</div>
</div>
</section>
@@ -7,6 +7,7 @@
'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.',
'category_mismatch' => 'The selected staff member does not perform the chosen services. Please start again.',
'service_not_offered' => 'The selected staff member does not perform all of the chosen treatments. Please pick another staff member or change the treatments.',
'single_service_only' => 'Only one treatment can be booked at a time. Please select just one.',
'invalid_email' => 'That e-mail address does not look valid. Please check it - your confirmation and the link to manage your booking are sent there.',
'no_service' => 'No service was selected. Please choose at least one service.',
@@ -7,6 +7,7 @@
'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.',
'category_mismatch' => 'A kiválasztott kolléga nem végzi a kiválasztott szolgáltatásokat. Kérjük, válasszon újra.',
'service_not_offered' => 'A kiválasztott kolléga nem végzi el az összes kiválasztott szolgáltatást. Kérjük, válasszon másik kollégát, vagy módosítsa a szolgáltatásokat.',
'single_service_only' => 'Egyszerre csak egy kezelés foglalható. Kérjük, csak egyet válasszon.',
'invalid_email' => 'Az e-mail cím nem tűnik érvényesnek. Kérjük, ellenőrizze - a visszaigazolást és a foglalás kezelésére szolgáló linket erre a címre küldjük.',
'no_service' => 'Nem választott ki szolgáltatást. Kérjük, válasszon legalább egyet.',
@@ -7,6 +7,7 @@
'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.',
'category_mismatch' => 'Den valgte medarbeideren utfører ikke de valgte tjenestene. Vennligst velg på nytt.',
'service_not_offered' => 'Den valgte medarbeideren utfører ikke alle de valgte behandlingene. Vennligst velg en annen medarbeider eller endre behandlingene.',
'single_service_only' => 'Du kan kun bestille én behandling om gangen. Vennligst velg kun én.',
'invalid_email' => 'E-postadressen ser ikke ut til å være gyldig. Vennligst kontroller den - bekreftelsen og lenken for å endre bestillingen sendes dit.',
'no_service' => 'Du har ikke valgt noen tjeneste. Vennligst velg minst én tjeneste.',
@@ -364,12 +364,19 @@
}
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
serv_cat_slug:categorySlug,
service_ids:selectedServiceIds
},
error: function() {
},
@@ -365,12 +365,19 @@
}
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
serv_cat_slug:categorySlug,
service_ids:selectedServiceIds
},
error: function() {
},
@@ -363,12 +363,19 @@ if (is_array($services) && count($services) > 0) {
}
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
serv_cat_slug:categorySlug,
service_ids:selectedServiceIds
},
error: function() {
},