Task 01 planned a new `branches` table with `doctor_addresses.branch_id` bridging
to it. That plan was wrong: the branch already exists and is called
`DoctorAddress`. It carries name, address, telephone, coordinates, city/province
FKs and an owner (`forDoctor` / `forClinic` + `type`), and the whole system
already consumes it with exactly that meaning — `WeeklySchedule.sessions[].location_id`
points at `doctor_addresses.id`, `appointment-booking-locations` calls each row a
booking location, and nine CRUD endpoints plus four admin pages manage them.
A parallel table would mean two sources of truth for one physical place and a
branch that `location_id` never references.
So no `branches` table and no duplicate branch CRUD. Only the three genuinely
missing pieces:
- `doctor_addresses.active` / `.timezone`, both NOT NULL with a default so
existing rows need no backfill and no current behaviour changes. `active` is
stored only — applying it to slot calculation is task 03, since touching
`SlotCalculatorService` is off limits in this phase.
- `branch_working_hours`, keyed to `doctor_addresses.id`. Minutes from midnight
rather than "09:00" strings so range intersection stays arithmetic. PUT
replaces all seven days; validation of the whole week runs before any DELETE,
so an invalid sixth day cannot wipe the five valid ones and then answer 422.
- `rooms`, with `capacity` as concurrency (a three-bed injection room is one
resource with capacity 3, not three resources) and a deletion-guard iterator
so tasks 02 and 07 can add reasons without editing RoomService.
`BranchWorkingHours` first registered as an aggregate child of `DoctorAddress`;
TenantSchemaCoverageTest rejected it correctly, because that root is itself
declared global. It now carries a real tenant pair instead, derived in the
constructor from the address's `type` — a total mapping, and the address is only
ever listed in its own context, so nothing is hidden wrongly.
RoomController checks ownership explicitly rather than trusting TenantFilter:
hard isolation only applies to a *chosen* context, so a doctor who had not
selected one could PATCH another clinic's room. Caught by
RoomCrudTest::testForeignRoomIsNotFound, which failed with 200 before the fix.
35 tests, 97 assertions. Slot-mode frozen contract still green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The public list GET /api/v1/doctors only excluded inactive doctors
when an explicit `active` filter was passed; with no param it returned
everyone (deactivated doctors just ranked lower). Deactivated doctors
(admin toggled active_doctor_appointment off) leaked onto nobat724.
- DoctorRepository::findWithFilters: default (no `active` param) now
filters activeDoctorAppointment = true. The active=1 (bookable) and
active=0 (admin, inactive-only) escape hatches are unchanged.
- Doctor::toDetailArray: expose raw `is_active` (= activeDoctorAppointment,
independent of schedule) so public clients can 404 a deactivated
doctor's profile page; distinct from `active` (flag && has_schedule).
- Tests + docs/api/doctor.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Implemented a helper function `displayDoctorName` to prepend "دکتر" to doctor names for consistent display across the application.
- Updated various components (InviteDoctorModal, DashboardPage, DoctorDetailPage, DoctorsPage, etc.) to utilize the new helper for rendering doctor names.
- Modified the DoctorFormPage to automatically add the "دکتر" title in the UI without requiring user input.
- Fixed the EditSpecialtyPicker component to allow multiple specialty selections, resolving a UI bug where only one specialty could be selected at a time.
- Ensured that the backend strips the "دکتر" title from the name during pre-registration and doctor creation processes.
- Added tests for the new functionality, including checks for title handling and specialty selection logic.
- Updated API documentation to reflect changes in name handling and display logic.
The public doctor list had no location field, so multi-domain consumers
could not tell which city domain owns a doctor. nobat724_front's sitemap
worked around this by fetching the list once per city (35 sweeps) and
subtracting, costing ~13s to build the root sitemap.
Location is resolved in bulk by DoctorRepository::findLocationsByDoctors
using the same rule the city_id/state_id filter applies: the doctor's own
address first, falling back to the address of a clinic they belong to.
Without the clinic fallback a doctor could match city_id=X yet report no
city, which would break the sitemap's per-domain partitioning.
city/state are arrays with at most one entry, matching the shape already
used by the doctor detail response and the clinic list. A doctor with no
address reports [] rather than null. Multi-location doctors get a single
primary city, mirroring the canonical rule on the public site.
Also surface the applied page size as meta.limit. Repositories silently
clamp limit to 50, which previously made clients believe pagination had
ended early — this is what truncated the sitemap to 50 doctors.
The clinic doctor-list endpoint gets the same location data so both
endpoints agree.
Location resolution costs at most 2 queries regardless of page size,
asserted directly against the repository rather than through the
endpoint, since the endpoint carries a pre-existing specialties N+1 in
findWithFilters that is unrelated to this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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.
Eight required (nullable:false) FKs had no referential action. Set per the
codebase's existing pattern: CASCADE for owned relations (doctor_insurances,
doctor_secretaries), RESTRICT for owner/reference FKs (Doctor/Clinic.user_id,
invited_by_id, subscription plan/period). Only the two CASCADE FKs need DDL —
RESTRICT is the MySQL default.
On prod (opcache.preload + prod container), getRepository(Entity::class)
returned Doctrine's default repository instead of the custom one when the
entity's #[ORM\Entity] had no repositoryClass — so custom finders like
DoctorSecretaryRepository::findAllActiveBySecretary threw BadMethodCallException,
making /oauth/userinfo return 500 after login. Declare repositoryClass explicitly
on all 25 affected entities.
Also add app:create-admin command (create/promote a ROLE_ADMIN user by mobile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added `clinic_id` and `type` fields to `DoctorAddress` entity to differentiate between personal and clinic addresses.
- Updated constructor to support creation of addresses for both doctors and clinics.
- Modified repository methods to handle new address types and added methods for counting and finding addresses by clinic.
- Implemented migration to update the database schema accordingly.
- Removed deprecated endpoint for creating addresses from clinics and updated related controller methods.
- Added new endpoints for managing clinic addresses, including CRUD operations.
- Updated frontend components to handle new address types and display accordingly.
- Implemented GET /api/v1/dashboard/clinic to return clinic stats and today's schedule for clinic owners.
- Implemented GET /api/v1/dashboard/doctor to return doctor's stats and today's schedule for doctors.
- Implemented GET /api/v1/dashboard/secretary to return stats and conditional appointments for secretaries.
feat(migrations): create user_active_context and mobile_verification_otp tables
- Added migration to create user_active_context table for tracking active user sessions.
- Added migration to create mobile_verification_otp table for handling mobile number verification.
feat(migrations): create site_config table for application settings
- Added migration to create site_config table to store various site configuration settings.
feat(appointments): create MyAppointmentsController for user-specific appointments
- Added MyAppointmentsController to handle fetching user-specific appointments with pagination and filtering.
feat(auth): implement NotificationMobileController for mobile number verification
- Added NotificationMobileController to handle OTP requests and verification for mobile number changes.
feat(auth): create MobileVerificationOtp entity for OTP management
- Created MobileVerificationOtp entity to manage OTP records for mobile verification.
feat(auth): create UserActiveContext entity for user session management
- Created UserActiveContext entity to manage user active sessions.
feat(config): implement SiteConfigController for managing site settings
- Added SiteConfigController to handle fetching and updating site configuration settings.
feat(config): create SiteConfig entity and repository for configuration management
- Created SiteConfig entity and repository to manage site configuration data.
- Updated security configuration to include new API route for file uploads.
- Added new endpoints in AdminApiController for user statistics, toggling user status, updating user roles, and managing user details.
- Implemented doctor statistics and management endpoints, including toggling doctor status and creating new doctors.
- Enhanced user listing with filtering options for roles and status.
- Introduced DoctorFormPage component for adding new doctors with specialties selection.
- Integrated react-leaflet for mapping functionalities and added necessary dependencies.
- Updated package.json and package-lock.json to include new dependencies.
- Introduced InsuranceType enum for insurance categorization.
- Created InsuranceRepository for managing insurance entities.
- Developed LocationController for handling provinces and cities, including CRUD operations.
- Implemented City and Province entities with necessary fields and relationships.
- Added CityRepository and ProvinceRepository for database interactions.
- Established Specialty management with SpecialtyController, including CRUD operations.
- Created Specialty and Tag entities with appropriate fields and relationships.
- Implemented TagController for managing tags, including CRUD operations.
- Added TagRepository for database interactions with tags.
- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.