Add massage as a third vertical, driven by a config registry

Introduces /massage alongside barber and beauty: landing page, booking
flow, admin support, home tile and SEO entries, in all three languages.

Architecture
- application/config/verticals.php + vertical_helper.php: one registry
  entry per vertical (branding, assets, views, behaviour flags). A fourth
  vertical is a config entry plus content files.
- Strangler: barber and beauty keep pointing at their existing view files,
  so their rendered HTML is unchanged. Only massage uses the new generic
  pages/vertical-*.php and includes/vertical-*.php views, which collapse
  the four duplicated per-language nav/footer branches into one.
- Pages::vertical() + one route; booking(), booking_finished(),
  _booking_error() and manage_booking() are now registry-driven.

Worker/vertical coupling
- getActiveWorkers() derives the vertical from services.service_category_id
  instead of the workers.is_barber / is_beauty flags, which were a
  hand-maintained cache of exactly that fact. Verified against production
  data: the derived set reproduced the stored flags for every worker, in
  both verticals. No schema change was needed for massage.
- The legacy flags are now written through from the category so a rollback
  cannot strand a new worker, and the admin worker UI shows the derived
  verticals read-only instead of two dropdowns that controlled nothing.

Bug fixes found along the way (all pre-existing)
- booking_process() had no server-side category guard; cross-vertical
  mixing was prevented only by client-side JS.
- add-service-form / add-worker-form emitted `selected` on every category
  option, so the newest category silently became the default.
- update-service-form offered only barber/beauty, so editing a service of
  any other type silently rewrote it.
- getWorkerScheduleByDay ignored schedule overrides while getAvailableTimes
  honoured them, so slots could be shown and then rejected. Added an
  override-aware getWorkerScheduleForDate() and used it in both guards.
- Booking lists dereferenced a null service if one had been hard-deleted.
- main.css: .tiles was tuned for exactly two tiles, including an
  absolutely-positioned .style1 at the 1280px breakpoint.

Massage-specific behaviour, opt-in per vertical
- strip_category_prefix: grouped service lists show "50 min" under the
  treatment heading rather than repeating the full name. The full name is
  carried in data-service-name so the totals panel stays unambiguous, and
  services.service_name is untouched for emails and admin.
- single_service_booking: one treatment per booking, enforced in the UI and
  in booking_process(). Re-clicking the selection releases it.
- Displayed treatment time (50/80/110 min) is in the service name; the
  booked slot (60/90/120 min) is service_time and covers changing and
  payment. service_time is never shown to the guest.

