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>
Two reported bugs, one root cause: resource tabs were built as a rival selection
to the doctor rather than a narrower view within them.
Selecting a resource cleared the doctor. The auto-select effect then quietly put
the *first* doctor back, so anyone working under the second doctor was thrown to
the first and lost that doctor's own booking. Selecting a resource now leaves the
doctor alone; only picking a doctor clears the resource.
A resource tab also still rendered the doctor's slot timeline, just with no data.
Resources have no slotted weekly schedule — their calendar comes from service
duration and real occupancy — so showing a slot grid promises times the booking
engine does not recognise. The resource tab now renders its own panel: that day's
appointments on the resource plus a service booking entry point.
Both regressions are pinned by tests, and both were checked by reverting each fix
in turn. The first attempt at the doctor-retention test passed even with the bug
restored, because the auto-select effect masked it; it was rewritten to use the
second doctor, where the bounce is observable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
my/clinic-doctors now reports has_schedule per doctor, and the appointments page
builds tabs from it. A doctor with no working days had a tab that could only ever
show an empty timeline.
The flag is resolved with one query for the whole list rather than one per
doctor. Clinic owners now read this authenticated endpoint too instead of the
public clinic doctor-list, which is where the flag lives; admin keeps the public
list and, with no flag present, hides nobody.
Also repairs fallout from making the resource supervisor mandatory: four test
classes build resources through their own helpers and were failing with 422. The
supervisorFor helper moved to ApiTestCase so all domains share one, rather than
copying it per suite. Full backend suite is green again (1306 tests) — the
previous commit only ran tests/Resource and missed this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resource tabs now sit under the selected doctor and list only the resources that
doctor supervises, so moving between a doctor's own appointments and the devices
under them is one row of tabs rather than a flat list of everything.
The booking modal reads the doctor from the resource's supervisor instead of
asking again. The doctor↔resource relation is defined once, on the resource, and
repeating the question here would have made a second source of truth. A resource
whose supervisor was removed is blocked with a message pointing at the fix rather
than a silently disabled button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Supervision now lives on the resource itself instead of being asked for again at
booking time, so one relation answers it everywhere.
The column is deliberately separate from the existing doctor_id bridge. That
bridge means "this resource IS this doctor" and isPerson() uses it to pin
capacity at 1; a supervised three-seat device must not become a person resource.
The FK is SET NULL rather than CASCADE because deleting a doctor should not take
the clinic's laser with it.
Required on create and non-clearable on update, enforced in the API where it can
give a Persian message. Ownership is checked through Clinic::hasDoctor so a
secretary cannot put their device under a doctor of another clinic; that returns
404, not 403, keeping foreign data invisible.
The 13 existing resources are backfilled deterministically: a practice resource
gets its own doctor, a clinic resource gets that clinic's first doctor. Both are
editable from the resource form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompt was wrong three times and the file now says so up front: the
doctor-less appointment path was built and fully reverted, the getDoctor()
blast radius was overstated, and the booking engine it asked to build already
existed. Also records two tooling traps found on the way — phpstan runs at
level 5 here so it never checks nullability, and migrations:diff missed the
nullable change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Add appointment" while a resource tab is active now opens a booking modal for
that resource: its own services, then a time, then the responsible doctor.
It reuses the booking engine that already existed (appointment-availability →
appointment-hold → appointment-confirm) rather than adding a second path. That
engine answers service-first and returns a resource assignment per slot, so the
modal keeps only the slots where the engine actually offered this resource and
pins that role to it on hold. Showing the other slots would let an operator pick
a time that can only come back as a 409.
The responsible doctor is required because every appointment has a doctor and
confirm will not run without one; the resource records which device the work
happens on.
The read-only "منابع" timeline under the schedule is removed along with its
component and hook, which had no other consumers. GET /api/v1/resources/timeline
is untouched on the backend and now has no client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resources now get their own tabs on the appointments page, alongside doctors.
An appointment on "Laser CO2" belongs to the device, not to whichever doctor
happens to stand behind it, so selecting a resource tab replaces the doctor
filter instead of stacking on top of it.
GET /api/v1/my/appointments gains an optional resource_uuid filter and returns
a `resource` object per row. The join is a leftJoin on purpose: appointments
created before the resource-first model have no resource and must not drop out
of the list.
The resource tab lives in the URL so Back and refresh restore the same view,
per the list-state rule in CLAUDE.md. The doctor tab is still useState; moving
it is a separate refactor and was left untouched.
Verified against the running app: filtering by a resource returns only its
appointments, a resource from another tenant returns an empty list (TenantFilter,
200 not 403), and legacy rows still list with resource: null.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>