Commit Graph
100 Commits
Author SHA1 Message Date
hamed a4a24c51af refactor(guidelines): update guidelines and add mandatory Grill section for all skills 2026-08-07 20:03:49 +03:30
hamed 9b9a7500e2 chore(graph): rebuild after secretary modal split 2026-08-07 19:55:55 +03:30
hamedandClaude Opus 5 7c3407b0b3 refactor(admin): split the secretary form from its permissions into two modals
Adding a secretary meant deciding all 17 permission resources in the same
dialog. The full-height capture showed the form running past 1300px with the
save button below 16 accordions, and the dynamic registry makes that worse: every
page added in future lengthens this one modal.

The add/edit modal now carries only doctors, profile and address, and fits on
screen with its footer visible. Permissions move to SecretaryPermissionsModal,
reachable from a row action and opened automatically right after a successful
add, since a new secretary starts on the role defaults and the owner usually
wants to set them.

Neither create nor update sends permissions any more — the backend seeds the role
defaults on create, and the permissions modal owns the writes, fanning out over
every link row so a secretary shared across doctors stays consistent.

PermissionAccordions moves to components/ui as a shared component. Sections now
start collapsed with a granted/total badge on each header, so the panel opens at
a fixed height and still says which sections are on.

Two design-system slips caught by re-screenshotting rather than by the audit:
- a text button as a third row action pushed the name column out of the table, so
  the desktop row uses an icon with a title and the mobile card keeps the label
- .btn.secondary is not defined in styles.css (variants are primary/ghost/soft/
  danger/accent), so it renders as a bare .btn. Used ghost here. 25 other files
  have the same dead class; left alone as a separate sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:55:32 +03:30
hamed a6eee1ce9d chore(graph): rebuild after clinic-doctor read gate symmetry 2026-08-07 19:31:14 +03:30
hamedandClaude Opus 5 d6de746938 fix(permissions): apply the patient and tag read gates to invited clinic doctors
PatientController::resolveScope and TenantTagController::guardTagView only ever
checked the secretary, while every write in both controllers already ran through
both checkers. So an invited clinic doctor with patients.view off got 200 with an
empty list where a secretary got 403 — one permission, two behaviours. No data
was exposed either way; tenant scoping emptied the result.

The fix is not canOrNonMember. That collapses two different situations: a
membership row switched to active=false means the collaboration ended, and
ClinicDoctorPermission::can() returns false for everything in that case too.
Routing it through the permission gate turned the existing 404 on a single record
into a 403, which confirms the record exists to someone who just lost access.
ClinicRecordAccessTest caught it.

isActiveMemberDenied() answers the narrower question — active member, permission
off — and leaves a deactivated row to the data scope, which closes it with a 404
and discloses nothing. A test now pins that distinction so it cannot be collapsed
again.

Tags keep the tags.view OR patients.view rule, now for both roles.

Verified live in three states: active with both off 403/403, deactivated not 403,
active with patients.view on 200/200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:30:52 +03:30
hamed 6a6852379d chore(graph): rebuild after addresses permission fix 2026-08-07 19:16:26 +03:30
hamedandClaude Opus 5 294ca19a46 fix(permissions): make the addresses resource real instead of decorative
A full role-by-role sweep (9 roles x 18 endpoints against the running app) showed
the addresses toggles in the owner's permission form controlled nothing. Grep
confirms it: no gate anywhere referenced 'addresses'. The panel's address list was
gated on appointment_settings.view instead — the same borrowed-permission pattern
already fixed for resources and treatment.

GET /api/v1/addresses now gates on addresses.view.

The resource drops to view-only. Creating, updating and deleting an address in
ClinicController is explicitly owner-or-admin
($clinic->getUser()->getId() !== $user->getId()), so those three actions could
never be delegated to a secretary or an invited doctor no matter what the form
said. Both role defaults narrow to ['view' => true] to match, and stored JSON
keeps its old keys harmlessly since merge only reads registry keys.

This widens secretary access: addresses.view defaults to true while
appointment_settings.view defaults to false, so secretaries who could not list
addresses now can. That is deliberate and costs no confidentiality — the same
addresses are already served anonymously from
GET /api/v1/clinic/{uuid}/addresses, which is whitelisted in security.yaml.

Verified live in three states: default 200, addresses.view off 403, and
addresses off with appointment_settings on still 403, proving the borrow is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:16:02 +03:30
hamed be5cdf8af6 chore(graph): rebuild after service catalog permission gate 2026-08-07 18:52:47 +03:30
hamedandClaude Opus 5 073a68bcc2 fix(admin): split the catalog page buttons onto per-action services permissions
The backend gate is now per-action, so a single canUpdate driving add, edit and
delete would show buttons the server answers 403 to. Each button now checks its
own action, and the includes modal takes canCreate/canDelete so its add select
and per-edge remove button follow the same split.

