Commit Graph
98 Commits
Author SHA1 Message Date
hamedandClaude Opus 5 fb1cb20c11 feat(representation): let registering reps edit their doctors and clinics
A representative could create a doctor or clinic but not finish its profile:
PATCH /api/v1/doctor/{uuid} accepted only the doctor or an admin, and the
clinic gate ran through ClinicDoctorPermissionChecker, which asks about clinic
membership — a representative is not a member. Onboarding stopped at an empty
public record.

Grant is permanent while representation_id points at the rep, and limited to
content: RepresentationEditPolicy holds ownership plus the field whitelist.
Sending a key outside it aborts the whole request with 403 and names the field,
rather than filtering the payload silently, so a rep never believes a change
saved when it did not. medical_system_code, `active` and clinic `doctors` stay
out — credential, and membership, belong to the record's owner. `active` already
has a dedicated rep endpoint.

ClinicDoctorPermissionChecker is untouched on purpose; folding a second concept
into it would give it two reasons to change.

Doctor/clinic detail responses now carry can_edit, computed by the same policy
the PATCH gate uses, so the panel reads authorization instead of re-deriving it
and drifting. Both endpoints stay public: no token means can_edit false and an
otherwise unchanged payload, which is what nobat724_front consumes.

Address endpoints follow the same policy. createAddress now resolves its target
from an explicit doctor_uuid instead of findByUser first — a representative who
also has a doctor profile was silently writing the address onto their own.

Every rep edit writes one app_log row (channel representation_edit) recording
who, what, and which field names — never values. Owner and admin edits write
nothing, keeping /admin/logs readable.

Docs corrected where they already disagreed with the code: 403/404 error codes
on both PATCH routes, a non-existent "cannot delete the last clinic address"
409, and the missing gallery-size 422.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:50:17 +03:30
hamed d74a351e5a feat: enhance search functionality to include parent names and related fields across categories 2026-08-08 14:57:36 +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
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 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 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 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
hamed 3e7028d77a feat(subscription): implement resource quota management based on subscription plans 2026-08-04 19:38:49 +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
hamedandClaude Opus 5 ab4974d174 feat(resource): every resource is supervised by a doctor
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>
2026-08-03 12:15:42 +03:30
hamedandClaude Opus 5 3e3a2482fc feat(appointments): per-resource tabs backed by a resource_uuid list filter
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>
2026-08-03 11:31:37 +03:30
hamed 8509a04ae2 feat: refactor ResourceExceptionsPanel and related hooks; remove unused availability logic and simplify resource detail management 2026-08-03 10:36:13 +03:30
hamedandClaude Opus 5 dd284ec622 refactor(branch): remove the branch domain, keep the address
Branches and rooms are not part of the resource-first product: a room is a
resource like any other, and the only thing the branch pages still managed —
opening hours — duplicated the resource's own shift.

What could not go is the address. Every appointment carries address_id (75 of
75 rows), the public booking site reads /clinic-pro/doctor-address/{id}, and a
resource derives its tenant pair from the address it belongs to. So
DoctorAddress stays as an invisible anchor with no page and no menu entry, and
GET /api/v1/addresses replaces GET /api/v1/branches for the forms that still
need to say "where".

BranchResolver was likewise not a branch feature. doctor_addresses is a global
table, so TenantFilter does not cover it and eight callers across booking,
availability, pricing and the catalog went through this resolver to avoid
leaking another clinic's address. It moved to Doctor\Service\AddressResolver
rather than dying with the domain.

The availability engine loses one layer: a resource's real hours were the
branch hours intersected with its shift, and are now the shift alone. That is
the single behavioural change, and the three tests that asserted the old
contract are replaced by one that states the new one.

Rooms already had a resource row each; the migration drops only the bridge
back to `rooms`, and drops it before the table — that foreign key is ON DELETE
CASCADE and the other order would take the resources, and their appointments,
with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 15:25:32 +03:30
hamedandClaude Opus 5 f11514950e feat(services): pick a global category from the service page
The service page could not say which category a service belongs to, so the
containment edges defined in settings had nothing to match against.

A Categories tab now selects one — and only selects. Creating, renaming and
deleting stay in Settings > Categories: if every page could create one,
"whole body" would exist three times with three spellings and the
includes edge would stop catching anything.

PATCH /api/v1/service-item/{uuid} carries the choice as
catalog_category_uuid. Absent field leaves the current category alone, null
clears it, and a category from another environment is refused with 422 —
the uuid arrives in the request body where TenantFilter does not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:16:53 +03:30
hamedandClaude Opus 5 e08876f7b4 feat(resources): one tabbed page per resource
In the resource-first model a resource is the unit of capacity, so its
working hours, holidays, services, skills and categories belong to it — not
scattered across a list page's modals plus a separate calendar page.

/admin/resources/{uuid} now carries six tabs and the active tab lives in the
query string, so back and refresh land on the same view. The old
/calendar URL redirects to ?tab=hours instead of 404ing.

