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,123 @@
|
||||
-- ============================================================================
|
||||
-- Massage vertical - data migration
|
||||
-- ============================================================================
|
||||
-- Studio Beve, 2026-08-10.
|
||||
--
|
||||
-- NOTE: there is NO DDL here. The massage vertical needed no schema change:
|
||||
-- the vertical is derived from workers.service_category_id, and
|
||||
-- services.service_type is already a VARCHAR. No ALTER, no backfill,
|
||||
-- no UPDATE of any existing row.
|
||||
--
|
||||
-- ORDER IS LOAD-BEARING. Run the whole file in ONE session (it uses @vars):
|
||||
--
|
||||
-- 1. service_categories FIRST. getAllServiceByServiceType() and
|
||||
-- getWorkersByCategorySlug() both INNER JOIN service_categories, so a
|
||||
-- dangling category_id makes rows silently INVISIBLE rather than erroring.
|
||||
-- 2. The worker row, carrying that category id. Must exist before any
|
||||
-- massage service is enabled, or the public booking page renders an
|
||||
-- empty step 2.
|
||||
-- 3. worker_schedule rows for every bookable weekday. NOT OPTIONAL, and
|
||||
-- worker_schedule_overrides alone are NOT sufficient - getAvailableTimes()
|
||||
-- returns [] without a schedule row, and booking_process() rejects with
|
||||
-- 'worker_unavailable'.
|
||||
-- 4. Services inserted with is_enabled = '0'. Flipping that to '1' is the
|
||||
-- go-live switch (step 9), after the smoke test.
|
||||
--
|
||||
-- DEMO DATA: the therapist ("Nora Lindqvist"), the four services and their
|
||||
-- prices are placeholders. Replace with real values before production.
|
||||
-- ============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ---------------------------------------------------------------- 1. category
|
||||
INSERT INTO service_categories (serv_cat_slug, serv_cat_name, is_deleted)
|
||||
VALUES ('massage', 'Masszázs', 0);
|
||||
|
||||
SET @cat := LAST_INSERT_ID();
|
||||
SELECT @cat AS new_service_category_id;
|
||||
|
||||
-- ---------------------------------------------------------------- 2. worker
|
||||
-- is_beauty / is_barber are the legacy columns; nothing reads them any more
|
||||
-- (the vertical is derived from service_category_id). They are NOT NULL, so
|
||||
-- they are written as 0 here to stay consistent with the derived value.
|
||||
INSERT INTO workers (
|
||||
worker_name, worker_profile_img, worker_info,
|
||||
service_category_id, is_barber, is_beauty, is_active, is_deleted,
|
||||
google_calendar_id, ntfy_topic,
|
||||
lunch_window_start, lunch_window_end, lunch_preferred_time
|
||||
) VALUES (
|
||||
'Nora Lindqvist', 'worker_Nora_Lindqvist.jpg', '',
|
||||
@cat, 0, 0, 1, 0,
|
||||
NULL, NULL,
|
||||
'12:00:00', '14:00:00', '12:30:00'
|
||||
);
|
||||
|
||||
SET @worker := LAST_INSERT_ID();
|
||||
SELECT @worker AS new_worker_id;
|
||||
|
||||
-- ---------------------------------------------------------------- 3. schedule
|
||||
-- weekday follows PHP date('w'): 0 = Sunday .. 6 = Saturday.
|
||||
-- Mon-Fri 10:00-18:00, Sat 10:00-16:00, Sun closed (no row).
|
||||
INSERT INTO worker_schedule (worker_id, weekday, start_time, end_time, is_alternate_week) VALUES
|
||||
(@worker, 1, '10:00:00', '18:00:00', 0),
|
||||
(@worker, 2, '10:00:00', '18:00:00', 0),
|
||||
(@worker, 3, '10:00:00', '18:00:00', 0),
|
||||
(@worker, 4, '10:00:00', '18:00:00', 0),
|
||||
(@worker, 5, '10:00:00', '18:00:00', 0),
|
||||
(@worker, 6, '10:00:00', '16:00:00', 0);
|
||||
|
||||
-- ---------------------------------------------------------------- 4. services
|
||||
-- Inserted DISABLED. service_time drives the slot length, so it also decides
|
||||
-- how many slots the therapist appears to have against the 18:00 close:
|
||||
-- a 90-minute service has its last possible start at 16:30.
|
||||
INSERT INTO services (
|
||||
service_type,
|
||||
service_category_no, service_category_en, service_category_hu,
|
||||
service_name_no, service_name_en, service_name_hu,
|
||||
service_description_no, service_description_en, service_description_hu,
|
||||
service_price, service_time, service_category_id, is_enabled, is_deleted
|
||||
) VALUES
|
||||
('massage',
|
||||
'Massasje', 'Massage', 'Masszázs',
|
||||
'Klassisk massasje 60 min', 'Classic massage 60 min', 'Klasszikus masszázs 60 perc',
|
||||
'Helkropp, avslappende', 'Full body, relaxing', 'Teljes test, lazító',
|
||||
900, '01:00:00', @cat, 0, 0),
|
||||
|
||||
('massage',
|
||||
'Massasje', 'Massage', 'Masszázs',
|
||||
'Klassisk massasje 90 min', 'Classic massage 90 min', 'Klasszikus masszázs 90 perc',
|
||||
'Helkropp, utvidet behandling', 'Full body, extended treatment', 'Teljes test, hosszabb kezelés',
|
||||
1300, '01:30:00', @cat, 0, 0),
|
||||
|
||||
('massage',
|
||||
'Massasje', 'Massage', 'Masszázs',
|
||||
'Dypvevsmassasje 60 min', 'Deep tissue massage 60 min', 'Mélyszöveti masszázs 60 perc',
|
||||
'Mot spenninger i nakke, skuldre og korsrygg', 'For tension in neck, shoulders and lower back', 'Nyaki, vállövi és deréktáji feszülésre',
|
||||
1000, '01:00:00', @cat, 0, 0),
|
||||
|
||||
('massage',
|
||||
'Massasje', 'Massage', 'Masszázs',
|
||||
'Nakke og skuldre 30 min', 'Neck and shoulders 30 min', 'Nyak és váll 30 perc',
|
||||
'Kort behandling for stillesittende arbeid', 'Short treatment for desk-bound days', 'Rövid kezelés ülőmunkához',
|
||||
550, '00:30:00', @cat, 0, 0);
|
||||
|
||||
-- ---------------------------------------------------------------- verification
|
||||
SELECT 'category' AS step, service_category_id AS id, serv_cat_slug, serv_cat_name
|
||||
FROM service_categories WHERE service_category_id = @cat;
|
||||
|
||||
SELECT 'worker' AS step, worker_id, worker_name, service_category_id, is_active
|
||||
FROM workers WHERE worker_id = @worker;
|
||||
|
||||
SELECT 'schedule' AS step, weekday, start_time, end_time
|
||||
FROM worker_schedule WHERE worker_id = @worker ORDER BY weekday;
|
||||
|
||||
SELECT 'services' AS step, service_id, service_name_no, service_price, service_time, is_enabled
|
||||
FROM services WHERE service_category_id = @cat ORDER BY service_id;
|
||||
|
||||
-- The derived-vertical check: this is what getActiveWorkers('massage') resolves.
|
||||
SELECT 'derived' AS step, w.worker_id, w.worker_name,
|
||||
(SELECT GROUP_CONCAT(DISTINCT s.service_type ORDER BY s.service_type)
|
||||
FROM services s
|
||||
WHERE s.service_category_id = w.service_category_id
|
||||
AND s.is_deleted != '1') AS derived_verticals
|
||||
FROM workers w WHERE w.worker_id = @worker;
|
||||
Reference in New Issue
Block a user