ServiceCategoryTab is deliberately left alone: its save patches
/api/v1/service-item/{uuid}, which is ClinicServiceController and already gated on
services.update, so canEdit was already the right permission. Only its read of the
category tree moved behind services.view, and the page it lives on already
requires that.

The page test now drives a configurable can(), covering view-only, create-only,
update-only and delete-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:52:25 +03:30
hamedandClaude Opus 5 52c45443c5 fix(security): gate every ServiceCatalogController route on the services permission
The controller carried only IS_AUTHENTICATED_FULLY on the class and none of its
15 routes checked a permission. A secretary whose owner had turned `services`
fully off could still create, rename and delete service categories, build item
groups, replace group members, and rewrite service relations and per-branch
overrides.

Scope is intra-tenant privilege escalation, not IDOR: owned() and requireItem()
already resolve every uuid against the caller's active environment, so no data
crossed tenants.

Gating is per-action (view/create/update/delete) and reuses denyServices() from
ClinicServiceController in the same domain, so a secretary with `update` cannot
create or delete. The call is the first statement in every action, before
requireCategory/requireItem — placed after, an unknown uuid would answer 404 and
leak whether the record exists.

An earlier note claimed these endpoints were consumed by the booking flow and so
could not be closed. That was wrong. service-selection/validate, the group routes
and the relation routes have no consumer in any of the three API clients, and the
sibling controller already puts every service read behind services.view — the
booking modal reads service-items through it — so any flow needing services
already needed the permission.

The docs claimed appointment_settings.* for the includes routes, which was never
enforced either; corrected to services.*.

The test loops the whole route list rather than sampling, and a guard asserts the
count of #[Route( equals the count of denyServices( so a future ungated route
fails here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:46:16 +03:30
hamed be93e15bad chore(graph): rebuild after subscription doc correction 2026-08-07 18:39:05 +03:30
hamedandClaude Opus 5 1d10f8c907 docs(api): correct the subscription/my enforcement claim
The previous note said GET /api/v1/subscription/my has no gate because it
returns 200 with every permission off. That was wrong. It gates deliberately
with a degraded payload instead of a 403: without subscription.view the
response drops the active subscription and used_trial, and effective_plan
keeps only features, max_secretaries and max_resources — no plan identity,
no billing. Verified against the running app both ways.

The 403 it does not throw is the point: FeatureGate and useSubscription need
capability flags on every page, so a 403 would break the whole panel.

Also adds .claude/prompt/service-catalog-permission-gate.md for the one real
gap, with the per-route analysis that was previously deferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:38:36 +03:30
hamed ccd869e442 chore(graph): rebuild after permission docs 2026-08-07 18:21:11 +03:30
hamedandClaude Opus 5 0a22cf98fb docs(api): document the permission registry and correct two enforcement claims
clinic.md's default envelope is regenerated from the running app, so it now shows
all 17 resources instead of 13, including services with its full create/delete
actions. Both role docs point at permission.md for the shared registry and spell
out the merge rule that makes new resources work on existing rows: deleting a key
means "take the default", not "deny" — denying requires an explicit false.

A systematic sweep over every gated route with all permissions off found two
places where the docs claimed enforcement that does not exist:
- GET /api/v1/subscription/my returns 200 with every permission off. Only trial
  is gated.
- ServiceCatalogController has no gate at all.

Both are pre-existing and both are left as-is rather than half-fixed: their
endpoints are also consumed by the booking and subscription flows, where a hard
gate would break secretaries who legitimately need them. The docs now say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:20:49 +03:30
hamed a0fd85d910 chore(graph): rebuild after dynamic permission UIs 2026-08-07 18:11:25 +03:30
hamedandClaude Opus 5 ddd5f8f75a feat(permissions): render both permission forms from the catalog, fix borrowed gates
The three hardcoded resource lists in the admin panel are gone. MySecretariesPage,
SecretariesPage and DoctorPermissionsModal now render from
GET /api/v1/permission-catalog, so a resource added to the backend registry shows
up in all of them with no frontend change. Each has a test that proves exactly
that by adding a resource to the mock and asserting it renders.

SecretaryPermissions was an interface with a field per resource, which made
"dynamic" impossible in TypeScript — every new resource would have been a compile
error. It is now an open map. Only two files consumed it.