The skills and services modal bodies became panels the tab renders directly;
the modals are now thin wrappers, so the list page keeps working unchanged
and there is still one implementation of each editor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:06:06 +03:30
hamedandClaude Opus 5 03d8d7d68a feat(catalog): manage global categories from settings
Categories are the taxonomy both services and resources select from, but
until now they could only be reached through the service catalog, so every
environment ended up with its own spelling of "whole body".

- Settings > Categories page: global CRUD plus the "includes" edge
- POST/GET/DELETE /api/v1/service-category/{uuid}/includes — a DAG, kept
  separate from `parent` because "hand" sits under both "whole body" and
  "upper limb"; a cycle is refused with 422
- PUT /api/v1/resource/{uuid}/categories — full replacement, and a category
  from another environment is rejected explicitly since the uuid arrives in
  the request body where TenantFilter does not reach

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 12:58:26 +03:30
hamedandClaude Opus 5 26425bec31 Manage a resource's services from the panel
A "سرویس‌ها" action on each resource row opens a modal listing what that
resource performs, with its own duration and price. It follows the skills modal
exactly — same PUT-replaces-everything contract, same components, no new page
and no new route.

Leaving a cell empty means inherit, so the effective value is shown as the
placeholder along with where it came from: "40 — service default", "1,800,000
toman — branch". Without that the user cannot tell an unset field from a zero,
which is the one thing this screen has to communicate.

Two backend adjustments came out of wiring it up:

- the offering filter in findEligible is now scoped to the requirement's
  resource type. Registering lasers for a service was making the room in the
  same plan ineligible and breaking the whole booking — "who performs this" is
  about the performing role, not about rooms and support resources. The seeder
  caught this immediately.
- the scenario seeder now creates offerings and resource categories, so the
  demo data exercises this model instead of leaving every resource empty.

Three vitest tests: inherited value with its source, saving an override, and
clearing back to inheritance. Verified in the browser at 1440 dark, 1440
compact and 390 mobile — the last with no horizontal scroll.

Panel suite 648 green across 98 files, tsc clean, encore build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:57:55 +03:30
hamed c4f1f25c80 Refactor booking system: Remove unused policies, packages, and related entities
- Removed package consumption flags and related properties from PriceQuote.
- Eliminated unused domain event publishing for policies and waitlist in Schedule.
- Cleaned up BookingEngineSeeder by removing package and policy related logic.
- Updated SeedScenariosCommand to reflect removal of policies from output.
- Dropped policy, package, treatment course, cancellation, waitlist, and domain event tables in migration.
- Removed domain event assertions from tests related to resource blocking.
2026-08-01 20:50:47 +03:30
hamedandClaude Opus 5 2db7a500e6 feat(reports): restore the documented sample threshold, and draw the chart
MIN_SAMPLE goes back to the specified 10. The reason it had been lowered to 3
was real — a small clinic saw an empty report — but the fix was wrong: three
samples do not make an average, and calling that "accurate" is worse than
saying nothing.

Rows below the threshold are now returned rather than dropped, with severity
null and below_min_sample true. That refuses both mistakes: it claims no
severity it cannot support, and it does not show a small clinic an empty page
that implies everything is fine. They sort after the usable rows and render
faded with a "small sample" badge.

The utilization page gets its Recharts bar chart. The table stays underneath —
six numeric columns are not something a chart answers — but the one question
the table is bad at, "which resource is behind", is exactly what a chart is
for. Colours come from the design tokens rather than hex, which is where a
chart usually breaks in dark mode, and a resource with no calendar is left out
entirely: null is not zero, and a zero bar would be a lie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:01:40 +03:30
hamedandClaude Opus 5 5c754244f2 feat(admin): finish the screens that were stopping one step short
Five places where the data existed and the screen did not use it.

Booking a whole course had no button because it needs a doctor and the course
does not carry one — each session can be with a different doctor. The page now
asks for the doctor the same way the resource booking page does, and the button
explains that it is all-or-nothing before it is pressed.

A course whose package does not cover the remaining sessions is still valid —
the rest is simply charged normally — but nobody was told. The course response
carries package_balance and the shortfall, and the page warns. Before session
six, not during it.

The credit ledger already returned who recorded a row and which appointment it
belonged to, and showed neither. An adjustable ledger without the name of the
person who adjusted it is half an audit trail.

Version history printed a JSON blob of each version's effects, which does not
answer the question anyone actually has: what changed? It now diffs each
version against the previous one, field by field, and says so plainly when a
version changed nothing meaningful.

A resource with no calendar showed "—" for utilization. Null means undefined,
not zero, and the next step is always the same: set up the calendar. It is a
link now. The report range also accepts a custom from/to, kept in the URL like
the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 15:07:27 +03:30
hamedandClaude Opus 5 e9e61adfee feat(course): show how the course is actually going, not just how it was planned
Three gaps on the treatment-course page, all of them about the difference
between the protocol and reality.

The sessions table listed each date but not the gap between them, leaving the
operator to subtract two Jalali dates in their head. It now shows the real gap
and colours it as a warning past the protocol maximum.

A course cancelled mid-way stretches silently: the session goes back to
planned and nobody is told. The suggestion endpoint does warn, but only once a
branch is picked, so the warning could go unseen indefinitely. The page now
derives "N days since the last session, past the protocol maximum" from the
course itself, so it shows immediately.

