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:
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Vertical registry helper.
|
||||
*
|
||||
* Thin accessors over application/config/verticals.php so controllers and
|
||||
* views never poke at the raw config array. Every function is safe to call
|
||||
* with untrusted input — vertical_get() returns NULL for anything that is not
|
||||
* a known, enabled vertical, which is what makes it usable as a whitelist for
|
||||
* $_POST['subpage'] and for route segments.
|
||||
*/
|
||||
|
||||
if ( ! function_exists('vertical_all'))
|
||||
{
|
||||
/**
|
||||
* Every registered vertical, including disabled ones, keyed by slug.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function vertical_all()
|
||||
{
|
||||
$CI =& get_instance();
|
||||
$CI->config->load('verticals', FALSE, TRUE);
|
||||
$verticals = $CI->config->item('verticals');
|
||||
|
||||
return is_array($verticals) ? $verticals : array();
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_get'))
|
||||
{
|
||||
/**
|
||||
* One vertical by slug, or NULL when the slug is unknown or disabled.
|
||||
*
|
||||
* Use this as the whitelist for any user-supplied vertical value.
|
||||
*
|
||||
* @param string $slug
|
||||
* @return array|null
|
||||
*/
|
||||
function vertical_get($slug)
|
||||
{
|
||||
if ( ! is_string($slug) || $slug === '')
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$verticals = vertical_all();
|
||||
|
||||
if ( ! isset($verticals[$slug]))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (empty($verticals[$slug]['enabled']))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $verticals[$slug];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_exists'))
|
||||
{
|
||||
/**
|
||||
* @param string $slug
|
||||
* @return bool
|
||||
*/
|
||||
function vertical_exists($slug)
|
||||
{
|
||||
return vertical_get($slug) !== NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_slugs'))
|
||||
{
|
||||
/**
|
||||
* Slugs of every enabled vertical. Suitable for in_array() checks and for
|
||||
* building the admin service_type dropdown.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function vertical_slugs()
|
||||
{
|
||||
$slugs = array();
|
||||
|
||||
foreach (vertical_all() as $slug => $vertical)
|
||||
{
|
||||
if ( ! empty($vertical['enabled']))
|
||||
{
|
||||
$slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
return $slugs;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_public'))
|
||||
{
|
||||
/**
|
||||
* Enabled verticals flagged public, ordered by 'sort'. This is what the
|
||||
* home page tiles iterate over — flipping 'public' is the launch switch.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function vertical_public()
|
||||
{
|
||||
$public = array();
|
||||
|
||||
foreach (vertical_all() as $slug => $vertical)
|
||||
{
|
||||
if ( ! empty($vertical['enabled']) && ! empty($vertical['public']))
|
||||
{
|
||||
$public[$slug] = $vertical;
|
||||
}
|
||||
}
|
||||
|
||||
uasort($public, 'vertical_sort_compare');
|
||||
|
||||
return $public;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_sort_compare'))
|
||||
{
|
||||
/**
|
||||
* uasort() callback. Named rather than a closure so this helper stays
|
||||
* compatible with the PHP version the live host runs.
|
||||
*
|
||||
* @param array $a
|
||||
* @param array $b
|
||||
* @return int
|
||||
*/
|
||||
function vertical_sort_compare($a, $b)
|
||||
{
|
||||
$sa = isset($a['sort']) ? (int) $a['sort'] : 0;
|
||||
$sb = isset($b['sort']) ? (int) $b['sort'] : 0;
|
||||
|
||||
if ($sa === $sb)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($sa < $sb) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_label'))
|
||||
{
|
||||
/**
|
||||
* Display label for a vertical in the given language, falling back to
|
||||
* English and then to the uppercased slug.
|
||||
*
|
||||
* @param array $vertical
|
||||
* @param string $lang
|
||||
* @return string
|
||||
*/
|
||||
function vertical_label($vertical, $lang)
|
||||
{
|
||||
if (isset($vertical['label'][$lang]))
|
||||
{
|
||||
return $vertical['label'][$lang];
|
||||
}
|
||||
|
||||
if (isset($vertical['label']['en']))
|
||||
{
|
||||
return $vertical['label']['en'];
|
||||
}
|
||||
|
||||
return isset($vertical['slug']) ? strtoupper($vertical['slug']) : '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_lang'))
|
||||
{
|
||||
/**
|
||||
* Whitelist a language segment. The site serves no/en/hu; anything else
|
||||
* falls back to English, matching the existing per-view $langArray checks.
|
||||
*
|
||||
* @param string $lang
|
||||
* @return string
|
||||
*/
|
||||
function vertical_lang($lang)
|
||||
{
|
||||
$allowed = array('no', 'en', 'hu');
|
||||
|
||||
return (is_string($lang) && in_array($lang, $allowed, TRUE)) ? $lang : 'en';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('vertical_service_label'))
|
||||
{
|
||||
/**
|
||||
* Display label for a service in a list that is ALREADY grouped under its
|
||||
* category heading.
|
||||
*
|
||||
* services.service_name has to stay self-describing - the confirmation email
|
||||
* and the admin lists render it with no category around it. But in the
|
||||
* booking form, the manage-booking form and the landing-page price list the
|
||||
* category heading sits directly above the row, so repeating it makes every
|
||||
* row wrap and the list read as cramped:
|
||||
*
|
||||
* [Studio Beve Signature - individuelt tilpasset massasje]
|
||||
* Studio Beve Signature - individuelt tilpasset massasje - 50 min
|
||||
*
|
||||
* Opt in per vertical via 'strip_category_prefix' so barber and beauty keep
|
||||
* rendering exactly as before.
|
||||
*
|
||||
* @param array $vertical registry row (may be NULL or missing the key)
|
||||
* @param object $serviceItem needs ->service_name and ->service_category
|
||||
* @return string
|
||||
*/
|
||||
function vertical_service_label($vertical, $serviceItem)
|
||||
{
|
||||
$name = isset($serviceItem->service_name) ? $serviceItem->service_name : '';
|
||||
|
||||
if (empty($vertical['strip_category_prefix']))
|
||||
{
|
||||
return $name;
|
||||
}
|
||||
|
||||
$category = isset($serviceItem->service_category) ? $serviceItem->service_category : '';
|
||||
|
||||
if ($category === '' OR strpos($name, $category) !== 0)
|
||||
{
|
||||
return $name;
|
||||
}
|
||||
|
||||
// The /u modifier matters: the separator is an en dash, and a byte-wise
|
||||
// trim() would only mangle it.
|
||||
$stripped = substr($name, strlen($category));
|
||||
$stripped = preg_replace('/^[\s\-–—]+/u', '', $stripped);
|
||||
$stripped = trim($stripped);
|
||||
|
||||
return ($stripped !== '') ? $stripped : $name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user