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>
- 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.
- 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>
- 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.
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>
- 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.
Service-booking mode now selects services by section like slot mode:
appointment-booking-services returns service_section per item; ServiceSlotPicker
groups by section (SearchableSelect), accumulates picks across sections into a
removable 'section -> service' chip list.
Secretaries can override a service's duration for a single appointment without
changing the service default: appointment-service-slots accepts durations[uuid]
and both create endpoints accept service_durations; the override drives total
duration and slot_end. Online (patient) booking is unaffected — it never sends
overrides. Backend + frontend tests and docs updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced a new booking mode in WeeklySchedule to support service-based appointments.
- Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times.
- Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side.
- Implemented validation to ensure at least one bookable service exists for doctors in service mode.
- Added new API endpoint to retrieve available appointment slots based on selected services.
- Updated MyAppointmentsController to accept service items during appointment creation.
- Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling.
- Created migration to add bookable column to service_items table.
- Added tests for service-based slot calculations and validation logic.
Admin-side booking (POST /api/v1/my/appointment and
/api/v1/admin/appointment) resolved the patient User by mobile only, so
one person booked under two mobiles produced two User rows — and two
case-files, since PatientRecord is keyed on user_id. National code is the
real unique identity (User.national_code is already unique); a person may
have several mobiles.
Booking now requires + validates patient_national_code and resolves the
patient national-code-first (then mobile) via a shared PatientResolver, so
the case-file stays unique per national code even across mobiles. Reusing a
mobile already bound to a different national code returns 422
ERR_PROFILE_MOBILE_TAKEN. The admin create form and NewAppointmentDrawer
gain a national-code field and send it; both had a dead patient-picker URL
(/api/v1/patient) fixed to the real /api/v1/patients, whose payload already
carries user_national_code for autofill.
Docs (appointment.md, admin.md) and tests updated; new
AppointmentNationalCodeTest covers success, single-file reuse, missing,
invalid, and identity-conflict cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added a new 'category' field to the InventoryItem entity and updated the database schema.
- Replaced free-text input for 'unit' and 'category' with select dropdowns in the AddItemModal.
- Introduced a new API endpoint to fetch metadata for units and categories.
- Updated inventory filtering logic to use the new 'category' field instead of 'consumable'.
- Enhanced validation for item creation and updates to ensure valid unit and category values.
- Updated tests to cover new functionality and ensure proper validation.
Set primary to tauri purple, bg #fafafa/#1f1d2b, dark surface #222433,
text #D7D8ED/#A1A1A1. Sync sRGB fallbacks and @supports oklch block.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DoctorClaimController: ALTCHA CaptchaGuard on /claim (dev no-op via
ALTCHA_ENABLED=false); optional `mobile` field must match the logged-in
user's number (422 ERR_CONFLICT_001 on mismatch)
- DoctorController::delete: now IS_AUTHENTICATED_FULLY — admin (any) or the
owner of a claimed profile (IDOR-guarded); FK appointment guard kept
- DoctorDetailPage address map: MapController calls map.invalidateSize()
before flyTo (fixes needing to pick a city twice on a freshly-mounted map);
geocode retries once (nominatim empty/429 on first hit)
- tests: mobile mismatch, owner-delete allowed + others 403, unclaimed not
deletable by random user
- docs: doctor-claim.md (mobile+captcha), doctor.md (delete permission)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of "دکتر دکتر …" (and ellipsis-truncated "…نی") in admin: IRIMC
names already contain the «دکتر» title, while the panel renders «دکتر {name}».
Convention is to store the bare name.
- DoctorImportService: normalize name via PersianText::stripDoctorTitle
(also fixes ي/ی, ك/ک, half-space)
- PersianText::stripDoctorTitle now strips consecutive «دکتر دکتر …» prefixes
- app:doctors:fix-irimc-names: one-off backfill for existing source='irimc'
rows (dry-run supported) — fixed 340 rows
- app:doctors:purge: FK-safe full wipe of doctors + all dependent tables +
orphan surrogate users, for a clean test DB (dry-run default, --force to
apply, prod-guarded)
- tests: PersianTextTest cases for the title stripping; DoctorImportTest
asserts stored name has no «دکتر» prefix
- docs/api/doctor-import.md: name convention + the two new commands
Verified: import "دکتر صفورا حجازی نیا" → stored "صفورا حجازی نیا" → panel
shows single «دکتر صفورا حجازی نیا».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added the remaining components for the doctor import feature in the backend, including role management, ownership transfer endpoint, and captcha bypass for crawler service login.
- Created detailed scenarios for the doctor claim process, ensuring proper identity verification and mobile validation.
- Established a crawler interface for token management and state tracking using SQLite, enabling a resume capability for the crawling process.
- Implemented ClinicInvitationWebController to manage the invitation process via web.
- Added view and respond methods to handle invitation display and responses.
- Created result.html.twig and view.html.twig templates for rendering invitation results and views.
- Integrated CSRF protection for form submissions.
- Established routes for invitation viewing and responding.
- Introduced new columns: owner_status, source, source_ref, managed_by, and claimed_at to the doctors table.
- Created indexes for owner_status and source to optimize queries related to unclaimed doctors.
feat(auth): implement SystemOwnerCommand for managing system-owner user
- Added command to create, activate, and deactivate a system-owner user for IRIMC crawler.
- Ensured the user has ROLE_ADMIN to access import endpoints.
- Handled password setting and user status management within the command.
- Added AltchaService class for managing ALTCHA captcha challenges and solutions.
- Created CaptchaController to handle API requests for generating challenges.
- Introduced CaptchaGuard for validating captcha solutions on public endpoints.
- Developed unit tests for AltchaService to ensure challenge creation and solution verification functionality.
- Implemented integration tests for the Captcha API endpoint and captcha bypass behavior when disabled.
- Added documentation for the Captcha API in the corresponding markdown file.
- Created migration to add representation_cities table and domain, is_global fields to representations.
- Implemented SiteContextController to resolve domain to site context (city | representation | unknown).
- Developed DomainContext and DomainContextResolver services for domain mapping.
- Added tests for DomainContextResolver and commission logic based on domain ownership.
- Created JSON representation for `collect-diagnostics.sh` including nodes and edges for its structure.
- Added JSON for `ops-restarts.md` detailing various sections and their relationships.
- Introduced JSON for `server-down-oom-diagnosis.md` capturing its content and connections.
- Created a new JSON file for the City entity's AST representation, detailing its methods and properties.
- Added a migration to alter the cities table by adding a nullable title column.
- Implemented CorsRegexEnvProcessor to build CORS origin regex from a comma-separated host list (ALLOWED_FRONTEND_HOSTS).
- Added tests for CorsRegexEnvProcessor to validate regex generation and matching behavior.
- Created JSON files for AST representation of the new classes and tests.
- Removed SmsTextResolver dependency from multiple services and controllers.
- Introduced dispatchTemplate method in SmsService to handle SMS sending with templates.
- Updated existing SMS sending logic across various services (OtpService, PreRegistrationController, ClinicInvitationService, PaymentManager, SecretaryController, RepresentationActionController) to utilize the new dispatchTemplate method.
- Enhanced SmsMessageTemplate entity to include kavenegar_template and token_map fields.
- Created migration to add new fields to the sms_message_templates table and populate them with existing data.
- Updated SeedSmsMessageTemplatesCommand to handle new template structure.
- Added documentation for the new SMS template structure and usage.
- Added support for sending OTP messages using Kavenegar's VerifyLookup method, ensuring compliance with specified token formatting and template usage.
- Updated OtpService to handle new template parameters and fallback mechanisms.
- Introduced ImageCropModal component for cropping images with a user-friendly interface.
- Created utility function for cropping images and generating downloadable files.