The borrowed gates are corrected:
- five resource pages moved off appointment_settings onto their own 'resources'
- treatment-cases moved off appointments onto 'treatment'
- service-categories moved onto 'services', which is what ServiceCatalogController
  actually manages (categories, item groups, service relations) — not resources

TreatmentCaseController had no permission gate at all, only IS_AUTHENTICATED_FULLY,
so any secretary could read and edit treatment cases. All seven of its actions are
now gated on treatment view/update.

ResourcePermissionTrait takes the resource from an overridable method instead of
hardcoding appointment_settings. HolidayController overrides it back, since the
holidays page really is appointment settings. The booking gate keeps its
appointments.view fallback so a secretary who may book is not blocked by a
resource-config permission.

Defaults were picked to preserve today's effective access, so no role gains or
loses a page from this move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:11:05 +03:30
hamed dc40651308 chore(graph): rebuild after permission catalog endpoint 2026-08-07 17:40:11 +03:30
hamedandClaude Opus 5 5211b34d0e feat(permissions): expose the registry over GET /api/v1/permission-catalog
Both permission forms in the admin panel can now render from the backend
registry instead of their own hardcoded lists. Resources come back as an array
so display order is part of the contract, each carrying its Persian label, its
actions, and the clinic_only flag that used to live in the frontend.

contextPermissions() normalizes the no-row branch through the registry too, so
a doctor whose permission row was never provisioned sees the same shape as one
who has it.

Two existing assertions compared the API response against DEFAULT_PERMISSIONS
by identity. The values are unchanged; only key order moved to the registry's,
so both now compare through PermissionCatalog::merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:39:45 +03:30
hamed aff7b7fd4a chore(graph): rebuild after permission registry 2026-08-07 17:35:08 +03:30
hamedandClaude Opus 5 1d1efd7a85 feat(permissions): single registry for secretary and clinic-doctor permissions
The list of permissionable resources was duplicated in six places that had
already diverged: both permission entities, three admin UI files and the
SecretaryPermissions TypeScript interface. Adding a resource meant editing all
of them, so new pages borrowed an unrelated resource instead — five resource
pages sat on appointment_settings.view and treatment-cases on appointments.view.

PermissionCatalog is now the only place that says which resources and actions
exist. Each entity keeps its own DEFAULT_PERMISSIONS, but as role policy only;
a test asserts those defaults never name a resource the registry doesn't have.

getPermissions() merges the stored JSON over the role defaults, so a resource
added to the registry later resolves to the role default instead of silently
false for every existing row. Explicitly stored values are never overwritten,
and no data migration is needed.

Two asymmetries fixed along the way:
- ClinicDoctorPermission validated writes against its own DEFAULT_PERMISSIONS,
  so services.create/delete could never be stored for an invited doctor.
- DoctorSecretary had no validation at all and would store any key, and it only
  read $patch['resources'] — the admin SecretariesPage sends a flat map, so its
  permission edit silently did nothing. Both entities now accept either shape
  and filter through the registry.

New resources 'resources' and 'treatment' are registered with defaults chosen to
preserve today's effective access, since both pages are currently gated on a
borrowed resource.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:32:28 +03:30
hamed 0360a5e46f Refactor code structure for improved readability and maintainability 2026-08-07 17:16:10 +03:30
hamedandClaude Opus 5 01100b90ad chore(graph): rebuild after collapsible session cards
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:12:38 +03:30
hamedandClaude Opus 5 7a6bc313d2 refactor(admin): collapse session cards in the course detail
A finished session prints three areas with their device readings, so a course
with any history pushed the sessions that still need work off the screen.

Each session is a collapsible card now, with the scannable facts kept in the
head — number, status, date, staff, and an area count so opening is a decision
rather than a guess. History starts collapsed; a session that still needs
booking starts open, because its button is the reason it is on the page and
should not sit behind an extra click. A session with neither areas nor an
action has no body and renders as a plain row rather than an empty toggle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:12:28 +03:30
hamedandClaude Opus 5 bc8003562a chore(graph): rebuild after treatment card view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:05:09 +03:30
hamedandClaude Opus 5 f61b79b67e refactor(admin): course cards with a detail view, and fill the banner's next date
The tab stacked every course's full session list on one page. A protocol allows
sixty steps, so one open course was enough to bury the others. Courses are cards
now — service, progress, supervising doctor, staff — and opening one replaces
the list with its detail: a back button, two tabs (the whole course, or only
what is still to come), search and paging inside each. The choice lives in the
URL so browser-back returns to the same course.

Booking stays where the work is: the button sits on the session card inside the
upcoming tab, not on a separate page.

