Commit Graph
651 Commits
Author SHA1 Message Date
hamedandClaude Opus 4.8 e6422014d1 fix(appointments): file the case file on every confirmation path
Confirming an appointment was supposed to create the patient's record and its
session, and PatientService already knew how. Only two of the five paths that
confirm an appointment ever called it, and the one that mattered most did not:
a booking paid for online was confirmed inside the payment callback, which
never ran the side-effects. Every Nobat724 booking therefore went unfiled — 7
confirmed appointments in dev had no session at all.

The side-effects now run through AppointmentConfirmationService, which every
path calls: the payment callback, both PATCH endpoints, and panel/admin
bookings. Creating the record can no longer roll back a confirmation or a
payment; a failure is logged and can be repaired with the new
app:appointment:backfill-sessions command.

Two related defects fixed along the way:

- A doctor working at a clinic got two records for one appointment, one under
  the doctor and one under the clinic, so a single visit's revenue was counted
  twice. The booking context now decides, and it decides once.
- That context was inferred from address_id, falling back to "the doctor's only
  clinic" — a guess that files an appointment under the wrong practice now that
  schedules are per-context. It is stored as appointments.clinic_id instead.

Panel and admin bookings were left pending forever: nothing confirmed them and
no payment was expected. They are created confirmed.

Repeat confirmations no longer duplicate the session; an archived one still
counts as filed, so archiving a mistaken visit does not resurrect it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:55:39 +03:30
hamedandClaude Fable 5 7baa4df3d4 fix(booking): aggregate public booking state across all schedules
The public doctor payload built `active`/`free_turn`/`hours_of_work` from the
personal schedule alone, so a doctor bookable only at a clinic was reported as
"نوبت‌دهی غیرفعال". Aggregate over every schedule instead: any schedule with
online booking on and an active day makes the doctor bookable, and the disabled
label only appears when all of them are off.

Three admin-panel fixes for the same class of bug:

- AppointmentsPage took the selected doctor from `dbUuid`, which is the clinic's
  uuid inside a clinic context — the slots request 404'd. Use `doctorUuid`.
- TurnsTimeline rendered any error or unknown empty_reason as "این روز شیفت کاری
  ندارد". Errors now surface as errors and unknown reasons get a neutral message;
  the day-off wording is reserved for an explicit day_off from the backend.
- Admins have no clinic context, so slots fell back to the personal schedule.
  They now pick a location from `appointment-booking-locations` and that choice
  drives the slot, service and create-appointment requests.

Adds `app:schedule:normalize-format` for legacy rows stored as a bare JSON list
covering only Saturday, which read as day-off for the rest of the week.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:25:24 +03:30
hamedandClaude Opus 4.8 63bf2cdd12 chore(graphify): rebuild code graph after booking-context fix
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:35:41 +03:30
hamedandClaude Opus 4.8 7a0654f8ba fix(booking): carry the clinic context through the panel and drop phantom locations
Two faults, one root: the per-context booking work updated ScheduleSection but
left the rest of the panel calling slot endpoints without clinic_uuid. Absent
clinic_uuid means the personal practice, so the panel asked about a schedule the
doctor barely uses and got nothing back.

- useClinicContext() resolves the current environment once and is used by the
  appointments page, useDoctorBookingServices, ServiceSlotPicker and both
  queries in NewAppointmentDrawer (a fifth call site a sweep turned up). It
  returns null in a doctor's personal environment so the mirror-image bug — a
  doctor seeing the clinic's schedule at their own practice — cannot appear.
  clinicUuid is part of every query key; without it the cache leaks across
  environments.
- appointment-slots returns empty_reason (no_schedule | holiday | day_off |
  outside_window). TurnsTimeline rendered «این روز تعطیل است» for any empty day,
  which is what the bug report actually saw; it now says which of the four it is.
- booking-locations lists a location only when the context has an address and an
  active shift points at it. The dev data had three "personal" schedules whose
  shifts referenced the clinic's address, so the public site advertised a
  personal practice that could never be booked.
