Screenshotting the pages under dark mode and compact density (rather than
trusting that design tokens were enough) turned up two mistakes repeated across
every page this feature set added:
- `.card` carries only the surface, border and radius — padding comes from the
separate `.card-pad`. Fifteen cards were rendering with their content flush
against the edges.
- `.field` *is* the input box, a 40px-tall flex row. Wrapping a label plus a
control in it produced a joined addon rather than a label above its field.
`.field-block` is the label-above layout, and thirty-seven wrappers now use it.
Both were invisible to type-checking and to the tests, which is exactly why the
visual pass was worth running. Numbers in the new UI now go through
formatNumber so they render as Persian digits, and the utilization page's
header no longer repeats the sentence that appears under its filters verbatim.
The QA driver gained a `--ui` flag: theme and density live in
localStorage['clinicpro-ui'], so without seeding them dark mode and compact
density cannot be screenshotted at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows closed by the resource-strategy and blocking work: task 06's picker
interface and four strategies, task 07's ad-hoc blocking and 409 recovery, and
task 12's same-as-previous preference, which had been blocked on task 06's
missing strategies since it was written.
Tasks 13 and 14 both carried the flaky-suite caveat against their final review;
that flake has a diagnosed cause and a fix, so both now say so instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine from tasks 06 and 07 could find slots and hold them, but nothing in
the panel could actually book one.
- Search, hold, confirm stay three separate steps because they are three
separate states: between seeing a slot and taking it the seat is still open,
and between taking and confirming there is a deadline
- HoldCountdown reads the server's expires_at rather than starting its own
timer at render: browser clock skew and network latency both cost seconds,
and those seconds are exactly where a hold is lost. It turns urgent under a
minute and tells the parent the moment it lapses
- Per-role resource swap offers only the resources the engine returned for that
same slot. Listing every resource in the branch would let an operator pick
one that was never free and collect a 409
- An empty result is not an error: the reason code renders as a sentence
saying what to change
- Confirm requires a doctor and stays disabled until one is chosen — the
endpoint rejects it anyway, and finding that out after the hold clock has
been running is the wrong time
Reached from the appointments page as a separate action rather than folded into
the existing form: its search comes from the intersection of resource
calendars, not from one doctor's slots, and merging the two would confuse both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 11 and the third closing rule of the design document: preventing a double
booking is the database's job, not the code's. Any "is it free?" check in PHP has a
race window between the read and the write — two concurrent requests both see free
and both write.
MariaDB has no range EXCLUDE constraint, so every occupied interval is broken into
fixed five-minute buckets under UNIQUE(resource_id, bucket_at, seat). The code only
INSERTs; a rejection from the database *is* the answer. `seat` carries capacity: a
three-bed room has seats 0..2, allocation walks upward on each collision, and the
fourth concurrent hold finds nowhere to sit. Counting capacity in PHP would have
rebuilt the very race this removes.
Buckets are written through DBAL rather than the ORM on purpose: a unique violation
raised inside flush() closes the EntityManager, and the next seat attempt would then
fail with "EntityManager is closed", hiding the real outcome.
Occupancy is one row per (segment × resource). The reference test asserts the payoff
directly: for a 55-minute appointment of numbing / waiting / laser, the room gets
three rows and the operator only two — the operator holds nothing during the wait and
stays bookable for someone else.
A partial hold never survives. If the second resource has no room, the first is
released and the hold itself removed; otherwise a resource stays locked for an
appointment that will never exist.
Confirming does not re-reserve anything — the seats were taken at hold time and only
the label changes. Re-reserving on confirm would reopen the race the hold closed.
Cancelling marks rows `released` instead of deleting them, because the history of
which resource was busy when is the input to the utilisation reports; the uniqueness
buckets *are* deleted, or that interval would stay locked forever.
Expired holds are released by the existing scheduler rather than a new one. That
exposed a bug in my own change: the flush guard used $count, which now includes
released holds, so reset([]) could pass false to save(). It is guarded on $expired.
The appointment itself is still built with the existing constructor, so
active_slot_key, events and the payment path behave exactly as before — the
multi-resource occupancy sits beside them, not instead of them.
12 tests. Two matter most: the second hold on the same resource and interval getting
409, and a test that writes a duplicate bucket row over a *separate connection* and
expects the unique-key violation — if that one ever passes silently, the guarantee
had moved back into the code.
1208 tests / 3495 assertions. phpstan at its 14-error baseline. Frozen slot contract
green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Created checklist for task 11: Package and Credit Ledger
- Created checklist for task 12: Treatment Course
- Created checklist for task 13: Cancellation Policy, No-Show, and Waitlist
- Created checklist for task 14: Domain Events and Utilization Reports
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.