The patient banner's 'نوبت بعدی' read '—' for anyone mid-course, because it only
looked at booked appointments and a course's later sessions have none yet. It
now falls back to the next session of the active course and relabels itself
'جلسهٔ بعدی' when it does — a planned session is not a booking, and the banner
should not call it one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:04:55 +03:30
hamedandClaude Opus 5 8672608696 feat(admin): let the booking date be changed inside the modal
The modal took its date from the caller and had no way to change it. Opened from
a session card, that meant the projected date or nothing: if the device had no
free time that day the user had to close a form they had already filled with a
patient and a service, go elsewhere, and start again.

The date is now a field in step one, seeded from the prop and reset when the
prop changes. ServiceSlotPicker already clears the picked slot when its date
changes, so the times reload for the new day on their own.

The header chip drops the date whenever that field is on screen; showing the
same value twice, one editable and one not, invites the reader to trust the
wrong one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:57:17 +03:30
hamedandClaude Opus 5 e385056f09 chore(graph): rebuild after treatment tab rename and prefill
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:43:49 +03:30
hamedandClaude Opus 5 288919339e feat(admin): name the tab for what it shows, prefill the patient, paginate and search
The tab was called 'نوبت‌های بعدی' but shows the whole course — finished sessions
with their recorded readings as much as upcoming ones. It is 'دوره‌های درمان' now.

Booking from a session still made the user search for a patient the page already
had open. The plan response carries the patient's national code (from the
profile, falling back to the user — the same COALESCE PatientController uses,
because users.national_code is routinely empty), and the modal takes a patient
prop that seeds the lookup and hides the search step. The old 'بیمار یافت شد'
card is suppressed in that mode; saying it twice is noise.

Sessions are now searchable and paged. A protocol allows up to 60 steps and a
patient can hold several courses, so an unbounded list was only ever going to
work for the small cases. Search filters on what the card actually shows —
service, staff, status, session number, area names — and runs in the page,
since /plan already returns the whole course and a round trip would add latency
and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:43:34 +03:30
hamedandClaude Opus 5 02fdfd13cb chore(graph): rebuild after patient treatment plan tab
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:28:27 +03:30
hamedandClaude Opus 5 c4099f0f63 feat(admin): default the booking staff from the service's protocol, and call it پرسنل
The staff field started empty even when the service's treatment protocol already
named who may perform it — a decision made once in the service settings and then
asked again on every booking. The modal now reads that protocol and preselects
its first staff member, but only until the user touches the field; otherwise a
manual choice would be wiped on the next service change. A protocol with no
staff leaves it empty and does not block submission.

Renames the four user-facing 'اپراتور' strings to 'پرسنل', matching the record
in /admin/staff that they all refer to. ResourcesPage keeps the word: there it
names a kind of bookable resource (doctor, operator, room, device), not a
ClinicStaff row.

Drops a test whose premise the default invalidated; the two new ones cover both
sides — protocol with staff sends staff_uuid, protocol without staff sends none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:28:13 +03:30
hamedandClaude Opus 5 de52d668c0 feat(admin): 'next appointments' tab in the patient file
A patient's file had no view of their multi-session courses: which ones they
have, when the remaining sessions fall, or what was recorded in the sessions
already done. All of it lived on a tenant-wide page.

The tab lists the patient's courses and, per course, a card for every session
with its date, its status, and — for finished ones — the areas treated with the
device readings a staff member logged. Estimated dates are labelled as such, so
a projection is never read as a booking.

Each unbooked session carries a button that opens the same NewAppointmentModal
used elsewhere, seeded with that session's date, and now binds the resulting
appointment to that exact session via a new treatmentSessionUuid prop — a
patient can have several open courses, and without it the attachment falls back
to guessing from the service.

The modal opens in resource mode, not doctor mode: a course's service is booked
against the device's calendar, so useDoctorBookingServices returns nothing for
the supervising doctor and the picker would render 'no bookable services'. The
plan response now carries the course's device for exactly this. A course with no
device yet says so instead of offering a button that cannot work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:21:31 +03:30
hamedandClaude Opus 5 fc50ac4b3b feat(treatment): endpoint for a course's calendar and its recorded work
GET /api/v1/treatment-case/{uuid}/plan returns every session with a date and an
is_estimate flag, plus each session's area records — the device readings a staff
member actually logged. Until now nothing exposed either: due_at existed only
for the next session, and TreatmentCase::toArray() serialised sessions without
their areas, so 'what was done' was unreachable outside the staff panel.