- ?date= adds available_on_date per location, validated as a real calendar date.
- MyAppointmentsController and AdminApiController resolved the appointment
  address with no context and could store the wrong one. Both now go through the
  new BookingContextResolver, which also replaces AppointmentController's private
  copy of the same membership check.
- app:schedule:audit-locations reports shifts pointing at a missing or foreign
  address; --fix deactivates them rather than deleting.

Verified against the reported doctor: same date, no clinic_uuid -> 0 sessions,
with it -> 1 session; a full week matches the configured Sat/Tue/Wed/Thu.

Suite: 417 tests, 2 failures — both pre-existing and unrelated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:35:31 +03:30
hamedandClaude Opus 4.8 8d31ebb3cb chore(graphify): rebuild code graph after test-profiling change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:06:47 +03:30
hamedandClaude Opus 4.8 8065ae3be1 test: enable Doctrine profiling in the test env and fix the N+1 it exposed
APP_DEBUG=0 in .env means doctrine.dbal.profiling, which defaults to
%kernel.debug%, was off in tests too, so doctrine.debug_data_holder was never
registered. Every test calling countQueries() errored out — all four N+1
regression tests had been dead for as long as they have existed. Turning
profiling on for when@test brings the harness back.

Three of the four passed immediately. The fourth was a real N+1: the
service-coverage endpoint batch-fetched its ServiceItem entities to avoid one
find() per row, but ServiceItem maps staffMembers as fetch: EAGER, so hydrating
N items fired N extra collection loads and the batch bought nothing. Six
coverage rows cost 11 queries where one row cost 6.

ServiceItemRepository::findUuidsByIds() returns the id => uuid map as a scalar
query, so no entity is hydrated and no eager collection is touched.

Also adds the query-count assertion for next_available_at that could not be
written while the harness was broken. Confirmed it fails against the previous
per-day implementation (40 queries for 2 locations, 113 for 6) and passes now.

Suite: 411 tests, 2 failures — both pre-existing and unrelated
(LowTierFixesTest, PatientWalletSessionSettleTest).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:06:38 +03:30
hamedandClaude Opus 4.8 9724504527 chore(graphify): rebuild code graph after next_available_at rework
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:59:23 +03:30
hamedandClaude Opus 4.8 63ce0f81ad perf(appointment): resolve next_available_at in one pass per location
next_available_at called getAvailableSlots() once per day for up to 30 days, and
that helper re-read the schedule, holidays and overrides on every call and then
issued an isSlotTaken() query per candidate slot. Cost grew with both the days
scanned and the slots per day, multiplied by the number of locations.

findNextAvailableStart() fetches the schedule, holidays, overrides and blocking
intervals once for the whole window and walks the days in memory.

Measured on the dev data (a doctor with two locations, first opening several
days out): 73 -> 20 queries for one request. The gap widens as locations or the
distance to the first opening grow.

Reserve appointments must keep blocking here: findBusyIntervals() filters
isReserve = false, so reusing it would have reported a reserved slot as free.
Added findBlockingIntervals(), which mirrors isSlotTaken()'s predicate, and
factored both onto a shared builder.

Verified the endpoint returns identical timestamps before and after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:59:14 +03:30
hamedandClaude Opus 4.8 ae6ecc5cf7 chore(graphify): rebuild code graph after opening-hours change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:53:39 +03:30
hamedandClaude Opus 4.8 ac6bdef9ef feat(appointment): expose weekly opening hours per booking location
The booking-locations endpoint described where a doctor can be booked but not
when, so the public site had no way to emit openingHoursSpecification and had
to fall back to availableService alone.