DB migrations for dev/prod are in documents/ - additive only, no ALTER.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ubuntu
2026-08-13 16:34:47 +00:00
co-authored by Claude Opus 5
parent 0fd7f7a7a5
commit 5c30467fbd
61 changed files with 7130 additions and 169 deletions
@@ -0,0 +1,291 @@
<?php
/* -----------------------------------------------------------------------
* Massage vertical landing content - Hungarian.
*
* Bio, treatment names and descriptions supplied by the salon.
* Generated from one template so massage-form-no / -en / -hu stay
* structurally identical.
*
* DURATIONS: the price list shows the treatment time from the service NAME
* (50 / 80 / 110 min). services.service_time holds the longer BOOKED slot
* (60 / 90 / 120 min) which covers changing and payment, and is never
* rendered to the guest. See prices_note below.
*
* Still a placeholder: assets/img/massage/masseur.png (therapist portrait),
* assets/img/massage/banner.jpg and map.jpg.
* -------------------------------------------------------------------- */
?>
<!-- Banner -->
<section id="banner">
<img src="<?php echo base_url();?>assets/img/massage/banner.jpg" style="width: 100%;" />
</section>
<section id="mobil-menu">
<div class="mobil-menucont">
<ul>
<li class="mobil-menu-book"><a href="<?php echo SITEURL;?>hu/booking/massage">IDŐPONTFOGLALÁS</a></li>
<li class=""><a href="<?php echo $vertical['facebook'];?>" class="icon brands fa-facebook-f"><span class="label">Facebook</span></a></li>
<li class=""><a href="<?php echo $vertical['instagram'];?>" class="icon brands fa-instagram"><span class="label">Instagram</span></a></li>
</ul>
</div>
</section>
<!-- About Us -->
<section id="aboutus" class="wrapper style1">
<div class="container">
<div class="inner">
<header class="major">
<img src="<?php echo base_url();?>assets/img/massage/ollo_rose.png">
<h2>Rólunk</h2>
</header>
<?php /* Owner block, lifted verbatim from beauty-form-<lang>.php at
generation time so all three verticals introduce Evelin
identically. Do not edit here - edit the beauty file and
regenerate, or the two will drift. */ ?>
<div class="bemutatkozas">
<div class="bem-bal">
<p>
Bence Evelin vagyok a Studio Beve vezetője és megálmodója. Fontosnak
tartom, hogy a vendégek a nálunk töltött idő alatt – egy kicsit megpihenve a világ zajától – feltöltődjenek, és kívül-belül egszépülve térjenek vissza a mindennapokhoz.
</p>
<p>
A minőségi idő, a maximális figyelem, a legprofesszionálisabb eszközök, a
hivatástudattal és szakértelemmel karöltve jelentik a filozófiánk és
szolgáltatásaink alapjait.
</p>
<p>
Hiszem, hogy a szépség, a divat és a trend nem nemhez kötött, így nálunk
lehetőség van arra, hogy mindenki egy helyen szépülhessen nemtől függetlenül.
A Barber, Beauty és Masszázs részlegeink között pedig megtalálhatjátok a számotokra
megfelelő szolgáltatásokat.
</p>
<p>
Közel 10 éves szépségipari, és közte több éves nemzetközi tapasztalattal
elérkezettnek láttam az időt, hogy megnyissam saját stúdiómat itt
Trondheimben, az Innheredsveien 92 szám alatt, ahol a vendégeknek lehetősége
van ingyenes parkolásra is a kezelések időtartama alatt.
</p>
<p>
Extra öröm számomra, hogy egy olyan tradicionális helyet sikerült találnom,
ahol az 50-es években már szépségszalon működött, ennek köszönhetően a
hagyományt, modern környezetben lehetőségem van tovább vinni.
Szépülj velünk, szeretettel várunk.
</p>
</div>
<div class="bem-jobb">
<img src="<?php echo base_url();?>assets/img/evelin_beauty.JPG">
<h4>Bence Evelin <br> <span>Tulajdonos, Studio Beve</span></h4>
</div>
</div>
<?php /* Therapist sub-section, following the per-worker pattern
barber/beauty use: circular portrait + name, then the bio
with a photo strip alongside. */ ?>
<div class="dolgozo-nev">
<hr>
<img src="<?php echo base_url();?>assets/img/massage/vincent/vincent.jpg" class="dolgozo-kep">
<h4>VINCENT Pusch <span>- Masszőr</span></h4>
</div>
<div class="bemutatkozas-dolgozo">
<div class="bem-dolgozo-bal">
<p>A masszázs, a manuális kezelések és az emberi test működése iránti érdeklődésem 2011 óta kísér, és az évek során szakmai pályám meghatározó részévé vált.</p>
<p>Fizioterápiás végzettségemet Németországban szereztem. Pályafutásomat katonai kórházban kezdtem, ahol fizioterapeutaként és manuálterapeutaként dolgoztam. Később multidiszciplináris rendelőben folytattam a munkámat, majd saját magánpraxist vezettem Németországban, mielőtt Norvégiába költöztem.</p>
<p>Az évek során folyamatosan bővítettem szakmai ismereteimet és tapasztalataimat. A fizioterápia és manuálterápia mellett sportfizioterápiával, valamint 2019 óta craniosacralis terápiával és oszteopátiával is foglalkozom. Fontos számomra a folyamatos szakmai fejlődés és az új módszerek megismerése.</p>
<p>Hiszek abban, hogy minden ember és minden test más, ezért a kezeléseim során mindig az egyéni igényeket és célokat tartom szem előtt. A különböző masszázs- és manuális technikákat ennek megfelelően választom ki és kombinálom, legyen szó relaxációról, a mindennapi vagy fizikai terhelés következtében kialakult izomfeszültségről, sport utáni regenerációról vagy egyszerűen a testi feltöltődésről.</p>
<p>2026 májusa óta élek Trondheimben. Külföldön megszerzett fizioterápiás képesítésem norvég elismertetése jelenleg folyamatban van. A Studio Bevében jelenleg masszőrként végzem szakmai tevékenységemet, ahol masszázs- és olyan manuális kezeléseket kínálok, amelyekhez nem szükséges norvég egészségügyi engedély. A norvég engedély megszerzésével szakmai tevékenységem és az általam kínált kezelések köre a jövőben tovább bővülhet.</p>
<p>Örömmel várom, hogy a Studio Beve vendégeit is megismerhessem, és szakmai tapasztalatommal személyre szabott, figyelmes kezelést nyújthassak számukra.</p>
</div>
<div class="bem-dolgozo-jobb">
<img src="<?php echo base_url();?>assets/img/massage/vincent/1.jpg">
<img src="<?php echo base_url();?>assets/img/massage/vincent/2.jpg">
<img src="<?php echo base_url();?>assets/img/massage/vincent/3.jpg">
<img src="<?php echo base_url();?>assets/img/massage/vincent/4.jpg">
</div>
</div>
</div>
</div>
</section>
<!-- Treatments -->
<section id="treatments" class="wrapper style2">
<div class="container">
<div class="inner">
<header class="major">
<img src="<?php echo base_url();?>assets/img/massage/ollo_rose.png">
<h2 class="nyitva">Szolgáltatások</h2>
</header>
</div>
<div class="service-block">
<div class="service-area">
<div class="one-third service-part1">
<div class="service-inner">
<div class="service service1">
<div class="service-text service-text1">
<div class="service-icon icon1"></div>
<div class="service-content">
<div class="main-title">Klasszikus svédmasszázs</div>
<div class="sub-title-serv">teljes test, lazítás és feltöltődés</div>
</div>
</div>
<div class="service-text service-text2">
<div class="service-icon icon2"></div>
<div class="service-content">
<div class="main-title">Sportmasszázs</div>
<div class="sub-title-serv">aktív életmódhoz és sportolóknak</div>
</div>
</div>
<div class="service-text service-text3">
<div class="service-icon icon3"></div>
<div class="service-content">
<div class="main-title">Mélyszöveti masszázs</div>
<div class="sub-title-serv">mélyebb izmok és kötőszövet</div>
</div>
</div>
</div>
</div>
</div>
<div class="one-third service-part2">
<div class="service-inner">
<div class="service service2">
<a href="<?php echo SITEURL;?>hu/booking/massage" class="banner-anchor"><img src="<?php echo base_url();?>assets/img/service-banner-massage.png" alt="massage" class="banner-image1"></a>
</div>
</div>
</div>
<div class="one-third service-part3">
<div class="service-inner">
<div class="service service3">
<div class="service-text service-text1">
<div class="service-icon icon1"></div>
<div class="service-content">
<div class="main-title">Irodai és fizikai munka utáni masszázs</div>
<div class="sub-title-serv">ülőmunkához és fizikai terheléshez</div>
</div>
</div>
<div class="service-text service-text2">
<div class="service-icon icon2"></div>
<div class="service-content">
<div class="main-title">Studio Beve Signature</div>
<div class="sub-title-serv">személyre szabott kezelés</div>
</div>
</div>
<div class="service-text service-text3">
<div class="service-icon icon3"></div>
<div class="service-content">
<div class="main-title">Craniosacralis kezelés</div>
<div class="sub-title-serv">gyengéd, holisztikus manuális kezelés</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Prices -->
<section id="one" class="wrapper style1">
<div class="container">
<div class="inner">
<header class="major">
<img src="<?php echo SITEURL;?>assets/img/massage/ollo_rose.png">
<h2>Árlista</h2>
<h4 class="prices-note">A feltüntetett kezelési idő a tényleges masszázsidőt jelenti. A lefoglalt időpont ennél valamivel hosszabb, hogy legyen idő az át&shy;öltözésre és a fizetésre.</h4>
</header>
<?php
/**
* Grouped by treatment type. Two deliberate differences from the
* barber/beauty price lists, both because massage sells the SAME
* treatment at two or three lengths:
*
* 1. The description is printed ONCE per group rather than once
* per duration, so a three-length treatment does not repeat
* its blurb three times.
* 2. The row label is the duration alone. services.service_name
* has to stay self-describing ("Klassisk svensk massasje -
* 50 min") because the booking form and the confirmation
* email render it with no category around it - but here the
* category heading is directly above, so repeating it just
* makes every row wrap.
*
* $services arrives ordered by service_id and the catalogue is
* inserted grouped, so a simple change-detect is enough.
*/
$prevousCategory = '';
if(is_array($services)){
foreach($services as $serviceItem){
if($serviceItem->service_category != $prevousCategory){
if($prevousCategory !== ''){
echo '<div style="clear:both;"></div>';
}
?>
<div class="prices-category" style="width: 100%">
<h4><?php echo $serviceItem->service_category;?></h4>
<p class="category-intro"><?php echo $serviceItem->service_description;?></p>
</div>
<?php
}
// Same rule as the booking form: the category heading is
// directly above, so the row need not repeat it.
$rowLabel = vertical_service_label($vertical, $serviceItem);
?>
<div class="one-half">
<div class="category-content">
<div class="category-text-left">
<div class="main-title"><a><?php echo $rowLabel;?></a></div>
</div>
<div class="category-text-right">
<div class="price">kr <?php echo $serviceItem->service_price;?></div>
</div>
</div>
</div>
<?php
$prevousCategory = $serviceItem->service_category;
}
echo '<div style="clear:both;"></div>';
}
?>
</div>
</div>
</section>
<!-- Opening hours -->
<section id="two" class="wrapper style2">
<div class="container">
<div class="inner">
<header class="major">
<img src="<?php echo SITEURL;?>assets/img/massage/ollo_rose.png">
<h2 class="nyitva">Nyitvatartás</h2>
<h4>Ingyenes vendégparkolás a kezelés ideje alatt</h4>
</header>
</div>
<div class="nyitvatartas">
<div class="about-content">
<div class="about-text-left">
<div class="title">Hétköznap</div>
</div>
<div class="about-text-right">
<div class="time">10:00 - 18:00</div>
</div>
</div>
<div class="about-content">
<div class="about-text-left">
<div class="title">Szombat</div>
</div>
<div class="about-text-right">
<div class="time">10:00 - 16:00</div>
</div>
</div>
<div class="about-content">
<div class="about-text-left">
<div class="title">Vasárnap</div>
</div>
<div class="about-text-right">
<div class="closed">Zárva</div>
</div>
</div>
</div>
</div>
</section>