Kept separate from GET /treatment-case/{uuid}; that response feeds the edit
modal, which needs neither the calendar nor the areas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:12:55 +03:30
hamedandClaude Opus 5 50d82279d6 feat(treatment): project a whole course's calendar without storing it
TreatmentScheduler deliberately writes only the next session's due_at, because a
date not yet anchored to anything real is a false claim about the future and has
to be rewritten every time a patient runs late. But the panel still needs to
show the whole course.

TreatmentPlanProjector builds that chain at display time and writes nothing.
Each date carries is_estimate so a projection is never mistaken for a fact. A
session's anchor is, in order: when it was finished, when its appointment is, or
its written due_at; a case with none falls back to when it was opened, so a
course that has not been booked yet still shows dates instead of blanks.

Read and write stay in separate classes — mixing them risks storing an estimate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:08:19 +03:30
hamedandClaude Opus 5 250e0b0813 feat(treatment): filter treatment cases by patient record
The list could be narrowed by status, search and open-date, but not by patient
— so a patient's own file had no way to ask which courses belong to them.
`?record=` adds that bound.

patientRecord is joined once and shared with the search branch; joining it twice
under the same alias is a DQL error, and search already needed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:05:35 +03:30
hamedandClaude Opus 5 fbaf98a1f5 chore(graph): rebuild after explicit session booking
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:44:36 +03:30
hamedandClaude Opus 5 c7a3b88b32 feat(treatment): bind an appointment to a chosen session, and free it on cancel
Two holes in how a course's later appointments were made.

The link from the unbooked queue carried nothing — `/admin/appointments/new`
with no parameters — so the secretary retyped the patient and the service, and
which case the appointment joined was inferred from the service they happened to
pick. A patient with two open courses had no way to say which one they meant,
and picking the wrong service silently opened a third case. (The suggestion link
did pass slot_start and resource_uuid, but the create page never read either.)

POST /api/v1/my/appointment now takes an optional treatment_session_uuid.
SessionBookingLink validates it — same tenant, still unbooked, case open, same
patient — and reserves that session. Confirm-time attachment steps aside when
the appointment already holds a session. The booking form states in words which
session, which course and which patient it is about to book, read from a new
GET /api/v1/treatment-session/{uuid}.

Nothing ever detached a session from its appointment, so a cancelled booking
left the session `booked` forever, and since findNextUnbooked requires
"has no appointment", it could never return to the queue. Cancellation and
no-show now release it back to `planned`. A finished session is history and is
left alone.

The system still never books the next appointment by itself — it only suggests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:44:22 +03:30
hamedandClaude Opus 5 d857242145 chore(graph): rebuild after treatment cases table view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:13:42 +03:30
hamedandClaude Opus 5 d3f7812d15 feat(admin): table/card views for treatment cases, matching the patients list
The list was cards only, so comparing courses across patients meant reading four
stacked blocks instead of scanning columns. It now has the same table/card
toggle the patients page uses, reusing that page's toggle icons and keeping the
choice in the URL so back and refresh hold it. Table is the default here: the
operator, status and session progress are the columns a manager scans.

Both views derive the operator the same way, through one helper — history
(performed_by) when a session has been done, otherwise the plan
(assigned_staff), labelled as not yet performed.

Also fixes a three-states slip in the operator picker I added last time: it
rendered "no staff defined" while the staff list was still loading, which is
what an empty list looks like from the user's side. Loading now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:13:27 +03:30
hamedandClaude Opus 5 9cabb28355 chore(graph): rebuild after per-case operator assignment
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:55:16 +03:30
hamedandClaude Opus 5 b78f7311cf feat(treatment): per-case operators, shown on the list and searchable
A treatment case said which doctor supervised it but never who actually did the
work, so the list could not answer the first question a manager asks about a
course: who performed it.

Two separate things now travel with the case. `performed_by` is history —
derived from the sessions' performedBy, so it only ever reports what happened.
`assigned_staff` is plan — a new treatment_case_staff table, editable from the
modal, saying who is meant to handle this patient's course. The card shows the
first and falls back to the second while nothing has been performed yet.

Search matches both. A manager typing an operator's name wants that person's
work, and work already done is part of it.

Assignment also narrows the operator queue: a case with assigned staff shows its
sessions only to those people, because a patient who started a multi-session
course with one operator should keep them. An unassigned case keeps the existing
protocol rule, and an empty list means "anyone the protocol allows" rather than
"nobody" — the same "no rows is not a restriction" convention used elsewhere.