Each location now carries opening_hours: its active weekly shifts flattened,
with the English weekday name so the consumer can map it onto schema.org
directly. A day with two shifts appears twice; days with no active shift are
omitted. Hours are per context, so the personal practice and each clinic report
their own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:51:10 +03:30
hamedandClaude Opus 4.8 02566beacf chore(graphify): rebuild code graph after security.yaml change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:48:59 +03:30
hamedandClaude Opus 4.8 11bbc4e343 fix(security): make appointment-booking-locations publicly accessible
The new booking-locations endpoint is consumed by the public site without a
token, but it was missing from both the public_endpoints firewall pattern and
access_control, so every call returned 401 ERR_AUTH_001.

Verified against the dev data: the endpoint now returns both contexts for the
test doctor (clinic in service mode, personal practice in slot mode), ordered
by earliest free slot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:48:18 +03:30
hamedandClaude Opus 4.8 45814f254c chore(graphify): rebuild code graph after booking-context change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:38:29 +03:30
hamed f1258d206d feat(migrations): add clinic_id context to weekly_schedules, date_overrides, and holidays
- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules.
- Updated unique constraints and indexes to accommodate the new clinic context.

feat(command): create AssignScheduleClinicCommand to move schedules

- Added a command to move a doctor's personal weekly schedule into a clinic context.
- Implemented checks to ensure sessions align with the target clinic.

feat(context): implement EntityContext and EntityContextResolver

- Created EntityContext to represent the effective working environment of a request (doctor or clinic).
- Developed EntityContextResolver to determine the execution context based on user roles and active contexts.

test: add ServiceModeContextTest for appointment scheduling

- Implemented tests to ensure service booking respects clinic and personal contexts.
- Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
2026-07-18 13:32:56 +03:30
hamed 2553b45990 Add AST cache files for VisitPriceRequirementResolver, Version20260717093000, and DiscountRuleRepository
- Created JSON representation for VisitPriceRequirementResolver.php, including nodes and edges for methods and imports.
- Added AST cache for migrations/Version20260717093000.php, detailing methods and their relationships.
- Introduced AST cache for DiscountRuleRepository.php, capturing methods, imports, and their interactions.
2026-07-18 12:38:15 +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
hamed 42d9ad26c5 Add tests and implementation for ServiceDetailPage and PriceInput components
- Implement PriceInput component tests to validate Persian and Arabic numeral handling, input formatting, and controlled behavior.
- Create ServiceDetailPage component with detailed service information, including pricing, insurance coverage, and editing capabilities.
- Add API tests for service item detail retrieval and coverage synchronization with insurance contracts.
- Ensure proper error handling and user feedback for service item retrieval and coverage management.
2026-07-18 12:10:49 +03:30
hamed f1d58e1604 feat(subscription): display jalali expiry date and handle no expiry case in current plan card 2026-07-18 11:05:47 +03:30
hamed b659b84a7f feat(secretary): implement grouping of secretaries by doctor and sync profile data across links 2026-07-18 10:54:37 +03:30
hamedandClaude Opus 4.8 00cb9aaa1a feat(admin): normalize Persian/Arabic digits in every numeric field
Users typing on a Persian keyboard produced two distinct failures. Fields with
type="number" silently returned an empty string — the browser rejects Persian
digits, so the value was lost and saved as empty or zero. Text fields passed the
Persian characters straight through to the database, where a mobile stored as
۰۹۱۲… never matches 09… again. The secretary form hit the second case with no
validation at all.

Frontend:
- Adds digitsOnly() and the national-code schemas to lib/utils, plus lib/forms
  with numericField()/latinDigitsField() wrappers for React Hook Form fields.
- Converts every type="number" input to type="text" inputMode="numeric" with
  digit normalization; none remain. Fields that legitimately carry non-digits
  (sheba, landline) only get the digits translated, keeping IR and separators.
- Points the patient national-code and mobile schemas at the shared normalizing
  schemas, which accept Persian input instead of rejecting it.
- Drops two duplicate local digit converters in favour of the shared helper.

