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>
124 lines
5.9 KiB
SQL
124 lines
5.9 KiB
SQL
-- ============================================================================
|
|
-- 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;
|