Unlike areas, removing an operator erases nothing: a finished session carries its
real operator on itself and never consults this list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:55:01 +03:30
hamedandClaude Opus 5 12c0d57e1a chore(graph): rebuild after removing services from the staff dashboard
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:38:35 +03:30
hamedandClaude Opus 5 e43e8ec93e refactor(dashboard): drop the services list from the staff dashboard
The assigned-services card and its stat card are gone. The staff role has one
job — today's sessions — and with /admin/my-services already removed, a list of
services the operator cannot act on was the last thing on the page competing
with the work itself.

The remaining stat card no longer stretches the page: `.stat-grid` is built for
four cards and collapses to one full-width column with a single child, so the
staff grid is capped.

The endpoint still returns `services` and `stats.services`; the field is
documented and cheap, and nothing else needed to change to stop rendering it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:38:21 +03:30
hamedandClaude Opus 5 93c11139fd chore(graph): rebuild after area skip note and reopen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:28:48 +03:30
hamedandClaude Opus 5 a182b05e1f feat(treatment): skip reasons and reopening for session areas
Skipping an area recorded only that it was skipped. Why it was skipped is
clinical history — the next session needs to read it — so `skip` now takes an
optional note, the same way completing an area already did, and the panel asks
for it inline instead of firing on the first click.

An operator finds out mid-laser that they closed the wrong area, and until now
had to carry that mistake to the end of the session. `reopen` puts a settled
area — completed or skipped — back to in_progress and clears finished_at,
keeping the recorded parameters and note so they can be seen and overwritten.
It stops at the same boundary everything else in this domain stops at: once the
session is finished the record is history, and reopening it is 409.

Also drops /admin/my-services. The staff role has one job — today's sessions —
and the dashboard already lists the services they may perform, so the page was
a second place to read the same list. Route, page, sidebar entry and the two
links to it are gone; the services stat card is no longer a link because it no
longer has a destination.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:28:30 +03:30
hamedandClaude Opus 5 a6180bc7e7 chore(graph): rebuild after staff dashboard fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:16:15 +03:30
hamedandClaude Opus 5 5b3e80f92a fix(dashboard): make the staff dashboard show the operator's actual work
Every number on it was zero. "نوبت‌های امروز من" counted appointments where
appointments.staff_id matches — a column no booking path fills by default, and
which is NULL on every row in the database. "سرویس‌های من" read only direct
service assignment, so an operator whose whole job comes from a treatment
protocol was told they had no services. The landing page of the only role that
has one data page said, in effect, that they had nothing to do — while they had
two sessions booked that day.

Today's work now comes from TreatmentSessionRepository::findTodayForStaff, the
same queue rule the sessions page uses, so there is one definition of "my work
today" rather than two that disagree. Services are the union of direct
assignment and protocol authorisation.

The two stat cards are links to the pages they name; a number with no
destination made the user hunt the sidebar for a page the card had just
mentioned. Each row of the work list opens that session. The avatar moves from
its own full-width card into the header — two lines of text were costing a card
and pushing the day's work below the fold on mobile. The assigned-appointments
table renders only when it has rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:16:00 +03:30
hamedandClaude Opus 5 384ea7b180 chore(graph): rebuild after treatment case date filter
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:57:52 +03:30
hamedandClaude Opus 5 73963020e2 feat(treatment): date-range filter on treatment cases, and time on the start stamp
The list could be narrowed by status and by search but not by when a case
opened, which is the one axis a clinic actually reports on. `from` and `to`
(YYYY-MM-DD) now bound `opened_at`, using the same strtotime day-boundary
convention the appointment date filters already use under the app's global
Tehran timezone. A malformed value is ignored rather than erroring — this is a
filter, not a form field.

Both bounds live in the URL via useUrlState, so back and refresh keep the range.
The two date inputs and the "تا" between them are one nowrap unit; letting them
wrap separately orphaned the word from its field on a 390px screen.

The card's "شروع" showed only the Jalali date, so several cases opened on the
same day were indistinguishable on that line too. It now uses formatDateTime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:57:14 +03:30
hamedandClaude Opus 5 2bb76258f6 chore(graph): rebuild after treatment case search and edit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:49:16 +03:30
hamedandClaude Opus 5 952e09bd6a feat(treatment): search and edit for treatment cases
The list had no way to tell two cases apart. TreatmentCase::toArray() carried
no patient, so four cases of the same service rendered as four identical
cards — same service, same supervisor, same date, same areas. Search would have
been meaningless without fixing that first, so the payload now carries the
patient (name, mobile, record number) and the card leads with the name.

Search: `?q=` on the list endpoint, matching patient name, mobile, national
code, record number and service name — the same keys a secretary already types
into the booking form. It lives in the URL via useUrlState, debounced, so back
and refresh keep the view.