Backend:
- Adds NumericFieldNormalizerSubscriber, translating digits in whitelisted
  numeric keys of JSON request bodies under /api/v1/ before controllers run, so
  nobat724_front and clinic-pro-tauri are covered too. Translation only — no
  characters are stripped, non-string values and other keys are untouched.

Three component tests asserted on role="spinbutton" and numeric input values;
both are properties of type="number", so they were updated to match the new
text inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 10:38:56 +03:30
hamedandClaude Opus 4.8 c103c393f3 feat(appointment-settings): let clinics manage each member doctor's booking
The API and React components were already parameterized by doctor uuid, but 14
copy-pasted identity checks limited every endpoint to "the doctor themselves or
an admin", so a clinic owner could not touch a member doctor's booking setup.

- Replaces those 14 checks with one denyDoctorAccess() that also admits the
  owner of a clinic the doctor belongs to, and a member doctor holding the
  clinic's appointment_settings permission (view for GET, update for writes).
  A doctor's own settings short-circuit before any permission lookup.
- Moves ScheduleSection and its tabs out of DoctorDetailPage into
  components/schedule/ScheduleSection.tsx so the doctor panel and the new
  clinic page render the same module instead of one page importing another.
  Pure relocation — no logic changed.
- Adds ClinicAppointmentSettingsPage: one tab per clinic doctor, each rendering
  that same section. The tab wrapper is keyed by doctor uuid so in-progress
  schedule edits cannot leak onto the wrong doctor.
- insurance-pricing accepts an optional doctor_uuid (query on GET, body on PUT)
  under the same access rule, so the visit-price card works inside the clinic
  tabs. Fixes saveInsurancePricing calling getInsurancePricing with the wrong
  argument by extracting the shared pricingPayload().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 10:02:27 +03:30
hamed e4ddd38f0c feat: add per-doctor permissions management in clinics
- Implement DoctorPermissionsModal for managing doctor permissions in clinics.
- Create usePermissions hook to handle user permissions context.
- Add migration for clinic_doctor_permissions table with default permissions.
- Develop ClinicDoctorPermissionController for handling permissions API.
- Create ClinicDoctorPermission entity to manage permissions data.
- Implement ClinicDoctorPermissionRepository for database interactions.
- Add ClinicDoctorPermissionChecker for permission validation logic.
- Write tests for clinic doctor permissions functionality.
2026-07-18 09:44:13 +03:30
hamedandClaude Opus 4.8 3a23aa242e fix(clinic-invitation): provision doctor accounts and repair panel actions
The invitation flow never created an account for the invitee. accept() only
looked up an existing doctor by mobile, so for a brand-new invitee it marked
the invitation accepted and burned the token while leaving doctor_id NULL —
no login, no clinic link, and every doctor-facing endpoint 404ing afterwards.

- invite/accept now provision the users + doctors pair, claim the profile on
  accept, link it to the clinic, and SMS generated credentials when the user
  has no password. Existing passwords are never overwritten.
- accept runs in one transaction so an invitation can no longer be marked
  accepted without its doctor profile and clinic link.
- changeStatus accepts `pending`, refreshing the token and re-sending the SMS
  so reactivating a suspended invitation yields a link that actually works.
  Answered invitations are rejected with 409.
- DELETE returns 200 with the standard envelope instead of a bodyless 204,
  which made the admin panel show a false error toast; api.ts also stops
  calling res.json() on empty responses.
- The clinic-doctors settings page sent the active context uuid as the clinic
  uuid, so users holding both a doctor and a clinic context got 404 on every
  invitation action. It now always resolves the clinic context.
