The page an operator opens to see the day's work did not say who any of it was
for. TreatmentSession::toArray() carries no patient, so the list showed a
service name, a time, and a repeated 40-char button — the same three lines for
every row, with the finished work leading.
The endpoint now sends patient_name and resource_name. They are added in the
controller next to case_uuid/service_name rather than in toArray(), so patient
identity does not leak into every other consumer of that method.
The list is now a queue: unfinished work first, settled work (done, cancelled,
no-show) below it, each group counted. Every row leads with its time, names the
patient, and carries the service, device, session number and area progress on
one meta line. The whole row is the link, so the repeated button is gone.
Also fixes three things the redesign checklist calls out: Latin digits in the
session and area counts (formatNumber), a date repeated on every row of a page
whose title is "today", and a missing error state — a failed request rendered
as "no sessions today", which reads as an empty day rather than a broken one.
Adds the test file the page never had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend already accepted staff_uuid on POST /api/v1/my/appointment; only
the booking modal never sent it, so the pre-assignment half of the operator
model had no way in.
The field sits in step 1 next to the section select and is optional by design:
left empty, the session stays in the shared queue that every allowed operator
sees. Its placeholder says so rather than leaving the blank state unexplained.
The section select is no longer the only combobox in the modal, so the tests
target it by its label instead of by role.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
findTodayForStaff filtered on appointments.staff, and no booking path ever sets
that column. The result was a staff panel that was empty for every operator, in
every tenant, no matter how much work the day held.
Today's sessions now reach an operator three ways: the session they already
claimed (performedBy), the appointment a secretary pre-assigned to them, or
unclaimed work whose protocol names them. A protocol with no staff list means
everyone may perform it — the same "no rows is not a restriction" rule
ResourceServiceOffering already uses.
The query is also tenant-scoped, which the old one was not: it relied on
appointments.staff being a same-tenant row rather than saying so.
Verified against the dev database: the operator behind 09128726723 now gets
both of today's sessions on "لیزر توتال", which the old query returned none of.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
confirmWithPayments — the path behind POST /appointment/{uuid}/confirm, which
is how a secretary actually confirms — created the patient session but never
called TreatmentCaseStarter. Only onConfirmed did. So an appointment on a
service with an active protocol was confirmed and paid, and no treatment case
or sessions were ever created; the staff panel had nothing to list.
Every existing test in OpenCaseOnConfirmTest drove onConfirmed, which is why
the gap survived. Added one that drives confirmWithPayments; it fails without
the fix.
Also adds app:treatment:backfill-cases, mirroring
app:appointment:backfill-sessions: it reports confirmed appointments on a
protocol service that have no case, and with --fix replays the starter and
prints the exception the logger would otherwise keep to itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The booking modal presented one flat scroll of fields whose order did not
match the order of the decisions behind them, and gave no reason when the
submit button stayed grey.
- Group the form into numbered steps (service+time, patient) so the order of
decisions is visible. The optional visit-price collapse stays unnumbered —
numbering an optional step reads as required.
- Show the first blocking condition above the footer instead of leaving a
disabled button unexplained.
- Label the header chip's facts ("device:", "supervising doctor:") and add the
appointment's Jalali date, which the modal never displayed at all.
- Replace the hand-rolled primary/ghost button pair with the design system's
`.seg` + `.on`, and announce state via aria-pressed.
- Move autoFocus off the patient search in picker mode; the first decision is
the section select above it.
- Give every input an id and its label an htmlFor.
- Surface a distinct error state for the slot query. A failed request used to
fall through to "not enough free time", which sent users to another day for
no reason.
- Raise the service remove button (18px), the duration pill and the time chips
to at least the 32px hit target; mark service rows role=checkbox.
- Modal close button gets an accessible name; `.field` controls stretch to the
full 40px box so the whole frame is clickable.
Runtime probe on the open modal goes from 4 unnamed icon controls, 1 unlabelled
field and 2 sub-32px controls to clean, across light/dark/compact/mobile.
The redesign-page probe now names the offending elements instead of only
counting them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The day-offset field was a bare input inside `.field`, and `.field` is itself
the 40px bordered input box — so the number rendered as plain text with no
border and nothing said it could be edited. It now uses the shared Input in
numeric mode, which also normalises Persian digits on the way in.
Turning the switch off deleted the protocol, its steps and its staff list on a
single click with no confirmation. It now asks first, and says what will be lost
and what will not: open treatment cases are untouched.
A failed read left `protocol` null, which the tab rendered as a switch in the off
position — telling the manager this service is single-session when in truth the
request had simply failed. That state now shows an error with a retry.
Both icon-only buttons were hand-padded down to 18px; the runtime probe flagged
them in all four views. They use `.mini-btn`, which the design system already
sizes to 32px and to 44px on touch. The probe now reports one short control on
this page, and the same one on tabs this change never touched.
The save button sat at the bottom of a long form, so saving on a phone meant
scrolling past everything. `.save-bar` was already in the stylesheet for exactly
this; it now also states what is about to be saved, and why the button is
disabled when no staff are selected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Starting a session created area records with no device, and the panel only ever
read the device it never set — so every "اتمام این ناحیه" came back 422 with
"دستگاه این ناحیه مشخص نیست". The backend tests passed because they sent
resource_uuid explicitly; from the UI the flow was unusable end to end.
The device now inherits from the appointment's resource, which the secretary
already chose at booking; asking the operator again is taking one decision
twice. The session screen offers a picker per area on top of that, because one
session really does run bikini on an alexandrite and underarms on a diode.
Treating without a device is allowed: botox is an injection, and requiring a
device would make clinics invent a fake resource per injection. Sending readings
with no device is still rejected — there would be no schema to validate against.
A protocol whose service has no ResourceServiceOffering rows now says so in the
tab where the manager is standing. It does not block booking: "no offering means
any resource" is a deliberate, tested rule. But silence meant the gap surfaced
only when the operator was already in front of a patient.
Also adds the live timer the spec asked for, and wires slot-suggestions into the
unbooked queue — the endpoint existed and tested green but no screen called it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the panel side. A clinic picks its practice domain in settings, where
the copy says plainly that this is not the specialty label the public site shows;
picking nothing stays valid and changes nothing.
Cases and the unbooked queue share one page rather than two, because they answer
the same question — which patient is where in their course and what is still
owed. The queue explains why booking is not automatic instead of leaving the
reader to wonder.
Platform admins get domain CRUD with a column showing whether a domain has a
dedicated workflow or falls back to the default, so the gap is visible rather
than guessed at; the code field is locked after creation because workflows bind
to it.
Also puts the staff session screens in the sidebar — they were reachable only by
typing the URL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three screens, each reusing what already exists rather than inventing a parallel
look. The treatment plan lives as a tab on the service page next to categories,
because the course belongs to the service; the switch is the protocol's existence
rather than a separate boolean that could disagree with the step list. Each step
asks for days since the previous session, which is how the interval actually
works and what the form should therefore say.
The staff screens are the flow from the reference screenshots: today's sessions,
then a session where each area is started, recorded and closed on its own. Field
inputs are built from the schema the server sends per resource type, so a clinic
adding an RF device sees its own form here without a code change.
StatusBadge gains treatment session and area states rather than a second badge
component sitting beside it, and the resource type modal grows a field editor so
the operator form is configured where the device is defined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The operator opens the session, treats each body area on its own device and
records what that device was set to. Readings are validated against the resource
type's field schema, so a laser form and an RF form each enforce their own rules
without this code naming either.
Finishing is allowed with areas still open — the operator is standing in front of
a patient and must not be trapped by the software — but the count comes back so
the panel can warn. Session state mirrors onto the appointment (salon, then
completed) while its slot times are never rewritten: those are the reservation's
promise and the input to occupancy, whereas how long it actually took belongs to
the session. Overwriting them would destroy the comparison between the two.
Who performed it is recorded on the session rather than inferred from the
appointment's planned staff: when a colleague covers a sick operator, the medical
record must say who actually held the device.
Endpoints live under /api/v1/dashboard/staff because StaffRouteGuardSubscriber
closes everything else to staff-only users. Opening a second door through its
allowlist would put the access boundary in two places.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Booking the next session stays a decision, not an automation: the system offers
free slots and the secretary picks one with the patient in front of them. Booking
automatically would fill the worst slot in the calendar — the one nobody wanted —
and produce a no-show.
A session whose due date has passed with nobody booking it surfaces in an
explicit queue instead of waiting silently for the patient to call. Suggestions
default to the resource the previous session ran on, since continuing a course on
the same device is both clinically steadier and one less choice to make; with no
previous booking the caller must name a resource rather than get an empty list.
Slot maths is reused from ResourceBookingSlotService; this only decides which
resource, from which day, and how far ahead to look.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Booking a device is not booking its doctor: the operator runs it and the doctor
only supervises. But bookAtomically locked the doctor row and isSlotTaken checked
overlap against the doctor alone, ignoring which resource was chosen, so a clinic
whose devices share one supervisor could not run two of them at once. Every
tenant in the database is in that position — clinic 2's six resources all point
at doctor 6.
Resource bookings now skip the doctor lock and carry no active_slot_key; their
guarantee comes from resource_occupancy, which understands capacity and seats.
Both direct paths write occupancy rows the way the hold engine already did, so
ResourceBookingSlotService stops being the only thing holding two sources of
truth together, and cancelling releases the seat.
Occupancy is bucketed in five-minute slices, which is coarser than a booking
time: a booking ending 12:35:04 spilled four seconds into the 12:35 bucket and
collided with the next one starting at that same second, despite zero real
overlap. This surfaced on real rows 76 and 77 during backfill. Resource bookings
now snap both ends of their window down to the bucket grid — schedule-driven
slots are already aligned, so only manually entered times move.
The seat is claimed after persist because it needs the appointment id; losing
the race removes the appointment rather than leaving a booking with no device
behind it.
app:appointment:backfill-resource-occupancy gives existing resource-backed
appointments their missing occupancy and clears the doctor keys that no longer
mean anything. It reports conflicts between two old bookings instead of picking
a loser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A laser device is not a doctor, so booking one from the public site sent
resource_uuid and no doctor_uuid and got back "doctor_uuid یا resource_uuid
الزامی است" — a message telling the caller to send something it had already
sent. The panel path had resolved this from ClinicResource.supervisor since it
was written; only the public path had not, and the field was defined but never
read there.
A resource with no supervisor now gets its own message pointing at the actual
fix, instead of the generic one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything that differs between specialties as data is already stored as data.
What is left is behaviour — when a case opens, what happens once a session ends —
so it becomes a TreatmentWorkflow resolved through a tagged-service registry.
The booking path calls one collaborator and never names a specialty; adding
dentistry is a new class, not an edit to confirmation.
A clinic that has chosen no practice domain still gets working multi-session
courses: DefaultTreatmentWorkflow answers for null and for any code without a
dedicated implementation, keeping "unset means behave as today, not error".
LaserTreatmentWorkflow is deliberately empty beyond claiming `beauty` — it is the
seam where laser-specific behaviour will land without disturbing anyone else.
Session due dates are anchored to the previous session's actual finish, so a
patient who comes twenty days late shifts the rest of their course instead of
getting the next session while it can still do nothing. Only the next session is
recomputed; later ones keep their estimate because they are anchored to nothing
yet.
Attachment targets the first session without an appointment rather than the
first open one: a patient booking again mid-course was otherwise matched to the
session that already had a booking, and the second appointment went nowhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What an operator writes down after treating an area is decided by the device,
not by the service: a laser has energy, pulse and shot count, an RF unit has
something else. So the field list lives on the resource type, and adding a new
kind of device becomes a settings change rather than a migration.
One validator covers both directions — the schema when a manager saves it and
the values when an operator submits them. Splitting them would let a schema be
stored that no value can ever satisfy.
A value whose key is not in the schema is rejected rather than stored: silently
keeping it means the operator believes they recorded something that will never
be shown back to them. Option matching compares as strings so "18" and 18 are
one option, not two.
The migration seeds the laser type's three fields onto existing rows that have
none, so clinics already running laser devices do not start from an empty form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening a case copies what must not move afterwards — the session count and the
list of body areas, each with its category name — because a treatment record is
a medical document and editing settings tomorrow must not rewrite what was done
yesterday. The areas are the leaf categories under the service's own category:
"توتال" contains bikini, leg and hand, and treatment happens on those three, not
on the grouping node above them. A category with no children is its own single
area, so "لیزر دست" gets one area rather than none.
Every session in the course is created up front so that "session 5 of 8" has
somewhere to live, but none of them is booked: creating eight real appointments
would lock eight months of slots for a patient who may not attend session three.
CategoryClosureResolver gains leaves(); the graph walk it already does is what
tells a leaf from a grouping node, so this belongs next to descendants() rather
than in a second traversal elsewhere.
TreatmentCase and TreatmentSession carry no money field, and must not: billing
lives on PatientSession, which is created when an appointment is confirmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A protocol says a course of a service runs over several sessions, when each
falls due, which doctor supervises it and which staff may perform it. The row
existing IS the "طول درمان" switch, so there is no separate boolean that could
disagree with the step list.
Each step's offset is measured from the previous session rather than from the
start of the course: laser spacing is a clinical requirement — hair regrows
relative to the last treatment — so a late patient shifts the rest of their
course instead of getting the next session early. That also lets one course use
uneven gaps, which a single min/ideal/max triple cannot express: a botox course
is session 1, then +15 days, then monthly.
Steps and staff are cleared and rewritten in two flushes inside a transaction.
A single flush sends inserts before deletes and the replacement row collides
with the unique (protocol, step_number) index — caught by the replace test.
Removes docs/api/course.md and the task-12 folder. They documented src/Course/,
a module deleted in 65d5831c whose commit message only mentions removing two
test files; that design is superseded by this one.
ServiceItem::$sessionCount is marked deprecated. It never had logic behind it
and session count now comes from the protocol; the column stays in payloads so
existing clients keep working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A practice domain is the field a clinic operates in — beauty, dentistry —
and unlike Specialty it is configuration, not a label: treatment workflows
will bind to its code, so the code is immutable once created and only a
platform admin can mint one. A clinic that has not chosen a domain keeps
behaving exactly as it does today.
Assignment reuses PATCH /api/v1/clinic/{uuid} rather than adding a second
endpoint. An unknown domain uuid is rejected instead of silently dropped,
because a lost selection would only surface at the first protocol-driven
booking.
Also corrects ADR-0003: resource occupancy does not in fact guard the panel
booking path, which writes appointments.resource_id and no occupancy row at
all, so the doctor slot key cannot simply be dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Updated DoctorDetailPage, MySecretariesPage, RecordNumberSettingsPage, RepresentationsPage, ResourcePoolsPage, ResourceTypesPage, SecretariesPage, SecretaryDetailPage, SettingsPage, SkillsPage, SmsWalletPage, and TagsSettingsPage to use the new Switch component instead of native checkboxes.
- Enhanced accessibility by ensuring the Switch component uses appropriate roles and labels.
- Added tests for the new Switch component to ensure functionality and accessibility compliance.
- Updated styles to accommodate the new Switch component design.
- Add RecordNumberSettingsController for managing patient record number patterns.
- Create RecordNumberPattern entity to represent the pattern configuration.
- Implement RecordNumberPatternRepository for database interactions.
- Develop RecordNumberGenerator service for generating and validating record numbers.
- Add tests for record number generation, backfilling, and API interactions.
- Ensure proper access control for viewing and updating patterns based on user roles.
Resources never needed a branch: devices and rooms belong to the clinic
itself, and the picker always had exactly one option — a mandatory click
that decided nothing.
- `address_uuid` is now optional on resource and pool creation; when it is
missing the environment's own address is used. Clients still sending it
keep working.
- The panel no longer asks for or displays a branch anywhere: resource
form, list column and filter, pool form and column, detail row, and the
resource-first booking page.
- Availability no longer gates on `doctor_addresses.active`. That gate shut
down every device of a clinic whose address row happened to be inactive,
with a message no page in the panel could act on — no endpoint writes
that column at all.
`address_id` stays on the resource: the timezone and the tenant pair are
derived from it. It is simply no longer the user's decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit introduces a new JSON file that contains the abstract syntax tree (AST) representation of the ResourceBookingSlotController.php. The AST includes nodes for classes, methods, and their relationships, as well as edges that describe imports and references between various components within the file. This addition will aid in understanding the structure and dependencies of the ResourceBookingSlotController.
- Add service timeline builder for appointments to manage available slots.
- Create a hook to fetch resource booking services with effective durations.
- Develop ResourceBookingSlotController to handle API requests for resource booking slots.
- Implement ResourceBookingSlotService to calculate available time slots based on resource occupancy and service durations.
- Add tests for resource appointment creation and booking slot functionality to ensure correct behavior and edge cases.
The resource booking modal groups services under their section, the way the
doctor's service booking does. The offering list had no section, so the list
could only be flat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Returning from a resource to the doctor's own booking meant clicking the doctor
tab in the row above, which nobody found. The resource strip now leads with a
"نوبتهای خود پزشک" tab that clears the resource, so going in and coming back
happen in the same row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>