Edit: PATCH /api/v1/treatment-case/{uuid} covering status, supervising doctor,
areas and session count, driven from a modal on the list. Rules live in
TreatmentCaseEditor, not the controller, around one boundary: no edit may
overwrite work already done. An area with session records cannot be removed, and
the session count cannot drop below the sessions that are booked or finished —
both 409, both tested. Reopening a closed case clears closed_at.

`areas[]` now also exposes `category_uuid`; the edit form selects catalog
categories, while `uuid` identifies the snapshot row.

Page fixes from the redesign checklist: the status filter was a hand-rolled
primary/secondary button pair, now `.seg` with `.on`; the raw `<progress>` bar
took the browser's own appearance and ignored the theme tokens, now a token-
styled bar with an explicit progressbar role; session counts go through
formatNumber; a failed request rendered as "no cases found", which reads as an
empty clinic rather than a broken one, and an empty search now says so in its
own words.

Adds the test files neither the page nor the case editor had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:48:57 +03:30
hamedandClaude Opus 5 00349cdb44 chore(graph): rebuild after session device read-only
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:00:44 +03:30
hamedandClaude Opus 5 cc2e630c40 refactor(admin): stop asking the operator to re-pick the session's device
The appointment is booked on a device, and SessionExecutor already stamps that
device onto every area record when the session starts. The area form still
showed a device select — pre-filled, but a select — so the operator was asked
to make a decision that had already been made at booking time.

The inherited device is now read-only text. A "تغییر" button reveals the
select, for the one case that still needs it: the appointment was booked on the
wrong device. An appointment with no resource at all still gets the select
outright, with a hint saying why.

Also in this file: the parameter fields wrapped a label and a SearchableSelect
inside `.field`, which is itself the bordered input box — two nested boxes. They
are `.field-block` now, each control labelled by id. The note textarea gets
`cp-textarea`; it had no styling at all. Session and area counts go through
formatNumber, so the page no longer mixes Latin and Persian digits.