- Adds app:invitations:repair to fix invitations already left orphaned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 09:14:13 +03:30
hamed 1779e0d6de feat(secretary): implement multi-doctor assignment for clinic secretaries
- Added functionality to assign a single secretary to multiple doctors within a clinic, allowing for scoped access to appointments.
- Introduced `SecretaryService` to handle the logic for assigning and syncing doctors for a secretary.
- Updated `SecretaryController` to support multi-doctor assignment via new endpoints and modified existing ones.
- Enhanced `DoctorSecretary` entity to include secretary UUID in its serialized output.
- Implemented repository methods to facilitate the retrieval and management of doctor-secretary relationships.
- Adjusted appointment filtering in `MyAppointmentsController` to ensure secretaries only see appointments for assigned doctors.
- Created tests to validate the new multi-doctor assignment functionality and appointment access restrictions.
- Updated frontend components to support multi-select for doctors in the secretary management UI.
2026-07-18 08:49:04 +03:30
hamed 6ab7ed38b8 feat: refactor clinic management into a dedicated settings tab
- Removed MyClinicPage and redirected its functionality to a new ClinicDoctorsPage.
- Created ClinicDoctorsManager component for managing doctors and invitations within the settings layout.
- Updated backend permissions to allow clinic owners to detach doctors, alongside admins.
- Adjusted API documentation to reflect new permission structure.
- Updated tests to cover new functionality and permissions.
- Modified sidebar and settings menu to reflect the new structure and role-based visibility.
2026-07-17 21:56:27 +03:30
hamedandClaude Fable 5 385b81fae0 fix(patient): consistent debt/paid status, invoice totals, global service search
- patient_debt_rials now = session remaining (final - discount - paid), so the
  card status and invoice status stay consistent with is_paid even after a
  session is edited post-invoicing (no more '0 remaining but تکمیل پرداخت').
- InvoiceSummaryModal: remaining subtracts discount; the فاکتور status reflects
  actual settlement (تسویه شده / بدهکار), and the payments table shows a
  «مجموع پرداختی‌ها» total row.
- Add GET /api/v1/service-items (all owner services) and make the service
  picker searchable across all services without first choosing a section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:53:28 +03:30
hamedandClaude Fable 5 e9108e7590 feat(admin): show remaining amount in the payment-methods section
Add «مبلغ باقی‌مانده» next to the wallet balance under «انتخاب روش های
پرداخت» on the session pay step, so the operator sees what's still owed while
choosing a payment method.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:39:58 +03:30
hamedandClaude Fable 5 2ba841e8fe feat(admin): payment management tab in the session edit page
EditSessionPage now has سرویس‌ها/پرداخت‌ها tabs; the payments tab renders
PaymentStep so payments can be added, edited, and deleted (with audit) while
editing a session, not only from the pay flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:36:08 +03:30
hamedandClaude Fable 5 142d653c68 fix(patient): derive is_paid from remaining, recompute on edit, audit add-payment
- is_paid now = remaining_rials == 0 (was payment_method != pending), so adding
  a service/package after settlement correctly flips the session back to debtor.
- updateSessionServices recomputes the cached settlement (payment_method/paid_at)
  after totals change.
- addSessionPayment now records a 'create' audit entry (actor, amount, time).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:34:41 +03:30
hamedandClaude Fable 5 f2ea2ff262 feat(admin): session edit form, payment edit/delete UI, and audit history modal
- SessionServiceCard menu gains «ویرایش» and «تاریخچه تغییرات» items.
- SessionAuditModal shows the session's change history (field, op badge,
  old->new, actor, time) from /session/{uuid}/audit-log.
- PaymentStep payment rows get edit (modal) + delete (confirm) controls for
  non-wallet payments, calling the new PATCH/DELETE payment endpoints.
- CreateStep accepts an editSession prop (prefill + PATCH); EditSessionPage
  reuses it at /patients/:recordUuid/session/:sessionUuid/edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:23:02 +03:30
