2 Commits

Author SHA1 Message Date
1eee1f9415 fix(timetable): align Zod decimal fields with backend wire format (decimal-as-string per Laravel cast) (B5)
Phase A diagnosed the "Kon timetable niet laden" browser symptom as Zod
schema drift. The prompt's hypothesis (enum {value, label} mismatch) was
incorrect — the schema already uses the enumLabel() wrapper for every
enum field. The actual drift is decimal-cast columns: Laravel serialises
`decimal(N,M)` columns as strings to preserve precision, but the schema
expected numbers, so the very first response triggered a ZodError.

Affected fields, all on `artist_engagements`:
  fee_amount         decimal(10,2)  → wire `"11503.58"`, schema was z.number()
  buma_percentage    decimal(5,2)   → wire `"7.00"`,     schema was z.number()
  vat_percentage     decimal(5,2)   → wire `"21.00"`,    schema was z.number()
  deposit_percentage decimal(5,2)   → wire `"…"`,        schema was z.number()

Backend has no explicit `decimal:N` cast on these columns
(api/app/Models/ArtistEngagement.php:64-85 — the `casts()` method covers
the enums + booleans + dates + integers, but skips decimals).

Per the strategic decision (frontend adapts, backend stays):
  - schemas/timetable.ts: four fields → z.string().nullable()
  - types/timetable.ts: matching ArtistEngagement interface fields →
    `string | null`
  - PerformancePopover.vue:129: only consumer doing arithmetic on a
    decimal field; coerce at the use site via Number(...).toFixed(2).
    Single line.
  - tests/component/PerformanceBlock.test.ts + tests/a11y/axe.test.ts:
    spot-checked mocks; the two with hand-built engagement payloads
    flipped fee_amount/buma_percentage/vat_percentage from numbers to
    strings to match the new schema. No other mocks needed updating.

The {value, label} enum wrapper claim in the prompt was specifically
debunked in Phase A — every consumer (Wachtrij, PerformanceBlock,
WachtrijCard, PerformancePopover, AddPerformanceDialog, page entry)
already uses .value/.label access against an enumLabel-wrapped schema.

B6 will lock the wire-format contract with a real-API fixture
regression test.

All 397 tests still pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:13 +02:00
210c443cc9 test(timetable): PerformanceBlock visual states + interactions (Step 6)
17 component-level tests via mountWithVuexy:

Visual states (10):
  - status palette × 3 (option, confirmed, cancelled) — asserts both the
    CSS class AND that the matching --tt-status-{X}-bg custom property
    resolves on :root (proves the token sheet really loaded)
  - capacity icon present when crew + guests > stage.capacity
  - capacity icon absent when sum ≤ capacity
  - capacity icon absent when stage.capacity is null (no warning possible)
  - B2B left dot present when b2bLeft prop true
  - B2B right dot present when b2bRight prop true
  - no dots when neither prop true
  - conflict ring class when warnings includes 'overlap'
  - cascade-pulse class when pulse=true
  - aria-label includes artist + stage + status + HH:mm time window
  - tabindex="0" for keyboard focus

Interactions (5, in second describe):
  - click → emits select with performance + DOMRect
  - pointerdown → emits pointerdown with (event, performance)
  - Delete keypress → emits delete
  - Enter keypress → emits select

Test count: 333 → 350.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:38:46 +02:00