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>
This commit is contained in:
hamed
2026-07-18 10:02:27 +03:30
co-authored by Claude Opus 4.8
parent e4ddd38f0c
commit c103c393f3
13 changed files with 1773 additions and 1364 deletions
+26 -13
View File
@@ -1,10 +1,23 @@
# Appointment Settings API
> **Prefix:** `/api/v1/appointment-settings`
> **Permission:** All write endpoints require `AUTH` — must be the doctor owner or `ROLE_ADMIN`
> **Permission:** every endpoint requires `AUTH` and resolves access through one shared rule (below)
Doctors configure their availability via three resources: **weekly schedule**, **date overrides**, and **holidays**.
## Access rule
All 14 endpoints in this file share a single check. Given the target doctor (resolved from the path/body uuid, or from the parent schedule/override/holiday), access is granted when the caller is:
1. `ROLE_ADMIN`, **or**
2. the doctor themselves, **or**
3. the **owner of a clinic** the doctor belongs to, **or**
4. a **doctor member of that clinic** holding the `appointment_settings` permission — `view` for `GET`, `update` for `POST`/`PATCH`/`DELETE` (see `docs/api/clinic.md`*Clinic Doctor Permissions*)
Anything else → `403 ERR_AUTH_006`. This is what lets the clinic panel manage every member doctor's booking settings from `تنظیمات → نوبت‌دهی`, one tab per doctor, using the same endpoints the doctor's own panel calls.
A doctor's own settings are never affected by clinic permissions — rule 2 short-circuits before any permission lookup.
---
## Weekly Schedule
@@ -29,7 +42,7 @@ Each doctor has **one** weekly schedule (upsert). The schedule is keyed by **day
Create or update the weekly schedule for a doctor (upsert).
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
> **الزام آدرس:** هر session با `active=true` باید `location_id` (آدرس مطب/کلینیک) داشته باشد. در غیر این صورت `422 ERR_VALIDATION_001` («برای هر شیفت فعال باید آدرس انتخاب شود»). این آدرس هنگام رزرو خودکار روی نوبت ذخیره می‌شود.
@@ -190,7 +203,7 @@ Same structure as POST response.
Update weekly schedule. `{uuid}` can be schedule UUID or doctor UUID.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Request Body
```json
@@ -233,7 +246,7 @@ Updated schedule object (same structure as POST).
Delete a weekly schedule.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
> Note: route is `/booking-setting/`, not `/appointment-settings/`
@@ -259,7 +272,7 @@ Override a specific date — mark it inactive (day off) or give it custom sessio
Get all date overrides for a doctor.
**Permission:** `AUTH`must be the owning doctor or `ROLE_ADMIN` (`403 ERR_AUTH_006` otherwise).
**Permission:** `AUTH`see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).
### Response `200`
```json
@@ -289,7 +302,7 @@ Get all date overrides for a doctor.
Create a date override.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Request Body
```json
@@ -380,7 +393,7 @@ Override object (same structure as above).
Update a date override.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Request Body (all optional)
```json
@@ -419,7 +432,7 @@ Updated override object.
Delete a date override.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Response `200`
```json
@@ -436,7 +449,7 @@ Mark a date range as holiday — all slots blocked, no overrides apply.
Get all holidays for a doctor.
**Permission:** `AUTH`must be the owning doctor or `ROLE_ADMIN` (`403 ERR_AUTH_006` otherwise).
**Permission:** `AUTH`see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).
### Response `200`
```json
@@ -466,7 +479,7 @@ Get all holidays for a doctor.
Create a holiday range.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Request Body
```json
@@ -525,7 +538,7 @@ Get a single holiday.
Update a holiday.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Request Body (all optional)
```json
@@ -546,7 +559,7 @@ Updated holiday object.
Delete a holiday.
**Permission:** `AUTH`must be the doctor owner or `ROLE_ADMIN`
**Permission:** `AUTH`see [Access rule](#access-rule)
### Response `200`
```json
@@ -584,7 +597,7 @@ The `SlotCalculatorService` calculates available slots in this priority order:
### `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`
**Permission:** `AUTH`must be the owning doctor or `ROLE_ADMIN` (`403 ERR_AUTH_006` otherwise).
**Permission:** `AUTH`see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).
Returns all locations a doctor can assign as `location_id` in their schedule sessions. Includes both the doctor's personal addresses and the addresses of all clinics they belong to.
+10
View File
@@ -284,6 +284,13 @@ entity جاری از `#[CurrentUser]` resolve می‌شود: نقش `ROLE_DOCTOR
**Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`)
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ❌ | قیمت‌گذاری همان پزشک را برمی‌گرداند به‌جای موجودیت کاربر جاری. برای تب‌های نوبت‌دهی پنل کلینیک. |
با `doctor_uuid`، دسترسی این‌گونه بررسی می‌شود: `ROLE_ADMIN`، خودِ پزشک، مالک کلینیکی که پزشک عضو آن است، یا پزشکِ عضو همان کلینیک با مجوز `services.view` (برای `PUT`: `services.update`). در غیر این صورت `403 ERR_ACCESS_DENIED`؛ پزشکِ ناموجود `404 ERR_NOT_FOUND_001`. بدون این پارامتر رفتار قبلی (موجودیت کاربر جاری) دست‌نخورده است.
### Response `200`
```json
{
@@ -326,8 +333,11 @@ entity جاری از `#[CurrentUser]` resolve می‌شود: نقش `ROLE_DOCTOR
**Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`)
### Request Body
> `doctor_uuid` (اختیاری) در بدنه پذیرفته می‌شود و مثل نسخهٔ `GET` عمل می‌کند — همان قواعد دسترسی، با اکشن `services.update`.
```json
{
"doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"free_visit_price_rials": 5000000,
"require_visit_price": true,
"insurances": [