hamed 48579dc06f feat(session): implement auto-fill for PatientSession on appointment confirmation 2026-07-17 15:15:14 +03:30
hamedandClaude Fable 5 7da7d968b9 feat(patient): session edit + payment PATCH/DELETE + audit-log endpoints
updateSession now accepts services/consumables/visit_price/insurance and calls
updateSessionServices; discount paths pass the actor for audit. Add
PATCH/DELETE /session/{uuid}/payments/{paymentUuid} and GET
/session/{uuid}/audit-log (owner-scoped). Inject the payment + audit repos.
Verified end-to-end (edit visit price, payment edit-exceeds guard, delete +
recompute, audit trail). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:14:49 +03:30
hamedandClaude Fable 5 cd1c9529cb feat(patient): session edit + payment edit/delete service logic with audit
Add updateSessionServices (replace services/consumables/visit-price/insurance,
recompute totals), updatePayment and deletePayment (wallet payments blocked;
recompute cached payment_method/paid_at via recomputeSettlement), and thread
audit logging (logSessionChange) through service edits and the discount path.
Add remove()/findByUuid to payment repo, remove() to service/consumable repos,
and method/amount/paid_at setters to SessionPayment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:11:07 +03:30
hamedandClaude Fable 5 d8c8ba0df7 feat(patient): add SessionAuditLog entity for financial/service change trail
New SessionAuditLog (mirrors AppointmentEvent): session FK, field, operation
(create/update/delete), old_value/new_value, actor id+name, note, created_at.
Repository saves and lists a session's history (newest first, array hydration).
Migration creates the table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:06:25 +03:30
hamedandClaude Fable 5 0d943d8fc5 feat(admin): session action menu (view invoice + archive) and archive filter
SessionServiceCard's '...' icon becomes a real dropdown with «مشاهده فاکتور»
and «آرشیو»/«خروج از آرشیو» (click-outside close), plus an «آرشیو» badge on
archived cards. PatientDetailPage adds a فعال‌ها/همه/آرشیو segmented filter on
the services tab (default active), threads the filter into the sessions query,
and wires the archive action to PATCH /session/{uuid} { archived }.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:48:08 +03:30
hamedandClaude Fable 5 666833fc69 feat(patient): soft-archive sessions with active/all/archived filter
Add archived + archived_at columns to PatientSession (setArchived stamps the
time). findByRecord/countByRecord accept an archived filter (default all to
keep existing callers, incl. the discount visit-count, unchanged); the
sessions GET reads ?filter=active|all|archived, defaulting to active so
archived visits are hidden. updateSession accepts { archived }. Records are
kept, only hidden. Verified end-to-end; docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:44:23 +03:30
hamedandClaude Fable 5 53bd87e807 feat(admin): show payment date/time and recorder on session payments
Each recorded payment row (PaymentStep + DetailsStep) now shows the payment
date+time (paid_at, Jalali via formatDateTime, falling back to created_at) and
the user who recorded it (created_by_name) beneath the method/amount line. Both
fields were already in the API payload. Add created_at to SessionPaymentEntry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:40:00 +03:30
hamedandClaude Fable 5 476e219165 fix(admin): convert toman to rial when recording session payment/discount
PaymentStep sent the toman amount straight through as amount_rials (and the
fixed discount value as discount_value), so a 500,000 toman payment was stored
as 5,000,000... no — as 500,000 rial (10x too small). Apply tomanToRial before
sending the payment amount and the fixed-discount value; percent discount and
rule-based discount are unaffected. Verified stored value is now correct rial.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:38:10 +03:30
hamedandClaude Fable 5 ee69ac96be feat(admin): comma-grouped Latin amounts on the session payment step
Add an opt-in latin prop to PriceInput (en-US grouping, English digits) and
use it for the discount-value and payment-amount fields on PaymentStep, which
were raw number inputs. Amounts now show 3-digit comma grouping and Persian
digits typed are converted to English (via PriceInput's toEnglishDigits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:45:45 +03:30
hamedandClaude Fable 5 204c7debe1 fix(discount): correct edit-form data + calendar clipping + input styling
- List endpoint returned camelCase keys (getArrayResult) so the edit modal
  read empty discount_type/target_*; return toArray() (snake_case) instead.
- Portal the Persian date-picker calendar to body (fixed) so it no longer
  renders under the modal's overflow-scroll body.
- Style the PriceInput/number fields with cp-input in the rule modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:37:43 +03:30
hamedandClaude Fable 5 809ad11680 feat(discount): add «مدیریت تخفیف‌ها» to the desktop settings sidebar
The desktop settings sidebar (PurchaseSubscriptionSidebar) uses its own
NAV_ITEMS list, separate from SETTINGS_MENU; add the discounts entry there so
it shows in the settings menu on desktop too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:29:52 +03:30
hamedandClaude Fable 5 8d741767ba feat(discount): dedicated «مدیریت تخفیف‌ها» item in the settings menu
Add a standalone settings sidebar entry + route (/admin/discounts) rendering
DiscountsPage (DiscountTab inside SettingsLayout, doctor/clinic only), and
revert the temporary tab inside the subscription page. Cleaner, discoverable
placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:24:32 +03:30
hamedandClaude Fable 5 07c9600991 fix(discount): move Discount Management tab to the /admin/subscription page
The discount tab was wired into AdminSubscriptionPage (route
/admin/admin-subscription, admin-only) but /admin/subscription renders the
doctor/clinic SubscriptionPage. Since discount rules are owner-scoped, add
the segmented «مدیریت تخفیف‌ها» tab there and revert AdminSubscriptionPage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:14:48 +03:30
hamedandClaude Fable 5 2691751e70 feat(discount): apply rule-based discounts on the session payment step
PaymentStep now loads /session/{uuid}/discount-suggestions and lets the
operator apply a suggested rule (PATCH discount_rule_uuid) or remove it,
alongside the existing manual discount. Adds an amount summary (before /
discount + source rule label / final payable). Verified end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:04:56 +03:30
hamedandClaude Fable 5 18d9cc9812 feat(discount): admin Discount Management tab + uuid-based rule targets
Switch rule target references from int ids to uuids (frontend-friendly; the
engine compares uuids directly) via a migration. Add a Discount Management
tab to the subscription page with a full CRUD UI (DiscountTab): table + modal
form with per-type dynamic target fields (tenant tag / service cascade /
amount / visit count / specific patient / occasion + validity window),
priority and combinable/active toggles. Verified TSC + build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:02:03 +03:30
hamedandClaude Fable 5 b3b1dad832 feat(discount): apply a discount rule to a session with audit
Extend PatientService::applyDiscount to carry the source rule id/label and
add applyDiscountRule, which computes the rial amount via the engine
(DiscountEngine::computeForRule now public) and records the rule for audit.
updateSession accepts discount_rule_uuid (owner-scoped, takes precedence over
the manual discount; ''/null clears). Verified end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:55:16 +03:30
hamedandClaude Fable 5 6cc42c941e feat(discount): admin CRUD + session discount-suggestions endpoints
DiscountController exposes owner-scoped CRUD for discount rules
(/api/v1/admin/discount-rules) and GET /api/v1/session/{uuid}/discount-
suggestions which runs the engine for a session. Owner resolved from the
doctor/clinic user. Verified end-to-end (create, list, suggestions). Adds
docs/api/discount.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:52:03 +03:30
hamedandClaude Fable 5 cf69369231 feat(discount): add DiscountEngine to evaluate rules for a session
Evaluates all active owner rules against a session across the six types
(patient tag, invoice amount, specific patient, occasion incl. birthday,
service, visit count), honoring validity windows and capping each computed
discount at the session's unpaid remainder. Returns priority-ordered
suggestions with computed rials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:49:08 +03:30
hamedandClaude Fable 5 f0e1f43d51 feat(discount): add DiscountRule entity and session discount-rule audit columns
New Discount domain: DiscountRule (generic per-tenant rule with 6 types,
priority, combinable, validity window, and per-type target fields) plus its
repository. PatientSession gains applied_discount_rule_id/label audit columns
and setDiscount() now records the source rule. Migration creates the table
and columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:47:33 +03:30