- 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.
- 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.
- 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.
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>
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>
- 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.
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>
- 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.
- 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.
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
DetailsStep now renders the appointment's visit date/time first (from
session_at, falling back to created_at) and an itemized cost table (visit +
each service line with its line total) plus the grand total. Extend
SessionCardData with session_at, services_total_rials, and per-line price
fields already returned by the session API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When an appointment is confirmed, the auto-created PatientSession was saved
empty: session_at null, visit_price_rials 0, no service line items, totals 0.
Now it copies the visit datetime from the appointment slot, the visit price
(falling back to the tenant free-visit price), a SessionService line per
appointment service (price snapshot from the service), and computes
services_total_rials / final_price_rials. Verified end-to-end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce the first per-appointment event system. On cancel (via the status
or general update endpoints) an AppointmentEvent (type=cancelled, «نوبت لغو
شد») is recorded with the actor, cancel time, and an optional cancel_reason,
plus a warning-level app_log entry. New GET /appointment/{uuid}/events
returns the ordered event list. The admin appointment detail page renders a
Timeline section and the cancel dialog now collects an optional reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NewAppointmentModal and NewAppointmentDrawer never read the require-visit-
price setting nor sent visit_price_rials, so booking failed with 422
"هزینه ویزیت الزامی است" when the flag was on. Add a visit-price field
(prefilled from the free-visit price), require it when the flag is set, and
include visit_price_rials in the POST payload, matching AppointmentCreatePage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add a numeric prop to the base Input component that sets inputMode,
dir=ltr, lang=en and normalizes Persian/Arabic digits to Latin via the
shared toEnglishDigits on every change.
- Dedupe digit conversion: PriceInput now uses toEnglishDigits instead of
its local map; AppointmentsPage mobile handler uses sanitizeMobileInput.
- Fix raw national-code / mobile inputs in NewAppointmentModal and
NewAppointmentDrawer that stripped Persian digits without converting.
- Cover the numeric Input behaviour with unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Relocate the ذخیره button out of the price-input row to the bottom of the
FreeVisitPrice card, right-aligned, so it sits after the require-visit-price
toggle in a logical end-of-form position.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Export ScheduleSection from DoctorDetailPage and render the full
weekly/overrides/holidays editor in AppointmentSettingsPage. Gate the
profile's schedule render on !isOwnProfile so it no longer appears in the
doctor's own profile while the admin doctor-detail view keeps it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Introduced a new boolean flag `require_visit_price` in the `EntityInsurancePricing` to enforce visit price for appointments.
- Updated the appointment creation endpoints to validate `visit_price_rials` based on the new flag.
- Added `visit_price_rials` field to the `Appointment` entity to store the visit price.
- Enhanced the `PatientService` to validate visit price during session creation.
- Updated API documentation to reflect changes in appointment and insurance pricing.
- Implemented a new service `VisitPriceRequirementResolver` to determine if a visit price is required for a doctor based on their pricing settings.
- Added migrations to update the database schema for the new fields.
SessionStepper copied MUI's LTR connector offsets (left: -50%), which in
the RTL admin drew each connector away from its previous step — a stray
line ran off the card edge past the last step and the first two steps
had no line between them. Swap the offsets so connectors extend toward
the previous step on the right, matching tauri's MUI-flipped rendering.
The acceptance-time field showed two clock icons (custom ClockP + the
browser's native picker indicator); hide the native one like the tauri
source, which has a single ClockField adornment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin SPA never called POST /billing/invoices, so every session had
invoice_uuid = null and the view-invoice buttons on the patient services
tab (and MyPatients visit modal) were permanently disabled. Add a shared
useIssueInvoice hook (idempotent create + finalize when draft) and wire
it into:
- DetailsStep: the wizard's final step now really issues the invoice
- SessionServiceCard / PatientDetailPage: clicking view-invoice on a
session without an invoice issues it first, then opens the summary
- MyPatientsPage visit modal: same, replacing the dead disabled button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the remaining gap of tauri /files/invoice-summary into the admin
invoice summary: GET /api/v1/billing/invoices/{uuid} now includes a
'session' key (full PatientSession payload) so InvoiceSummaryModal can
render the consumables table, the itemized payments table (method,
amount, date-time, recorder) and real discount/paid/remaining figures
instead of heuristics. Invoices without a source session keep the
previous behavior (session: null).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>