Adds the test file the page never had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:00:29 +03:30
hamedandClaude Opus 5 072e10d0ba chore(graph): rebuild after staff sessions page redesign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:51:44 +03:30
hamedandClaude Opus 5 8875b8c64a refactor(admin): rebuild the staff sessions page around the operator's question
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>
2026-08-07 12:51:26 +03:30
hamedandClaude Opus 5 1366a7f15c chore(graph): rebuild after staff queue and operator picker
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:39:49 +03:30
hamedandClaude Opus 5 251b45d807 feat(admin): let the secretary pre-assign an operator when booking
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>
2026-08-07 12:39:35 +03:30
hamedandClaude Opus 5 38ea477429 fix(treatment): make the staff panel a queue instead of an assignment list
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>
2026-08-07 12:34:01 +03:30
hamedandClaude Opus 5 f5902eeeeb chore(graph): rebuild after treatment case confirm fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:25:01 +03:30
hamedandClaude Opus 5 a331aab2b8 fix(treatment): open the treatment case when confirming from the panel
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>
2026-08-07 12:24:29 +03:30
hamedandClaude Opus 5 b437390e06 chore(graph): rebuild after appointment modal redesign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:08:48 +03:30
hamedandClaude Opus 5 103d913cd0 refactor(admin): restructure the new-appointment modal around its real steps
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>
2026-08-07 12:08:25 +03:30
hamedandClaude Opus 5 1f4f7e6927 refactor(admin): make the treatment plan tab usable and reversible
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>
2026-08-07 11:29:09 +03:30
hamedandClaude Opus 5 a63de2a52c fix(treatment): let the operator actually record an area
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>
2026-08-07 11:08:48 +03:30
hamedandClaude Opus 5 c7fdb92df4 feat(admin): practice domain settings, treatment case list and platform domain CRUD
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>
2026-08-06 21:51:21 +03:30
hamedandClaude Opus 5 d8aabe5d0a feat(admin): treatment plan tab, staff session screens and the device form editor
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>
2026-08-06 18:29:49 +03:30
hamedandClaude Opus 5 77eeefd5b4 feat(treatment): run a session from the staff panel, area by area
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>
2026-08-06 18:17:27 +03:30
hamedandClaude Opus 5 1cdd62979f feat(treatment): expose treatment cases, the unbooked queue and slot suggestions
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>
2026-08-06 18:03:16 +03:30
hamedandClaude Opus 5 9a95bc59d4 fix(appointment): make resource bookings independent of the doctor's calendar
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>
2026-08-06 17:57:00 +03:30
hamedandClaude Opus 5 12c1d2cbf4 fix(appointment): fall back to the resource's supervising doctor on public booking
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>
2026-08-06 17:31:21 +03:30
hamedandClaude Opus 5 252e20bfe9 feat(treatment): select treatment behaviour by practice domain, not by if-branch
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>
2026-08-06 17:23:51 +03:30
hamedandClaude Opus 5 9af763bfbe feat(resource): let a resource type declare the fields recorded against it
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>
2026-08-06 17:05:48 +03:30
hamedandClaude Opus 5 6847a473d4 feat(treatment): open a treatment case with snapshotted areas and its sessions
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>
2026-08-06 16:53:50 +03:30
hamedandClaude Opus 5 e2e3e6b43b feat(treatment): add treatment protocols, the multi-session course of a service
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>
2026-08-06 16:36:04 +03:30
hamedandClaude Opus 5 85985b04a0 feat(practice-domain): add practice domains and let a clinic select one
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>
2026-08-06 16:06:29 +03:30
hamed 1d43475724 feat(docs): add laser treatment plan and related ADRs for multi-session treatments 2026-08-06 15:23:06 +03:30
hamed 1a07dad17c feat(online-booking): add design document for online resource booking with deposit feature 2026-08-06 11:54:44 +03:30
hamed 0e7970d6e0 feat: replace checkboxes with Switch component for better UI consistency
- 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.
2026-08-05 16:09:46 +03:30
hamed f9bbdf1e7c feat(blog-review): enhance BlogReviewPage with detailed article retrieval and loading state 2026-08-04 20:38:09 +03:30
hamed 9d767a6c63 feat(dashboard): implement Jalali month start calculation and add related tests 2026-08-04 20:31:08 +03:30
hamed df8a43f8b2 feat(resource): remove ResourceCategoriesPanel and related functionality from ResourceDetailPage 2026-08-04 20:18:38 +03:30
hamed 2db4c3c4b0 feat(subscription): add resource quota management for subscription plans and update related components 2026-08-04 19:50:47 +03:30
hamed 3e7028d77a feat(subscription): implement resource quota management based on subscription plans 2026-08-04 19:38:49 +03:30
hamed 0dca245246 feat(invoice): synchronize invoice totals with patient session updates and add resync command 2026-08-04 13:12:24 +03:30
hamed 4653cd0e4a feat(subscription): return plan features for users without subscription permission 2026-08-04 12:54:12 +03:30
hamed 85a27812c7 feat(patient): implement record number pattern management
- 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.
2026-08-04 12:30:17 +03:30
hamed 9b8eef9598 feat(appointments): implement resource-based appointment scheduling and update related logic 2026-08-04 11:39:33 +03:30
hamed 810e9351a9 feat(invoice): implement recorder identity resolution for payments and update related tests 2026-08-04 11:19:07 +03:30
hamed 2c7b86d917 feat(resource): update permissions for resource access and enhance booking logic 2026-08-04 11:05:11 +03:30
hamed 429d7ed813 feat(appointments): enhance doctor and secretary views with scheduling context and resource management 2026-08-04 10:42:20 +03:30
hamed 1db1ee4cd8 test(schedule): add tests for booking mode changes and resource strategy removal 2026-08-04 10:35:49 +03:30
hamed 41b3109478 feat(appointments): add support for doctors without schedules and display relevant notice 2026-08-04 10:26:36 +03:30
hamed 654c1e8303 feat(search): enhance user search functionality to support Persian digits and user IDs 2026-08-04 09:53:31 +03:30
hamedandClaude Opus 5 cf0ca23f82 chore(graph): rebuild after the branch removal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:58:27 +03:30
hamedandClaude Opus 5 581a553516 refactor(resource): drop the branch domain from resources
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>
2026-08-03 14:57:47 +03:30
hamed 05bb4a4ade Add AST JSON representation for ResourceBookingSlotController.php
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.
2026-08-03 14:37:05 +03:30
hamed 4f69bc9044 feat: Implement resource booking functionality
- 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.
2026-08-03 14:34:23 +03:30
hamed 981261ed3a refactor(resource): remove ResourceBookingModal component and its tests 2026-08-03 14:34:17 +03:30
hamedandClaude Opus 5 28f853400b chore(graph): rebuild after offering sections
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:47:52 +03:30
hamedandClaude Opus 5 1543eddd9e feat(resource): expose each offering's service section
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>
2026-08-03 13:47:39 +03:30
hamedandClaude Opus 5 dc728acaaf chore(graph): rebuild after the back tab
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:08:44 +03:30