The course's preferred resource was applied by the engine but never named in
the UI. The API now returns preferred_resource_name alongside the uuid, and
the text says plainly that it is a preference — the engine moves it up the
list, it does not hold the slot.

Two backend tests that were owed: the stricter of the protocol spacing and a
spacing policy wins (protocol 7 days, policy 21, effective 21 — otherwise a
clinic's safety rule could be bypassed by writing a short protocol), and a
session whose earliest possible date falls outside the 90-day horizon is
skipped rather than failing book-all, leaving the course untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:51:34 +03:30
hamedandClaude Opus 5 3c43955800 feat(events): domain event outbox and the two reports that close the loop
Tasks 07 through 13 each changed something the rest of the system might want
to know about, with no contract for saying so. And task 05 shipped a powerful
segment editor with no feedback on whether a clinic defined its segments right.

Events
- A closed list of names, because a consumer branches on the string and a
  one-letter typo would produce an event nobody hears and no error either
- Payloads carry uuids and scalars only; non-scalars are dropped, not
  serialised, so a consumer always fetches fresh rather than reading a stale
  detached entity
- record() deliberately does not flush: the event row commits with the change
  it describes, so a rolled-back transaction leaves no event behind. A test
  pins exactly that
- app:events:publish drains the outbox; five failed attempts park a row with
  its error rather than deleting it, because a silently dropped event is a
  loss with no trace. app:events:prune only ever removes published rows

Reports
- Resource utilisation separates available, occupied and active minutes.
  The gap between occupied and active is what exposes a bad segment
  definition, and available is multiplied by capacity so a three-chair room
  does not read as permanently over 100%
- A resource with no calendar reports utilization: null, not zero — dividing
  by zero means something different from being idle
- Plan accuracy compares planned against actual duration per service and
  flags both directions: running short wastes capacity that could have been
  sold. Its row links straight to editing that service's segments, because a
  report with no route to a fix does not get read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:27:54 +03:30
hamedandClaude Opus 5 fba1555f22 feat(cancellation): cancellation policy, no-show tracking and a waitlist
Cancelling worked but had no policy behind it: no window, no penalty, nothing
happened to the deposit, and the no_show status had no effect at all.

Two rules that are expensive to get wrong, and both are load-bearing:
- The clinic cancelling its own appointment is never charged. That check is the
  first line of the calculation, not somewhere in the middle, so a later
  refactor cannot reorder it into charging patients for the clinic's decision.
- A penalty never exceeds what was actually paid. Anything above that is a
  debt, and debt belongs to billing, not to cancellation. An unpaid appointment
  is charged nothing and the response says why.

The default is no penalty at all — a penalising default would have made every
patient with a near appointment liable the moment this deployed.

No-shows are rows, not a counter on the patient: a counter loses which
appointment and when, which makes the 12-month window impossible. Crossing the
threshold adds an existing TenantTag; it never blocks the patient, because
blocking is an eligibility policy (task 09) written on top of that same tag.

Waitlist notifies up to ten matching people and the first to book wins. An
exclusive queue reads fairer but means a freed slot sits locked for half an
hour while someone ignores their phone — so the SMS says so explicitly instead.

Insufficient wallet balance does not fail the cancellation: the slot is freed
either way. A slot should not be held hostage to money.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:06:48 +03:30
hamedandClaude Opus 5 fc504f4415 feat(course): treatment courses with protocol-driven session planning
Laser is six to eight sessions; the previous design only knew single
appointments, which is the exception rather than the rule.

- CourseProtocol per service: session count and three distinct spacings —
  min is the earliest that is clinically allowed, ideal is best, max is where
  the course starts losing its effect
- Starting a course creates every session up front as `planned` and copies the
  protocol's numbers and per-session params, so changing the protocol tomorrow
  leaves a running course alone
- Suggestions anchor on the last *completed* session, not the course start:
  when session 2 slips, session 3 moves with it
- Slots are ranked by distance from ideal, not by earliest available — day 21
  is worse than day 27 when 28 is the target
- book-all is all-or-nothing inside one transaction, with a moving anchor and a
  90-day horizon; sessions past the horizon stay planned and are reported, not
  treated as failures
- The effective minimum is the stricter of the protocol and the task-09 spacing
  policy, so a clinic rule never fights the protocol
- Cancelling one session returns only that session to planned; abandoning a
  course does not cancel its appointments, which stays an explicit decision

One active course per (patient, service) via active_course_key, the same
partial-uniqueness trick as Appointment::activeSlotKey.

Admin: CourseProtocolsPage, TreatmentCoursePage and a courses tab on the
patient record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:33:07 +03:30
hamedandClaude Opus 5 ca9648732d feat(package): session packages backed by a credit ledger
"Six laser sessions" is the common case in an aesthetics clinic: the patient
pays once and books the sessions later.

Credit is a ledger, not a counter. No table has a remaining/used_count column
and a schema test enforces that — the balance is always SUM(delta) over
append-only rows, so every number a patient sees has a full history behind it.
Corrections are new rows, never edits.

- purchase / consume / refund / adjustment / expiry, each with a reason, an
  author and the appointment it belongs to
- consume happens in confirm(), never in quote(): if the preview consumed, a
  page refresh would cost the patient a session
- cancelling adds a refund row; the consume row stays
- FIFO across a patient's packages — the oldest is closest to expiring
- an empty package is not an error, it just does not apply and the patient pays
- adjust/expire need a doctor or clinic role, and adjust always needs a reason
- app:package:expire writes the closing row so "where did my 3 sessions go?"
  always has an answer

Consume takes a pessimistic lock on the one package row. That is the opposite
of task 07's slot buckets, and docs/api/package.md carries the table explaining
why, so nobody unifies them later.

Idempotency checks for an existing consume row before inserting rather than
catching the unique violation: in Doctrine that exception closes the
EntityManager and burns the rest of the request. The unique key stays as the
last line of defence.

Admin: PackagesPage, a packages tab on the patient record, and a ledger page
whose running-balance column shows where the final number came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:11:03 +03:30
hamedandClaude Opus 5 bcfa87bfad feat(policy): rule builder and mandatory dry-run sandbox
Task 09 shipped a powerful API that a non-technical clinic owner could not
safely use. This closes that gap: activation now requires having seen what the
rule actually does.

- PolicySimulator runs a policy against real past appointments and writes
  nothing: evaluation works on facts (never entities), the whole run sits in a
  transaction rolled back and cleared in `finally`, and a test counts rows in
  five sensitive tables before and after
- activate() now demands a simulation of the *same version* — a report for
  version 1 does not unlock version 2
- PolicyTemplateRegistry: six ready-made rules, so the common case never
  touches a raw condition
- Severity from the affected ratio; 0% is a warning too, since a rule that
  changes nothing usually has a condition that never matches
- An empty clinic still succeeds with a warning, otherwise a new clinic could
  never activate anything

Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a
PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a
field that exists only in the schema shows up with no frontend change, and that
operators are filtered per field type.

The schema response now carries per-field metadata (label, type, meaningful
operators) so the form has one source of truth instead of two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:44:28 +03:30
hamedandClaude Opus 5 4d722830e3 feat(resource): calendar UI, holiday admin, backfill and interval algebra
Completes task 03. The resource calendar page edits weekly shifts, records leave and
maintenance, and previews two weeks of availability with a Persian reason for every
empty day — showing the raw server key ("outside_branch_hours") to a user would have
been a meaningless message. The preview is labelled raw on the page itself, because
booked appointments are not subtracted yet and mistaking it for bookable time leads
to overbooking.

The interval algebra moved to src/Shared/Time/TimeInterval.php with twelve unit
tests: tasks 05 and 06 need the same union/intersect/subtract, and a second
implementation is how two subtly different definitions of "overlap" get born. The
half-open [start, end) contract is what makes a shift ending at 13:00 and one
starting at 13:00 not overlap.

AvailabilityQueryCountTest locks the query count flat: one day and ninety days cost
exactly the same number of queries. Without it the first refactor can put a query
inside the day loop and a 90-day response quietly becomes hundreds of queries —
something only production would reveal.

app:resource:calendar:backfill derives shifts from existing WeeklySchedule sessions,
so the resources created in task 02 are not left with empty calendars. It skips any
resource a user has already configured, which is also what makes it idempotent. The
weekly schedule itself is untouched: this is a copy, not a migration.

Also added --replace to the holiday import. upsert keys on the date, so a row written
with a *wrong* date can never correct itself — re-running just creates the right row
beside the wrong one. That is exactly what happened after fixing the Jalali
conversion bug, and it was caught while capturing real responses for the docs.

Deferred with reasons recorded in the checklist: seasonal shift validity (two
nullable columns can be added later without backfill, so "needed from day one" does
not hold), and a Jalali date picker in the exception form.

1154 tests / 3229 assertions. phpstan at its 14-error baseline, none in touched
files. tsc clean, vitest 622 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:28:48 +03:30
hamedandClaude Opus 5 964c09cc00 feat(resource): resource types, resources, skills and pools
The document's first golden rule is "the calendar belongs to the resource, not to
the doctor". Today the only thing that can be occupied is a doctor, and ClinicStaff
is a label on services and appointments with no calendar, capacity or skills. This
adds the layer underneath: anything that can be busy — doctor, operator, assistant,
device, room, bed, chair.

Two corrections to the planned schema:

- `address_id` → doctor_addresses, not `branch_id` → a new branches table. The
  branch already exists and is the address (task 01).
- UNIQUE is (doctor_id, address_id), not (doctor_id). A WeeklySchedule is per
  (doctor, clinic) but every session inside it carries its own location_id, so one
  doctor already works at several addresses within one environment. Keying on the
  doctor alone would have made that unrepresentable — and task 03 gives each
  resource its own calendar, which is exactly per-location.

Design points worth keeping:

- Resources bridge to Doctor/ClinicStaff/Room rather than absorbing them; those
  three have live consumers (appointments.doctor_id, service_item_staff, the public
  site) and subclassing would mean migrating all of them at once. At most one bridge
  column is non-null, enforced in the entity because MariaDB will not reliably
  enforce a multi-column CHECK.
- Capacity is concurrency: a three-bed injection room is one resource with capacity
  3, not three resources, so occupancy in task 06 stays a COUNT against a limit
  instead of a merge of three calendars. A person resource is refused capacity > 1.
- Skills are a table, not rules. With 50 operators and 200 services, expressing
  "who may operate what" as policy would mean 10,000 rules.
- findEligible() uses HAVING COUNT(DISTINCT …) because "skills A and B" means both;
  a plain IN would have matched a resource holding only one.
- setup/cleanup minutes occupy the resource without being part of the patient's
  appointment, and are per-resource — distinct from the existing per-doctor
  WeeklySchedule.meta.buffer_minutes, which stays untouched.

Two real bugs found by running the backfill against real data rather than fixtures:

ResourceLinker::systemType() persisted a type without flushing, so the next lookup
missed it and created a second — the run died on "Duplicate entry 'doctor-1-staff'
for key uniq_rt_tenant_code". It now keeps an identity map for the unit of work.

The command looped over every WeeklySchedule once per environment, which is
quadratic and never finished on real data. Doctors are now a single pass keyed by
the schedule's own environment. It also flushes per environment and accepts
--pair=clinic:12, so one bad row cannot close the EntityManager and abort a
fleet-wide run, and operators can re-run for a single clinic.

Staff are the one case that cannot be derived: nothing records which branch they
work at. Rather than guessing the first one and seating them in the wrong building,
multi-branch environments are skipped and reported.

88 tests, 230 assertions across tests/Resource and tests/Branch. phpstan clean on
src/Resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 17:34:36 +03:30
hamedandClaude Opus 5 d813843fcd feat(branch): admin UI for branch working hours and rooms, plus real API docs
Three pages, all on the existing design system: BranchesPage lists the current
environment's booking locations with their working-hours and active-room counts,
and two subpages edit the week and the rooms. The list page deliberately does not
create or rename a branch — clinic and doctor detail pages already do that, and
duplicating it would give one physical place two edit surfaces. Route permission
reuses `appointment_settings` rather than inventing a new one.

Two real bugs fell out of exercising this end to end:

`days` was serialising as a JSON *array*, not an object keyed "0".."6" — keys 0..6
are sequential so json_encode collapses them to a list. The client reads days["0"]
either way, so nothing looked broken, but the response shape was unstable: one
missing day would flip the same field to an object. The controller now casts to
stdClass and WorkingHoursTest::testDaysIsAJsonObjectNotAnArray pins it. Found by
curling the endpoint for the docs, not by any test.

`<input type="time">` caps at 23:59, so it can neither display nor produce the
legal end value 1440. An all-day range would have vanished from the form and been
corrupted by the first save. Ranges now carry an explicit end-of-day flag, with a
round-trip test proving 1440 survives.

docs/api/branch.md documents all eight endpoints with responses captured from real
curl runs against ddev, including the 422 and 404 bodies. doctor.md records that
active/timezone now appear on all nine existing address endpoints (additive), and
tenancy.md gains the two lessons this task taught: an aggregate child whose root is
itself declared global inherits no environment and needs a real pair, and
TenantFilter is not a substitute for an explicit ownership check because hard
isolation only applies to a *chosen* context.

Verified: phpunit 1067 tests / 2974 assertions green; slot-mode frozen contract
green; phpstan 14 errors before and after, none in touched files; tsc clean;
vitest 87 files / 612 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:48:49 +03:30
hamedandClaude Opus 5 4fbedecec1 fix(booking): reserve conversion produced a zero-length midnight appointment
TransferReserveModal built the live appointment from appointment_time/end_time,
which on a reserve entry are both 00:00 because slot_start == slot_end. Moving a
reserve back to the appointment list silently created a zero-length appointment
at midnight. With the new duration validation it would now fail loudly instead.

Converting back now asks for a real time: the service picker in service mode,
two required time inputs in slot mode. The appointment -> reserve direction is
untouched.

GET /my/appointments has its own array-hydration serializer rather than
Appointment::toArray(), so it exposed none of the service fields the panel needs.
Added service_items (separate query, no row multiplication and no N+1),
clinic_uuid and the duration pair. This was also a hidden prerequisite of the
public-site task, whose checklist listed it as "verify first".

The reserve table now lists every service instead of only the first.

Not done, deliberately: the DataTable migration the task asked for. Its stated
reason — inline tokens breaking dark mode — does not hold; this table's th/td
already use CSS variables and dark mode works. Rewriting a working table for no
real gain is unjustified risk.

Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:04:09 +03:30
hamed 57aeb40934 feat: add staff role functionality with dashboard access and service management
- Implemented SidebarStaff component tests to ensure staff users see only their dashboard and services.
- Created StaffMyServicesPage to display assigned services for staff users.
- Added migration to link clinic staff rows to user accounts for ROLE_STAFF access.
- Defined StaffPermissions class for static permissions related to staff role.
- Introduced StaffRouteGuardSubscriber to restrict API access for staff users.
- Developed StaffAccountService for managing staff user accounts and linking them to clinic staff.
- Added comprehensive tests for StaffAccountService to validate user creation, mobile number handling, and account attachment.
- Implemented tests for staff dashboard access to ensure proper permissions and access control.
- Created tests for staff login context to verify correct environment visibility based on user roles.
2026-07-30 10:18:41 +03:30
hamed 4f4bce9fe2 feat(migrations): update franchise to percentage in tenant_insurances and tenant_service_coverage
- Changed franchise_rials to franchise_percent in tenant_insurances and tenant_service_coverage tables.
- Reset old rial values to 0/NULL as they are not convertible to percentage.

feat(command): add SeedInsuranceScenarioCommand for seeding insurance data

- Implemented a command to seed supplementary insurance contracts, patients, and claims for a specified doctor.
- Includes functionality for purging existing scenario data and generating new entries with predefined contracts and patient scenarios.
2026-07-29 13:28:59 +03:30
hamed 8d2b0d908a feat: Add online share functionality for secretaries
- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments.
- Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements.
- Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`.
- Implemented `SecretaryEarning` entity and repository for managing secretary earnings.
- Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments.
- Added `UserIbanResolver` service to handle user IBAN retrieval and management.
- Created `HasIbansTrait` for entities to manage IBANs in a JSON format.
- Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
2026-07-25 18:34:18 +03:30
hamedandClaude Opus 5 1f58b1b9b3 feat(insurance): bill an appointment with a chosen service kind and insurance
An appointment can now carry the insurance it is billed with: the service kind
(outpatient/inpatient) and the basic insurance. Confirming it no longer hands the
whole amount to the patient — the visit is split through BillingCalculator with the
coverage percent of that service kind, and the choice travels to the encounter and
the invoice built from it.

The enabled service kinds are a tenant-wide setting (all of that tenant's
insurances share it), so a tenant covering only one kind is never asked which one:
the panel resolves it the same way the server does.

- add tenant_service_category_settings + TenantServiceCategoryService, exposed on
  the existing insurance-pricing endpoint (service_categories,
  default_service_category); at least one kind must stay enabled
- add appointments.insurance_service_category / insurance_base_id with
  AppointmentInsuranceService validating them against the tenant's own settings
  and active contracts (basic only), accepted by PATCH and by confirm
- snapshot the kind on patient_sessions and invoices; the visit's coverage rule is
  resolved per kind (services keep using their own ServiceItem.service_category)
- lib/insuranceShares becomes the single client-side mirror of BillingCalculator,
  shared by the confirm modal, the appointment edit page and the session form
- surface the selection: confirm modal (with live shares), turns timeline chip,
  appointment edit page, patient record service card and invoice summary
- the session form shows the insurance block whenever the tenant has an active
  contract and prefills the patient's own insurance, so it can be changed
- fix: the confirm modal showed a zero visit price when the appointment had none —
  it now falls back to the tenant's free-visit price like the server
- fix: useServiceCategories read one level too shallow, so Persian labels never
  arrived and raw enum keys leaked into the contract summary
- fix: BlogsPage test asserted the public blogs endpoint after the page moved to
  the admin one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:50:14 +03:30
hamedandClaude Opus 5 58c6d9ac18 feat(insurance): resolve coverage percent per service category
Base insurance is a percentage-only rule: patient share is now total minus the
base share, and the contract franchise no longer inflates it (franchise stays
meaningful for supplementary contracts only).

Coverage percentages are managed centrally by admin per service category
(outpatient/inpatient, extensible via the ServiceCategory enum). A tenant
contract may override a category, otherwise it follows the admin default live —
changing the central value immediately applies to every contract that did not
override it.

- add ServiceCategory enum + GET /api/v1/service-categories as the single source
  of the category list for every client
- add insurance_coverage_defaults (+ GET/PUT admin coverage-defaults endpoints)
  and expose coverage_defaults on the insurance list and insurance-pricing
- add tenant_insurance_category_coverage; tenant-insurances accepts optional
  category_coverages (needs insurances.update) and returns the effective
  percentages with their source
- add service_items.service_category; visits always resolve as outpatient
- drop the reverse-engineered percent from patient_share_rials in MyPatientsPage
  and align the client-side BillingCalculator mirror in CreateStep

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 16:21:19 +03:30
hamed 62b2f28c4f feat(blog): add SEO fields, scheduling, and representative ownership to blog posts
- Introduced new SEO fields (meta_title, meta_description, primary_keyword, secondary_keywords, faq, internal_links, external_links, reading_time, canonical_url, og_image) to the Blog entity.
- Added scheduling capability with a scheduled_at field to manage automatic publishing of blog posts.
- Implemented representative ownership through a foreign key representation_id in the Blog entity, allowing representatives to manage their own posts.
- Updated BlogController and RepresentationBlogController to handle new fields and ensure proper data handling for SEO and scheduling.
- Created BlogWriter service to encapsulate the logic for applying SEO and scheduling fields to blog entities.
- Added PublishScheduledBlogsMessage and its handler to manage the publishing of scheduled blogs.
- Implemented ScheduledBlogPublisher service to publish drafts whose scheduled_at has arrived, respecting review status.
- Created migration to update the database schema with new fields and constraints.
- Added tests to ensure the correct functionality of new features, including SEO fields, representative scope, and scheduled publishing.
2026-07-23 22:05:23 +03:30
hamed 14730e43ce feat(blog): implement medical review gate for blog posts
- Added new fields to the Blog entity: sources, review_status, reviewer, reviewed_at, review_note, and topic_slug.
- Created API endpoints for reviewing blog posts: GET /api/v1/admin/blog/review-queue and POST /api/v1/admin/blog/{uuid}/review.
- Updated BlogController to handle review logic, including approval and rejection of posts.
- Introduced BlogReviewPage component for admin interface to manage blog reviews.
- Added migration to update the database schema for new fields.
- Implemented tests for review queue functionality and review decision handling.
2026-07-23 21:01:58 +03:30
hamedandClaude Opus 4.8 43db753942 fix(secretary): allow insurance pages; add grantable subscription resource
Insurance pages redirected to the dashboard: the insurance-pricing/claims
routes never listed `secretary`, so RoleRoute bounced a secretary who had
insurances.view and saw the menu item. Added secretary + permission
['insurances','view'] to both routes; also gated my-financial with
['payments','view'] for consistency.

«خرید اشتراک» was owner-only with no permission toggle, so it could not be
granted. Added a `subscription` secretary resource (view/create) end-to-end:
- entity DEFAULT_PERMISSIONS + SecretaryPermissions type + both secretary forms.
- backend: SubscriptionController::my (view) and trial (create),
  PaymentController::initiateSubscription (create). resolveEntity in
  SubscriptionController was already secretary-aware.
- frontend: subscription + subscription/success routes accept secretary +
  permission; settings navs gate «خرید اشتراک» by ['subscription','view'].

Tests: subscription denied-by-default / allowed-when-granted. docs/api
secretary.md updated (resource list, enforcement map, JSON example).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:21:53 +03:30
hamedandClaude Opus 4.8 e8bf2ce9b1 feat(secretary): grant staff/discounts/sms/appointment_settings/clinic_doctors (phase B)
Extends the secretary permission system to five previously owner-only modules,
so a clinic/doctor can delegate each page to a secretary. All were unreachable
by secretaries before (role-based tenant resolution returned "unknown" → 403).

New permission resources (default-deny, three-place add: entity default,
SecretaryPermissions type, both MySecretariesPage + admin SecretariesPage):
staff, discounts, sms, appointment_settings (view/update only), clinic_doctors
(clinic-only — hidden from independent doctors via `clinicOnly` section filter).

Backend enforcement (SecretaryAccessChecker, three new reusable helpers):
- resolveOwnerEntity(): owner pair from active context — used by StaffController,
  DiscountController, SmsWalletController (now secretary-aware resolveEntity).
- canForDoctor(): per-doctor-scoped check (assigned doctor + toggle) — wired into
  AppointmentSettingsController::denyDoctorAccess.
- canForClinic(): clinic-scoped check — wired into ClinicController::detachDoctor,
  ClinicDoctorPermissionController (view/update), ClinicInvitationController
  (create/view/update/delete). clinic_doctors is clinic-context only.
Guards run ahead of any subscription gate; non-secretary roles pass unchanged.

Frontend:
- RoleRoute: staff, discounts, sms-wallet, appointment-settings (doctor+clinic
  variants), settings/clinic-doctors routes accept secretary + permission gate.
- Sidebar (secretary branch): five new items gated by can(); appointment_settings
  route follows active scope; clinic_doctors only in clinic scope.

Tests: SecretaryResourceEnforcementTest — denied-by-default + allowed-when-granted
for all five (18 total). Sidebar.test — B-resource gating + clinic_doctors scope
rule. docs/api/secretary.md resource list, enforcement map, JSON example updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:29:51 +03:30
hamedandClaude Opus 4.8 9d46577181 feat(secretary): add services permission resource + panel gating (phase A)
Secretaries could reach neither the services module (EntityContextResolver
does not recognise a secretary as clinic owner, so they resolved to
`unknown` → 403) nor had any toggle to grant it. Add `services` as a
first-class secretary permission resource, enforced end-to-end.

Backend
- DoctorSecretary::DEFAULT_PERMISSIONS: new `services` resource (default-deny).
- SecretaryAccessChecker::resolveOwnerEntity(): reusable owner (clinic/doctor)
  resolution from the secretary's active context, for controllers whose data
  is fetched by [entityType, entityId] and whose generic resolver is not
  secretary-aware.
- ClinicServiceController: resolveEntity() is now secretary-aware; every action
  (sections, items, tariffs — 13 total) guards with `services` view/create/
  update/delete via denyUnlessGranted, ahead of the subscription gate.

Frontend
- SecretaryPermissions type + MySecretariesPage + SecretariesPage: `services`
  section so owners can grant it.
- Sidebar (secretary branch): services / inventory / tags menu items gated by
  can(resource, 'view').
- RoleRoute: a secretary now needs the page's `permission` to open it (direct
  URL entry included); clinic-services, inventory, tags-settings routes accept
  secretary + permission gate.

Tests
- SecretaryResourceEnforcementTest: services denied-by-default, allowed-when-
  granted, create-denied-while-view-granted.
- Sidebar.test: secretary menu gating for services/inventory/tags.

Docs: secretary.md + clinic-services.md updated with the `services` resource
and the resolveOwnerEntity note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:10:54 +03:30
hamed 5c4976d65f feat: Implement secretary permissions enforcement across multiple resources
- Added SecretaryAccessChecker to manage resource access for secretaries.
- Integrated permission checks for payments, inventory, and tags in relevant controllers.
- Updated PaymentController and PaymentMethodController to enforce secretary permissions.
- Enhanced TenantTagController to check permissions for tag management actions.
- Introduced tests for secretary resource enforcement, ensuring proper access control.
- Updated DoctorSecretary entity to include inventory and tags permissions.
- Created a comprehensive audit document for secretary permissions coverage and enforcement.
- Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
2026-07-23 16:36:35 +03:30
hamed 357adb1d14 feat: enhance payment status handling and summary in MyPaymentsPage
- Added support for 'partial' payment status in MyPaymentsPage and related components.
- Updated API responses to include 'paid_rials' and 'summary' for invoices.
- Introduced InvoicePaymentStatus service to derive payment status based on actual payments.
- Enhanced tests to cover new payment scenarios including partial payments and payment methods.
- Updated documentation to reflect changes in payment status and API responses.
2026-07-19 10:38:29 +03:30
hamed 5c8fe8ece4 feat: add payments summary endpoint and UI redesign for MyPaymentsPage
- Implemented a new API endpoint `/api/v1/my/billing/payments/summary` to provide a financial summary of payments with filters for national code, status, and date range.
- Updated the InvoiceRepository to aggregate totals for paid and unsettled invoices.
- Created a new hook `usePaymentsSummary` to fetch summary data in the frontend.
- Redesigned the MyPaymentsPage to align with the ClaimsPage structure, incorporating a design system, summary statistics, and improved filtering options.
- Added tests for the new payments summary endpoint to ensure correct functionality and filtering behavior.
2026-07-19 10:12:01 +03:30
hamed a4b07c2f80 feat(blog): add city_id to blogs for city-specific scoping
- Introduced a new nullable city_id column in the blogs table to allow scoping of blog posts to specific cities.
- Updated Blog entity to include a ManyToOne relationship with the City entity.
- Enhanced BlogController to handle city_id in the request, allowing filtering of posts by city.
- Modified BlogRepository to support querying published posts based on city_id.
- Added tests to ensure correct behavior for city-scoped and nationwide posts, including creation and updating of posts with city associations.
2026-07-19 08:23:57 +03:30
hamed 20bdc49e89 feat(claims): add tracking number and status history for claims
- Introduced `tracking_number` field in the `claims` table to store the insurance tracking number.
- Created `claim_status_logs` table to maintain a history of status changes for claims, including who made the change and when.
- Implemented `ClaimStatusLog` entity and repository for managing status log entries.
- Updated `ClaimService` to log transitions and handle tracking numbers during claim submissions.
- Added new API endpoint for fetching claims by patient, including detailed claim history and status logs.
- Enhanced frontend with a new `ClaimPatientDetailPage` to display claims and their status history.
- Added tests to ensure correct aggregation of claims and proper handling of status transitions.
2026-07-18 23:38:02 +03:30
hamed b3a5cda808 Refactor insurance share calculation logic in PatientService
- Consolidated the calculation of patient and insurance shares into a single method using BillingCalculator.
- Introduced new fields in PatientSession to store breakdown of insurance shares and patient share.
- Updated the API responses to include the new fields for consistency across payment, invoice, and claims dashboard.
- Added migration to backfill existing sessions with appropriate values for the new fields.
- Implemented tests to ensure the correctness of the new logic and verify that the breakdown sums to the gross total.
- Redesigned the claims dashboard to provide a more user-friendly overview of patient claims and their statuses.
2026-07-18 22:56:46 +03:30
hamed c13cc57c48 feat: add support for individual consumable items in service items
- Introduced `consumables` field in `ServiceItem` to allow multiple individual items alongside inventory packages.
- Created `ServiceItemConsumable` entity to manage individual consumable items linked to a service.
- Updated `ServiceItemController` to handle CRUD operations for consumables.
- Enhanced `ServiceDetailPage` and `ServiceItemFormModal` to display and manage consumables.
- Added tests to ensure functionality for adding, updating, and validating consumables.
- Updated API documentation to reflect changes in service item structure and consumables.
2026-07-18 12:34:42 +03:30
hamed c4a661b542 feat: add optional inventory package association to service items and implement audit logging
- Added `inventory_package_uuid` and `inventory_package_title` fields to the `ServiceItem` interface.
- Updated API documentation to reflect new fields in service item responses.
- Implemented methods in `ClinicServiceController` to handle inventory package associations.
- Created `ServiceItemAuditLog` entity and repository for tracking changes to service items.
- Added functionality to log changes to service items, including inventory package associations.
- Implemented tests for attaching/detaching inventory packages and auditing changes.
- Created database migrations for new fields and audit log table.
2026-07-18 12:26:15 +03:30