Commit Graph
11 Commits
Author SHA1 Message Date
UbuntuandClaude Opus 5 13e1b1a672 Add Amarildo to the barber page and gate workers by service
New barber Amarildo Champimpi is introduced on the barber page in all three
languages, and workers can now be restricted to a subset of the services in
their category.

Amarildo cannot perform beard colouring, any waxing, or the all-in package, and
he is a barber only - but category 1 "Kozmetika" holds 19 barber AND 27 beauty
services, so the category-derived vertical wrongly made him beauty-capable.
Because the worker picker is only ever fetched AFTER services are ticked, one
per-service capability filter solves both problems: excluding him from every
beauty service removes him from that vertical entirely.

worker_services(worker_id, service_id) is an allow-list where an EMPTY set means
UNRESTRICTED. That default is deliberate: a missing migration degrades to the
previous behaviour instead of hiding every worker from the booking flow, and
existing workers keep working untouched. Ticking every box in the admin grid
stores nothing at all, so an unrestricted worker also picks up services added
later; unticking even one makes the worker restricted, and new services must
then be granted explicitly.

Enforcement is in three places. The picker offers only workers who can perform
EVERY selected service, and both booking paths re-check server-side, since the
picker is only a UI affordance - a crafted POST now gets service_not_offered/403
rather than a booking the worker cannot honour.

Fixed alongside, all found while building the above:

- getWorkersByCategorySlug() never filtered is_active, so marking a worker
  inactive had NO effect on the public booking flow. Both of its callers are
  guest-facing. The sibling fallback getActiveWorkers() had always filtered it.
- Worker profile picture uploads failed SILENTLY above PHP's upload_max_filesize.
  Both upload blocks gated on tmp_name alone, which cannot distinguish a rejected
  upload from "no file chosen" - PHP empties tmp_name in both cases - so the
  worker was saved with an empty worker_profile_img and no error shown. The
  upload error code is now read and reported, and a separate guard catches
  post_max_size overflow, where $_POST and $_FILES both arrive empty and the form
  silently did nothing at all.
- createWorker() omitted is_active from its INSERT, so the column default (1)
  always won and a worker created as inactive silently came back active.

