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>
The strftime() result was immediately overwritten by a hardcoded
Hungarian month-name array. Drop the dead line so PHP 8.1+ stops
emitting an E_DEPRECATED warning when prod loads the worker calendar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The security hardening commit accidentally rendered every input value as
value=\"...\" (literal backslash-quote in HTML), which mangled all
submitted fields including the hidden worker_id/service_id, causing
UPDATE to match zero rows and silently no-op.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New weekly calendar at /bookings/calendar with time blocks per booking
- Color-coded by worker, overlapping bookings shown side-by-side
- Click booking to see details, edit or delete
- Worker filter dropdown, week navigation (prev/next/today)
- AJAX week loading for smooth navigation
- Link between list view and calendar view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Monthly calendar grid per worker with colour-coded day status
- Override types: vacation, sick, custom hours, other, day-off
- Date-range override support via modal
- Floating 30-min lunch break: finds slot closest to preferred time
within configurable window, adapts to existing bookings
- Lunch break only applies for shifts >= 6 hours
- Lunch slot shown in admin calendar; blocked in booking availability
- DB migrations: absence_type/note on worker_schedule_overrides,
lunch_window_start/end/preferred_time on workers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Evelin receives all booking notifications via the shared ntfy topic in config.
Each worker also gets notified on their own personal ntfy topic (set per-worker
in the admin panel) — so workers only see their own booking events.
- _ntfy() now accepts optional $workerTopic and sends to both topics if different
- All three call sites (new/modify/cancel) pass $worker->ntfy_topic
- Admin worker form + worker_process() wired for ntfy_topic field
- DB: ALTER TABLE workers ADD COLUMN ntfy_topic VARCHAR(100) NULL DEFAULT NULL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New GoogleCalendar library: createEvent/updateEvent/deleteEvent via service account, all wrapped in try/catch so failures never break booking flow
- booking_process: creates worker + owner calendar events on new booking
- manage_booking_process: deletes old events, creates new ones on modify
- manage_booking_cancel: deletes events before cancellation
- Service_model: updateBookingCalEvents() stores gcal event IDs
- Admin worker form: Google Calendar ID field added
- PHPMailer: enabled exceptions (was silently swallowing SMTP errors)
- Config: application/config/google_calendar.php for service account path + Evelin calendar ID
DB migration required:
ALTER TABLE workers ADD COLUMN google_calendar_id VARCHAR(255) NULL DEFAULT NULL;
ALTER TABLE bookings ADD COLUMN gcal_event_id_worker VARCHAR(255) NULL DEFAULT NULL;
ALTER TABLE bookings ADD COLUMN gcal_event_id_owner VARCHAR(255) NULL DEFAULT NULL;
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>