- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience.
- Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel.
- Updated documentation to reflect the addition of tours and their implementation details.
- Implemented TourProgressController to handle API endpoints for tracking guided tours seen by users.
- Created UserTourProgress entity to store the highest version of tours seen by each user.
- Developed UserTourProgressRepository for database interactions related to user tour progress.
- Introduced TourProgressService to manage business logic for marking tours as seen and retrieving seen maps.
- Added comprehensive tests for API endpoints and entity behavior to ensure functionality and data integrity.
- Updated SubscriptionPeriod interface to include tax-related fields: tax_percent, tax_rials, and payable_rials.
- Modified payment API documentation to reflect changes in tax handling for subscriptions and SMS wallet charges.
- Adjusted PaymentController to calculate payment amounts based on subscription period details instead of client input.
- Enhanced PaymentManager to handle net amounts for SMS wallet charges, ensuring tax is not credited to the wallet.
- Created PaymentTaxCalculator and SubscriptionTaxCalculator services to manage tax calculations consistently across payment types.
- Added tests for tax calculations in both subscription and SMS wallet contexts, ensuring correct behavior with and without tax enabled.
- Updated frontend components to display tax information appropriately during payment processes.
- Implemented the ability for admins to grant subscriptions to doctors and clinics without payment.
- Added new API endpoint `/api/v1/admin/subscription/grant` for granting subscriptions.
- Updated the subscription model to track the admin who granted the subscription.
- Enhanced the subscription report to include details about granted subscriptions.
- Introduced a new `is_granted` field to indicate if a subscription was granted by an admin.
- Updated the database schema to support the new functionality with a migration.
- Added tests to ensure the correct behavior of the subscription granting process.
- Added OTP login functionality in driver.mjs to handle user authentication with a fixed code in dev environment.
- Enhanced RoleRoute component in App.tsx to support clinic-scoped doctor roles and permissions.
- Updated ClinicDoctorsManager component to include pagination and search functionality for better user experience.
- Refactored tests for ClinicDoctorsManager to cover new features and ensure proper API mocking.
- Adjusted permissions in settingsMenu.ts and PracticeDomainSettingsPage.tsx to align with updated backend requirements.
- Created RoleRoute.test.tsx to validate role-based access logic for different user scenarios.
- Replaced raw input fields for pricing with PriceInput component across various forms and modals to ensure consistent formatting and accessibility.
- Updated tests to reflect changes in pricing input handling, ensuring values are displayed in toman with proper formatting.
- Enhanced accessibility by adding aria-labels and aria-invalid attributes to PriceInput components.
- Adjusted UI elements to improve layout and user experience, particularly in forms related to service items and scheduling.
- Changed labels from "نمایش در نوبتدهی" to "نمایش در نوبتدهی آنلاین" for clarity.
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>
feat: create SanitizeBlogBodiesCommand to clean existing blog bodies according to current HTML sanitization policies
test: add AppointmentTreatmentSessionLinkTest to ensure appointment booking functionality works correctly with treatment session links
- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks.
- Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content.
- Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
Adding a secretary meant deciding all 17 permission resources in the same
dialog. The full-height capture showed the form running past 1300px with the
save button below 16 accordions, and the dynamic registry makes that worse: every
page added in future lengthens this one modal.
The add/edit modal now carries only doctors, profile and address, and fits on
screen with its footer visible. Permissions move to SecretaryPermissionsModal,
reachable from a row action and opened automatically right after a successful
add, since a new secretary starts on the role defaults and the owner usually
wants to set them.
Neither create nor update sends permissions any more — the backend seeds the role
defaults on create, and the permissions modal owns the writes, fanning out over
every link row so a secretary shared across doctors stays consistent.
PermissionAccordions moves to components/ui as a shared component. Sections now
start collapsed with a granted/total badge on each header, so the panel opens at
a fixed height and still says which sections are on.
Two design-system slips caught by re-screenshotting rather than by the audit:
- a text button as a third row action pushed the name column out of the table, so
the desktop row uses an icon with a title and the mobile card keeps the label
- .btn.secondary is not defined in styles.css (variants are primary/ghost/soft/
danger/accent), so it renders as a bare .btn. Used ghost here. 25 other files
have the same dead class; left alone as a separate sweep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend gate is now per-action, so a single canUpdate driving add, edit and
delete would show buttons the server answers 403 to. Each button now checks its
own action, and the includes modal takes canCreate/canDelete so its add select
and per-edge remove button follow the same split.
ServiceCategoryTab is deliberately left alone: its save patches
/api/v1/service-item/{uuid}, which is ClinicServiceController and already gated on
services.update, so canEdit was already the right permission. Only its read of the
category tree moved behind services.view, and the page it lives on already
requires that.
The page test now drives a configurable can(), covering view-only, create-only,
update-only and delete-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The tab stacked every course's full session list on one page. A protocol allows
sixty steps, so one open course was enough to bury the others. Courses are cards
now — service, progress, supervising doctor, staff — and opening one replaces
the list with its detail: a back button, two tabs (the whole course, or only
what is still to come), search and paging inside each. The choice lives in the
URL so browser-back returns to the same course.
Booking stays where the work is: the button sits on the session card inside the
upcoming tab, not on a separate page.
The patient banner's 'نوبت بعدی' read '—' for anyone mid-course, because it only
looked at booked appointments and a course's later sessions have none yet. It
now falls back to the next session of the active course and relabels itself
'جلسهٔ بعدی' when it does — a planned session is not a booking, and the banner
should not call it one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab was called 'نوبتهای بعدی' but shows the whole course — finished sessions
with their recorded readings as much as upcoming ones. It is 'دورههای درمان' now.
Booking from a session still made the user search for a patient the page already
had open. The plan response carries the patient's national code (from the
profile, falling back to the user — the same COALESCE PatientController uses,
because users.national_code is routinely empty), and the modal takes a patient
prop that seeds the lookup and hides the search step. The old 'بیمار یافت شد'
card is suppressed in that mode; saying it twice is noise.
Sessions are now searchable and paged. A protocol allows up to 60 steps and a
patient can hold several courses, so an unbounded list was only ever going to
work for the small cases. Search filters on what the card actually shows —
service, staff, status, session number, area names — and runs in the page,
since /plan already returns the whole course and a round trip would add latency
and nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The staff field started empty even when the service's treatment protocol already
named who may perform it — a decision made once in the service settings and then
asked again on every booking. The modal now reads that protocol and preselects
its first staff member, but only until the user touches the field; otherwise a
manual choice would be wiped on the next service change. A protocol with no
staff leaves it empty and does not block submission.
Renames the four user-facing 'اپراتور' strings to 'پرسنل', matching the record
in /admin/staff that they all refer to. ResourcesPage keeps the word: there it
names a kind of bookable resource (doctor, operator, room, device), not a
ClinicStaff row.
Drops a test whose premise the default invalidated; the two new ones cover both
sides — protocol with staff sends staff_uuid, protocol without staff sends none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A patient's file had no view of their multi-session courses: which ones they
have, when the remaining sessions fall, or what was recorded in the sessions
already done. All of it lived on a tenant-wide page.
The tab lists the patient's courses and, per course, a card for every session
with its date, its status, and — for finished ones — the areas treated with the
device readings a staff member logged. Estimated dates are labelled as such, so
a projection is never read as a booking.
Each unbooked session carries a button that opens the same NewAppointmentModal
used elsewhere, seeded with that session's date, and now binds the resulting
appointment to that exact session via a new treatmentSessionUuid prop — a
patient can have several open courses, and without it the attachment falls back
to guessing from the service.
The modal opens in resource mode, not doctor mode: a course's service is booked
against the device's calendar, so useDoctorBookingServices returns nothing for
the supervising doctor and the picker would render 'no bookable services'. The
plan response now carries the course's device for exactly this. A course with no
device yet says so instead of offering a button that cannot work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two holes in how a course's later appointments were made.
The link from the unbooked queue carried nothing — `/admin/appointments/new`
with no parameters — so the secretary retyped the patient and the service, and
which case the appointment joined was inferred from the service they happened to
pick. A patient with two open courses had no way to say which one they meant,
and picking the wrong service silently opened a third case. (The suggestion link
did pass slot_start and resource_uuid, but the create page never read either.)
POST /api/v1/my/appointment now takes an optional treatment_session_uuid.
SessionBookingLink validates it — same tenant, still unbooked, case open, same
patient — and reserves that session. Confirm-time attachment steps aside when
the appointment already holds a session. The booking form states in words which
session, which course and which patient it is about to book, read from a new
GET /api/v1/treatment-session/{uuid}.
Nothing ever detached a session from its appointment, so a cancelled booking
left the session `booked` forever, and since findNextUnbooked requires
"has no appointment", it could never return to the queue. Cancellation and
no-show now release it back to `planned`. A finished session is history and is
left alone.
The system still never books the next appointment by itself — it only suggests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list was cards only, so comparing courses across patients meant reading four
stacked blocks instead of scanning columns. It now has the same table/card
toggle the patients page uses, reusing that page's toggle icons and keeping the
choice in the URL so back and refresh hold it. Table is the default here: the
operator, status and session progress are the columns a manager scans.
Both views derive the operator the same way, through one helper — history
(performed_by) when a session has been done, otherwise the plan
(assigned_staff), labelled as not yet performed.
Also fixes a three-states slip in the operator picker I added last time: it
rendered "no staff defined" while the staff list was still loading, which is
what an empty list looks like from the user's side. Loading now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The assigned-services card and its stat card are gone. The staff role has one
job — today's sessions — and with /admin/my-services already removed, a list of
services the operator cannot act on was the last thing on the page competing
with the work itself.
The remaining stat card no longer stretches the page: `.stat-grid` is built for
four cards and collapses to one full-width column with a single child, so the
staff grid is capped.
The endpoint still returns `services` and `stats.services`; the field is
documented and cheap, and nothing else needed to change to stop rendering it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Skipping an area recorded only that it was skipped. Why it was skipped is
clinical history — the next session needs to read it — so `skip` now takes an
optional note, the same way completing an area already did, and the panel asks
for it inline instead of firing on the first click.
An operator finds out mid-laser that they closed the wrong area, and until now
had to carry that mistake to the end of the session. `reopen` puts a settled
area — completed or skipped — back to in_progress and clears finished_at,
keeping the recorded parameters and note so they can be seen and overwritten.
It stops at the same boundary everything else in this domain stops at: once the
session is finished the record is history, and reopening it is 409.
Also drops /admin/my-services. The staff role has one job — today's sessions —
and the dashboard already lists the services they may perform, so the page was
a second place to read the same list. Route, page, sidebar entry and the two
links to it are gone; the services stat card is no longer a link because it no
longer has a destination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every number on it was zero. "نوبتهای امروز من" counted appointments where
appointments.staff_id matches — a column no booking path fills by default, and
which is NULL on every row in the database. "سرویسهای من" read only direct
service assignment, so an operator whose whole job comes from a treatment
protocol was told they had no services. The landing page of the only role that
has one data page said, in effect, that they had nothing to do — while they had
two sessions booked that day.
Today's work now comes from TreatmentSessionRepository::findTodayForStaff, the
same queue rule the sessions page uses, so there is one definition of "my work
today" rather than two that disagree. Services are the union of direct
assignment and protocol authorisation.
The two stat cards are links to the pages they name; a number with no
destination made the user hunt the sidebar for a page the card had just
mentioned. Each row of the work list opens that session. The avatar moves from
its own full-width card into the header — two lines of text were costing a card
and pushing the day's work below the fold on mobile. The assigned-appointments
table renders only when it has rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list could be narrowed by status and by search but not by when a case
opened, which is the one axis a clinic actually reports on. `from` and `to`
(YYYY-MM-DD) now bound `opened_at`, using the same strtotime day-boundary
convention the appointment date filters already use under the app's global
Tehran timezone. A malformed value is ignored rather than erroring — this is a
filter, not a form field.
Both bounds live in the URL via useUrlState, so back and refresh keep the range.
The two date inputs and the "تا" between them are one nowrap unit; letting them
wrap separately orphaned the word from its field on a 390px screen.
The card's "شروع" showed only the Jalali date, so several cases opened on the
same day were indistinguishable on that line too. It now uses formatDateTime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The appointment is booked on a device, and SessionExecutor already stamps that
device onto every area record when the session starts. The area form still
showed a device select — pre-filled, but a select — so the operator was asked
to make a decision that had already been made at booking time.
The inherited device is now read-only text. A "تغییر" button reveals the
select, for the one case that still needs it: the appointment was booked on the
wrong device. An appointment with no resource at all still gets the select
outright, with a hint saying why.
Also in this file: the parameter fields wrapped a label and a SearchableSelect
inside `.field`, which is itself the bordered input box — two nested boxes. They
are `.field-block` now, each control labelled by id. The note textarea gets
`cp-textarea`; it had no styling at all. Session and area counts go through
formatNumber, so the page no longer mixes Latin and Persian digits.
Adds the test file the page never had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
- Updated DoctorDetailPage, MySecretariesPage, RecordNumberSettingsPage, RepresentationsPage, ResourcePoolsPage, ResourceTypesPage, SecretariesPage, SecretaryDetailPage, SettingsPage, SkillsPage, SmsWalletPage, and TagsSettingsPage to use the new Switch component instead of native checkboxes.
- Enhanced accessibility by ensuring the Switch component uses appropriate roles and labels.
- Added tests for the new Switch component to ensure functionality and accessibility compliance.
- Updated styles to accommodate the new Switch component design.
- Add RecordNumberSettingsController for managing patient record number patterns.
- Create RecordNumberPattern entity to represent the pattern configuration.
- Implement RecordNumberPatternRepository for database interactions.
- Develop RecordNumberGenerator service for generating and validating record numbers.
- Add tests for record number generation, backfilling, and API interactions.
- Ensure proper access control for viewing and updating patterns based on user roles.
Resources never needed a branch: devices and rooms belong to the clinic
itself, and the picker always had exactly one option — a mandatory click
that decided nothing.
- `address_uuid` is now optional on resource and pool creation; when it is
missing the environment's own address is used. Clients still sending it
keep working.
- The panel no longer asks for or displays a branch anywhere: resource
form, list column and filter, pool form and column, detail row, and the
resource-first booking page.
- Availability no longer gates on `doctor_addresses.active`. That gate shut
down every device of a clinic whose address row happened to be inactive,
with a message no page in the panel could act on — no endpoint writes
that column at all.
`address_id` stays on the resource: the timezone and the tenant pair are
derived from it. It is simply no longer the user's decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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.
Returning from a resource to the doctor's own booking meant clicking the doctor
tab in the row above, which nobody found. The resource strip now leads with a
"نوبتهای خود پزشک" tab that clears the resource, so going in and coming back
happen in the same row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two reported bugs, one root cause: resource tabs were built as a rival selection
to the doctor rather than a narrower view within them.
Selecting a resource cleared the doctor. The auto-select effect then quietly put
the *first* doctor back, so anyone working under the second doctor was thrown to
the first and lost that doctor's own booking. Selecting a resource now leaves the
doctor alone; only picking a doctor clears the resource.
A resource tab also still rendered the doctor's slot timeline, just with no data.
Resources have no slotted weekly schedule — their calendar comes from service
duration and real occupancy — so showing a slot grid promises times the booking
engine does not recognise. The resource tab now renders its own panel: that day's
appointments on the resource plus a service booking entry point.
Both regressions are pinned by tests, and both were checked by reverting each fix
in turn. The first attempt at the doctor-retention test passed even with the bug
restored, because the auto-select effect masked it; it was rewritten to use the
second doctor, where the bounce is observable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>