Migration - the table MUST be created before this code is deployed, because the
picker query subselects it whenever service ids are passed:

    CREATE TABLE worker_services (
      worker_id  INT NOT NULL,
      service_id INT NOT NULL,
      PRIMARY KEY (worker_id, service_id),
      KEY idx_worker_services_worker (worker_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Already applied on test, dev and prod. Server-side, upload_max_filesize/
post_max_size were raised to 8M/12M on all three environments (php.ini on test,
.user.ini on the shared-host dev and prod docroots) - not carried by this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYGSbK1erKv7VG1pvdPEjG
2026-08-24 12:07:07 +00:00
UbuntuandClaude Opus 5 1a364f21b3 Replace Vincent's gallery photos and align the strip with the bio
New treatment-room photos for the four-image strip in the massage
About-us block, cropped square at 800x800 to match the existing set
(the block is flex, and a non-square crop leaves the strip visibly
short against the bio) and stripped of EXIF.

Each is the full 4284px frame width, with the vertical offset picked
to hold both Vincent's face and the hands-on-client action. In pics 1
and 4 the subject is taller than the frame is wide, so the crop
sacrifices the top of his hair rather than the treatment itself.

Also corrects the strip's vertical alignment. The two columns were
already align-items: flex-start and did start level; the offset came
from the text, which is 13pt/1.75em and therefore carries 6.5px of
half-leading above its first line, while an inline image starts flush
at its box top. padding-top: 0.375em is exactly that half-leading. It
is zeroed at the 480px breakpoint, where the container turns
display:block and the strip no longer sits beside the text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wwCBWRnEZuorhv34FVUkY
2026-08-16 11:37:06 +00:00
UbuntuandClaude Opus 5 06088aee8b Replace Vincent's profile photo with the new studio shot
Both profile images are regenerated from the same new source photo,
cropped square and stripped of EXIF (the original iPhone file carried
GPS coordinates).

The two crops differ on purpose, because the display sizes do:
- massage/vincent/vincent.jpg (400px, shown as a 150px circle) gets a
  wide head-and-shoulders crop showing the studio behind him
- worker_Vincent_Pusch.jpg (300px, shown as a 75px circle in the
  booking worker picker) stays tight, so the face is still
  recognisable at that size

Filenames are unchanged, so workers.worker_profile_img needs no
migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wwCBWRnEZuorhv34FVUkY
2026-08-16 10:27:59 +00:00
UbuntuandClaude Opus 5 de38aae8d5 Apply client design feedback on the parking copy, icons and barber layout
Four items from the salon, all three languages where applicable.

Massage section icon
- The icon above Om oss (and above Behandlinger, Priser and Åpningstider -
  it is the same image in all four) was beauty's makeup icon, copied in as
  a placeholder before massage had any assets of its own. Replaced with the
  herbal-compress icon tinted to the massage sage, mirroring how beauty's
  is tinted to its rose. beauty/ollo_rose.png is untouched; the copy under
  assets/img/massage/ is removed, and the one dead template rule that still
  pointed at it (.box-heading:before, which renders on no page) repointed.

Parking copy, barber + beauty + massage, no/en/hu
- Dropped the "park free of charge during the treatments" clause from the
  owner bio and replaced it with the new two-space wording.
- Replaced the note under Åpningstider. The Norwegian original was
  misspelled differently in each vertical ("kundeprarking" on barber,
  "kunderparking" on beauty); both are gone.
- massage picks both up automatically: its owner block is extracted from
  beauty-form-<lang>.php at generation time, and the hours note comes from
  the generator.

Barber treatments grid: level the six icons
- .service-text reserves padding-bottom for the 80px floated icon but has
  no height, so every block sizes to its own text. Measured on the live
  page the left column ran 155/155px against the right column's 185/206px,
  because its titles wrap to two lines - so the icons drifted further apart
  with each row (0, 30, 81px).
- Trimmed the reserved padding and gave every block the same floor, so all
  six are identical and the rows line up. 195px clears the tallest block
  and leaves room for a three-line title at the narrow end of the desktop
  range. Desktop only: below 981px the columns stack full width, where a
  floor would only add dead space.
- beauty drifts 20px and massage 0px, so both are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:00:25 +00:00
UbuntuandClaude Opus 5 5c30467fbd 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>
2026-08-13 16:34:47 +00:00
UbuntuandClaude Fable 5 06966a898a Add job postings section with vacancies listing and footer links
- New /ledigestilling/ index listing all open positions (split
  barber/beauty card styling)
- Three ads: negletekniker + massør (beauty design), barber (gold
  barber design with recolored CSS + assets)
- Rename ledigestilling.html -> negletekniker.html with 301 redirect
- "Ledige stillinger" footer link on barber/beauty pages (NO/EN/HU)
- Back-to-list link on each ad page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:32:17 +00:00
UbuntuandClaude Opus 4.6 a100acddec Add Kateryna gallery images (2/2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:03:05 +00:00
UbuntuandClaude Opus 4.6 e95b32d017 Add Kateryna worker profile and gallery images (1/2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:03:00 +00:00
UbuntuandClaude Opus 4.6 420bcb37fd Security hardening: fix SQLi, XSS, file upload, and migrate DB to RDS
- Fix all SQL injection vulnerabilities across Service_model, User_model,
  Module_model, Log_model, and Admin controller using parameterized queries
- Add htmlspecialchars() to all user-controlled output in admin views
  (bookings, services, workers, service categories, login form)
- Fix XSS in AJAX worker response and manage-booking-cancelled view
- Add file extension whitelist (jpg, jpeg, png, gif, webp) to all uploads
- Remove webshell (pentest2.php) from assets/img/profiles/
- Stop logging plaintext passwords on failed login attempts
- Migrate database.php hostname from localhost to AWS RDS endpoint
- Fix dropdown styling (white-on-white) in worker calendar view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:00:32 +00:00
Astral04andClaude Sonnet 4.6 2183c8aff2 guest booking self-management feature + layout fixes
- add manage_token column to bookings (DB migration done)
- generate unique token per booking, include manage link in confirmation emails (no/en/hu)
- new routes: manage-booking, manage-booking-process, manage-booking-cancel
- new views: manage-booking.php, manage-booking-cancelled.php
- 24h cutoff enforcement for cancel/modify; emails sent to guest + studio CC
- fix manage-booking step 3 float layout (overflow:auto BFC clearfix)
- fix booking page time slot overflow: bookingResultsWrapper 378px -> 360px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 16:01:12 +01:00
Astral04 68c8245cef add all files 2025-10-04 11:38:07 +02:00