- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency. - Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation. - Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted. - Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight. - Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
731 lines
23 KiB
Markdown
731 lines
23 KiB
Markdown
# Appointment Settings API
|
|
|
|
> **Prefix:** `/api/v1/appointment-settings`
|
|
> **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**.
|
|
|
|
## Booking context (`clinic_uuid`)
|
|
|
|
Every endpoint in this file operates inside **one booking context**, selected by the optional
|
|
`clinic_uuid` parameter (query string on `GET`/`DELETE`, body field on `POST`/`PATCH`):
|
|
|
|
| `clinic_uuid` | Context | Services usable | Addresses selectable |
|
|
|---|---|---|---|
|
|
| omitted / `null` | the doctor's **personal practice** | `entity_type='doctor'` | the doctor's own `personal` addresses |
|
|
| a clinic uuid | that **doctor inside that clinic** | `entity_type='clinic'` | that clinic's addresses |
|
|
|
|
A doctor holds **one schedule per context** — a personal one plus one per clinic — and they are
|
|
fully independent: separate sessions, separate `booking_mode` lock, separate date overrides.
|
|
Services never cross the boundary (they are polymorphic on `service_sections.entity_type`).
|
|
|
|
If the doctor is not a member of the given clinic → `422 ERR_VALIDATION_001`
|
|
(«این پزشک عضو کلینیک انتخابشده نیست»). Unknown clinic → `404 ERR_VALIDATION_002`.
|
|
|
|
## Access rule
|
|
|
|
Given the target doctor and the resolved context, access is granted when the caller is:
|
|
|
|
1. `ROLE_ADMIN`, **or**
|
|
2. the doctor themselves, **or**
|
|
3. in a **clinic context only**, someone holding that clinic's `appointment_settings` permission —
|
|
`view` for `GET`, `update` for `POST`/`PATCH`/`DELETE` (see `docs/api/clinic.md` →
|
|
*Clinic Doctor Permissions*). The clinic owner always passes this check.
|
|
|
|
Anything else → `403 ERR_AUTH_006`.
|
|
|
|
> **Breaking change (2026-07):** a clinic owner can no longer read or write a member doctor's
|
|
> **personal** schedule. Without `clinic_uuid` the request targets the personal context, which only
|
|
> the doctor and an admin may touch. The clinic panel must send `clinic_uuid`; the admin SPA already
|
|
> does (`ScheduleSection` takes a `clinicUuid` prop).
|
|
|
|
---
|
|
|
|
## Weekly Schedule
|
|
|
|
Each doctor has **one weekly schedule per context** (upsert keyed by `doctor_id` + `clinic_id`).
|
|
The schedule is keyed by **day index** (0=Saturday ... 6=Friday), each day containing a `sessions`
|
|
array.
|
|
|
|
### Day Index Convention
|
|
|
|
| Index | Day (EN) | Day (FA) |
|
|
|-------|----------|----------|
|
|
| `"0"` | Saturday | شنبه |
|
|
| `"1"` | Sunday | یکشنبه |
|
|
| `"2"` | Monday | دوشنبه |
|
|
| `"3"` | Tuesday | سهشنبه |
|
|
| `"4"` | Wednesday | چهارشنبه |
|
|
| `"5"` | Thursday | پنجشنبه |
|
|
| `"6"` | Friday | جمعه |
|
|
|
|
---
|
|
|
|
### POST `/api/v1/appointment-settings/weekly-schedule`
|
|
|
|
Create or update the weekly schedule for a doctor (upsert).
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
> **الزام آدرس:** هر session با `active=true` باید `location_id` (آدرس مطب/کلینیک) داشته باشد. در غیر این صورت `422 ERR_VALIDATION_001` («برای هر شیفت فعال باید آدرس انتخاب شود»). این آدرس هنگام رزرو خودکار روی نوبت ذخیره میشود.
|
|
>
|
|
> **الزام محیط:** آدرس انتخابشده باید به همان context تعلق داشته باشد. آدرس کلینیک در محیط شخصی (و برعکس) → `422 ERR_VALIDATION_001` («آدرس انتخابشده متعلق به این کلینیک نیست»).
|
|
>
|
|
> **نوبتدهی سرویسی:** با `meta.booking_mode = "service"` صاحبِ همان context باید حداقل یک سرویس با `bookable = true` داشته باشد؛ وگرنه `422 ERR_VALIDATION_001` روی فیلد `booking_mode`. پیام در محیط کلینیک به کلینیک اشاره میکند.
|
|
|
|
### Request Body (`application/json`)
|
|
```json
|
|
{
|
|
"doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
"clinic_uuid": null,
|
|
"schedule": {
|
|
"0": {
|
|
"sessions": [
|
|
{
|
|
"active": true,
|
|
"location_id": 1973,
|
|
"start_time": "09:00",
|
|
"end_time": "13:00",
|
|
"duration_per_patient": 20,
|
|
"has_rest": true,
|
|
"rest_interval": 60,
|
|
"time_to_rest": 10,
|
|
"patient_limit": null
|
|
},
|
|
{
|
|
"active": true,
|
|
"location_id": 1973,
|
|
"start_time": "15:00",
|
|
"end_time": "18:00",
|
|
"duration_per_patient": 20,
|
|
"has_rest": false,
|
|
"rest_interval": 0,
|
|
"time_to_rest": 0,
|
|
"patient_limit": 5
|
|
}
|
|
]
|
|
},
|
|
"1": { "sessions": [] },
|
|
"2": { "sessions": [] },
|
|
"3": { "sessions": [] },
|
|
"4": { "sessions": [] },
|
|
"5": { "sessions": [] },
|
|
"6": { "sessions": [] }
|
|
}
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
|
|
| `schedule` | object | ✅ | Keys `"0"` through `"6"` (day indices) |
|
|
| `schedule.{n}.sessions` | array | ✅ | Array of session config objects |
|
|
| `meta` | object | ❌ | Online-booking settings (see below) |
|
|
|
|
**Online-booking `meta` object:**
|
|
|
|
```json
|
|
{
|
|
"meta": {
|
|
"online_booking_enabled": true,
|
|
"booking_window_value": 2,
|
|
"booking_window_unit": "month",
|
|
"booking_mode": "service",
|
|
"buffer_minutes": 5
|
|
}
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `online_booking_enabled` | boolean | ❌ | `false` = no online booking; the public slot/month endpoints return no availability |
|
|
| `booking_window_value` | integer | ❌ | How far ahead patients may book (≥ 1) |
|
|
| `booking_window_unit` | string | ❌ | `"day"`, `"week"` or `"month"` (invalid value keeps the current one) |
|
|
| `booking_mode` | string | ❌ | `"slot"` (پیشفرض) = نوبتدهی اسلاتی با مدت ثابت (`duration_per_patient`). `"service"` = مدت هر نوبت از `duration_minutes` سرویسِ انتخابشده؛ زمانها با `GET /api/v1/appointment-service-slots` گرفته میشوند. مقدار نامعتبر نادیده گرفته میشود |
|
|
| `buffer_minutes` | integer | ❌ | فقط حالت سرویسی: فاصلهٔ بین نوبتها (دقیقه، ≥ 0). در `slot_end` ذخیره نمیشود؛ فقط فاصلهٔ بین زمانهای پیشنهادی |
|
|
|
|
> Defaults when `meta` is absent: `{ online_booking_enabled: true, booking_window_value: 3, booking_window_unit: "month", booking_mode: "slot", buffer_minutes: 0 }`. `meta` is stored inside the schedule `setting` JSON (no DB migration) and is **preserved** when only `schedule` is sent. `SlotCalculatorService` rejects any date in the past, beyond `today + value unit`, or when online booking is disabled — for the weekly schedule, date overrides, and `appointment-slots` alike.
|
|
>
|
|
> **اجبار حالت سرویسی:** اگر `booking_mode = service` ذخیره شود ولی پزشک هیچ سرویسِ «نمایش در نوبتدهی» (`bookable = true`) نداشته باشد، `POST`/`PATCH` برنامهٔ هفتگی با `422` (`ERR_VALIDATION_001`, field `booking_mode`) رد میشود.
|
|
>
|
|
> **غیرقابلتغییر پس از ثبت:** `booking_mode` فقط تا **اولین ثبت** قابلانتخاب است. پس از آنکه یکبار بهصورت صریح ذخیره شد (در `setting.meta.booking_mode` نوشته شد)، هر `POST`/`PATCH` که آن را تغییر دهد با `422` («نوع نوبتدهی پس از ثبت قابل تغییر نیست»، field `booking_mode`) رد میشود. پاسخِ `toArray` فیلد boolean `booking_mode_locked` را برمیگرداند (`true` = قفلشده) تا پنل توگل را غیرفعال کند. رکوردهای قدیمی که هنوز mode صریح ندارند، `booking_mode_locked=false` و یکبار قابلانتخاباند.
|
|
|
|
**Session Config Object:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `active` | boolean | ✅ | Whether this session is active |
|
|
| `location_id` | integer\|null | ❌ | Doctor address/location ID |
|
|
| `start_time` | string | ✅ | Session start `"HH:MM"` |
|
|
| `end_time` | string | ✅ | Session end `"HH:MM"` |
|
|
| `duration_per_patient` | integer | ✅ | Minutes per appointment slot |
|
|
| `has_rest` | boolean | ❌ | Whether to insert rest breaks |
|
|
| `rest_interval` | integer | ❌ | Work minutes before taking a rest break |
|
|
| `time_to_rest` | integer | ❌ | Duration of each rest break (minutes) |
|
|
| `patient_limit` | integer\|null | ❌ | Max patients per session (`null` = unlimited) |
|
|
|
|
> Multiple sessions per day are supported (e.g., morning + afternoon). Sessions are sorted by `start_time` and overlapping ones are skipped.
|
|
|
|
### Response `201`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"data": {
|
|
"uuid": "sched-uuid-...",
|
|
"doctor_uuid": "550e8400-...",
|
|
"schedule": {
|
|
"0": { "sessions": [ { "active": true, "start_time": "09:00", ... } ] },
|
|
"1": { "sessions": [] },
|
|
"2": { "sessions": [] },
|
|
"3": { "sessions": [] },
|
|
"4": { "sessions": [] },
|
|
"5": { "sessions": [] },
|
|
"6": { "sessions": [] }
|
|
},
|
|
"meta": {
|
|
"online_booking_enabled": true,
|
|
"booking_window_value": 3,
|
|
"booking_window_unit": "month",
|
|
"booking_mode": "slot",
|
|
"buffer_minutes": 0
|
|
},
|
|
"booking_mode_locked": true,
|
|
"created_at": 1717000000,
|
|
"updated_at": 1717000000
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
|
|
> This is an **upsert** — if a schedule already exists for the doctor, it is overwritten.
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_VALIDATION_002` | 404 | Doctor not found |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
|
|
---
|
|
|
|
### GET `/api/v1/appointment-settings/weekly-schedule/{uuid}`
|
|
|
|
Get weekly schedule. `{uuid}` can be either the **schedule UUID** or the **doctor UUID** — the controller tries both.
|
|
|
|
**Permission:** `AUTH` (class-level `IS_AUTHENTICATED_FULLY`)
|
|
|
|
### Response `200`
|
|
Same structure as POST response.
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_VALIDATION_002` | 404 | Schedule not found |
|
|
|
|
---
|
|
|
|
### PATCH `/api/v1/appointment-settings/weekly-schedule/{uuid}`
|
|
|
|
Update weekly schedule. `{uuid}` can be schedule UUID or doctor UUID.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Request Body
|
|
```json
|
|
{
|
|
"schedule": {
|
|
"0": {
|
|
"sessions": [
|
|
{
|
|
"active": true,
|
|
"location_id": 1973,
|
|
"start_time": "10:00",
|
|
"end_time": "14:00",
|
|
"duration_per_patient": 30,
|
|
"has_rest": false,
|
|
"rest_interval": 0,
|
|
"time_to_rest": 0,
|
|
"patient_limit": null
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
> Replaces the entire `schedule` object if provided.
|
|
|
|
### Response `200`
|
|
Updated schedule object (same structure as POST).
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
| `ERR_VALIDATION_002` | 404 | Schedule not found |
|
|
|
|
---
|
|
|
|
### DELETE `/api/v1/booking-setting/{uuid}`
|
|
|
|
Delete a weekly schedule.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
> Note: route is `/booking-setting/`, not `/appointment-settings/`
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "success": true, "data": { "message": "برنامه هفتگی با موفقیت حذف شد" } }
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
| `ERR_VALIDATION_002` | 404 | Schedule not found |
|
|
|
|
---
|
|
|
|
## Date Overrides
|
|
|
|
Override a specific date — mark it inactive (day off) or give it custom sessions.
|
|
|
|
### GET `/api/v1/appointment-settings/date-override/list/{doctorUuid}`
|
|
|
|
Get all date overrides for a doctor.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"data": [
|
|
{
|
|
"uuid": "...",
|
|
"doctor_uuid": "...",
|
|
"date": 1718476800,
|
|
"date_string": "2024-06-15",
|
|
"active": false,
|
|
"reason": "تعطیل خاص",
|
|
"custom_slots": [],
|
|
"created_at": 1717000000
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
> `date` is a Unix timestamp at **server-local midnight** (`Asia/Tehran`); `date_string` is the same day as `Y-m-d` and is what clients must render — converting the timestamp in a browser with `toISOString()` (UTC) shifts it one day back. `active: false` = entire day blocked. `active: true` with `custom_slots` = custom session schedule.
|
|
|
|
---
|
|
|
|
### POST `/api/v1/appointment-settings/date-override`
|
|
|
|
Create a date override.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Request Body
|
|
```json
|
|
{
|
|
"doctor_uuid": "550e8400-...",
|
|
"date": "2024-06-20",
|
|
"active": false,
|
|
"reason": "تعطیل رسمی",
|
|
"custom_slots": null
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
|
|
| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `"2024-06-20"`) |
|
|
| `active` | boolean | ❌ | `false` = full day off (default); `true` = use custom_slots |
|
|
| `reason` | string | ❌ | Reason for override |
|
|
| `custom_slots` | array\|null | ❌ | Custom sessions (same SessionConfig format as weekly schedule, see below) |
|
|
|
|
**`custom_slots` format when `active: true`:**
|
|
```json
|
|
{
|
|
"custom_slots": [
|
|
{
|
|
"start_time": "14:00",
|
|
"end_time": "18:00",
|
|
"duration_per_patient": 20,
|
|
"location_id": 1973,
|
|
"has_rest": false,
|
|
"rest_interval": 0,
|
|
"time_to_rest": 0,
|
|
"patient_limit": null
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Legacy format (backward compatible):**
|
|
```json
|
|
{
|
|
"custom_slots": [
|
|
{ "start": "14:00", "end": "18:00", "duration": 20 }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Response `201`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"data": {
|
|
"uuid": "override-uuid-...",
|
|
"doctor_uuid": "...",
|
|
"date": 1718476800,
|
|
"date_string": "2024-06-15",
|
|
"active": false,
|
|
"reason": "تعطیل رسمی",
|
|
"custom_slots": [],
|
|
"created_at": 1717000000
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
| `ERR_VALIDATION_002` | 404 | Doctor not found |
|
|
| `ERR_VALIDATION_001` | 422 | Invalid date format |
|
|
|
|
---
|
|
|
|
### GET `/api/v1/appointment-settings/date-override/{uuid}`
|
|
|
|
Get a single date override.
|
|
|
|
**Permission:** `AUTH` (class-level)
|
|
|
|
### Response `200`
|
|
Override object (same structure as above).
|
|
|
|
---
|
|
|
|
### PATCH `/api/v1/appointment-settings/date-override/{uuid}`
|
|
|
|
Update a date override.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Request Body (all optional)
|
|
```json
|
|
{
|
|
"active": true,
|
|
"reason": "جبران مرخصی",
|
|
"custom_slots": [
|
|
{
|
|
"start_time": "14:00",
|
|
"end_time": "18:00",
|
|
"duration_per_patient": 20,
|
|
"location_id": 1973,
|
|
"has_rest": false,
|
|
"rest_interval": 0,
|
|
"time_to_rest": 0,
|
|
"patient_limit": null
|
|
}
|
|
],
|
|
"date": "2024-06-21"
|
|
}
|
|
```
|
|
|
|
### Response `200`
|
|
Updated override object.
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
| `ERR_VALIDATION_002` | 404 | Override not found |
|
|
|
|
---
|
|
|
|
### DELETE `/api/v1/appointment-settings/date-override/{uuid}`
|
|
|
|
Delete a date override.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "success": true, "data": { "message": "Override با موفقیت حذف شد" } }
|
|
```
|
|
|
|
---
|
|
|
|
## Holidays
|
|
|
|
Mark a date range as holiday — all slots blocked, no overrides apply.
|
|
|
|
### GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}`
|
|
|
|
Get all holidays for a doctor.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"data": [
|
|
{
|
|
"uuid": "...",
|
|
"doctor_uuid": "...",
|
|
"start_date": 1719792000,
|
|
"end_date": 1720656000,
|
|
"start_date_string": "2024-07-01",
|
|
"end_date_string": "2024-07-11",
|
|
"reason": "تعطیلات تابستانی",
|
|
"active": true,
|
|
"created_at": 1717000000
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
> `start_date` / `end_date` are Unix timestamps at **server-local midnight** (`Asia/Tehran`); `start_date_string` / `end_date_string` carry the same days as `Y-m-d` and are what clients must render (UTC conversion in the browser shifts them one day back). Holidays take **highest priority** — they block the day even if a date override exists.
|
|
|
|
---
|
|
|
|
### POST `/api/v1/appointment-settings/holidays`
|
|
|
|
Create a holiday range.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Request Body
|
|
```json
|
|
{
|
|
"doctor_uuid": "550e8400-...",
|
|
"start_date": "2024-07-01",
|
|
"end_date": "2024-07-10",
|
|
"reason": "تعطیلات تابستانی"
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
|
|
| `start_date` | string | ✅ | Start date `Y-m-d` |
|
|
| `end_date` | string | ✅ | End date `Y-m-d` (must be ≥ start_date) |
|
|
| `reason` | string | ❌ | Holiday reason |
|
|
|
|
### Response `201`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"data": {
|
|
"uuid": "holiday-uuid-...",
|
|
"doctor_uuid": "...",
|
|
"start_date": 1719792000,
|
|
"end_date": 1720656000,
|
|
"start_date_string": "2024-07-01",
|
|
"end_date_string": "2024-07-11",
|
|
"reason": "تعطیلات تابستانی",
|
|
"active": true,
|
|
"created_at": 1717000000
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not the doctor owner |
|
|
| `ERR_VALIDATION_002` | 404 | Doctor not found |
|
|
| `ERR_VALIDATION_001` | 422 | end_date before start_date or invalid format |
|
|
|
|
---
|
|
|
|
### GET `/api/v1/appointment-settings/holidays/{uuid}`
|
|
|
|
Get a single holiday.
|
|
|
|
**Permission:** `AUTH` (class-level)
|
|
|
|
---
|
|
|
|
### PATCH `/api/v1/appointment-settings/holidays/{uuid}`
|
|
|
|
Update a holiday.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Request Body (all optional)
|
|
```json
|
|
{
|
|
"start_date": "2024-07-02",
|
|
"end_date": "2024-07-12",
|
|
"reason": "تمدید تعطیلات",
|
|
"active": false
|
|
}
|
|
```
|
|
|
|
### Response `200`
|
|
Updated holiday object.
|
|
|
|
---
|
|
|
|
### DELETE `/api/v1/appointment-settings/holidays/{uuid}`
|
|
|
|
Delete a holiday.
|
|
|
|
**Permission:** `AUTH` — see [Access rule](#access-rule)
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "success": true, "data": { "message": "تعطیلات حذف شد" } }
|
|
```
|
|
|
|
---
|
|
|
|
## Slot Calculation Logic (Reference)
|
|
|
|
The `SlotCalculatorService` calculates available slots in this priority order:
|
|
|
|
1. **Holiday** — if date falls in a holiday range → return empty (no slots)
|
|
2. **Date Override** — if a date override exists for this date:
|
|
- `active: false` → return empty
|
|
- `active: true` → use `custom_slots` sessions
|
|
3. **Weekly Schedule** — use the day's `sessions` array (only `active: true` sessions, sorted by `start_time`, overlapping sessions skipped)
|
|
|
|
**Slot output format:**
|
|
```json
|
|
[
|
|
{
|
|
"start": 1718438400,
|
|
"end": 1718439600,
|
|
"start_time": "09:00",
|
|
"end_time": "09:20",
|
|
"location_id": 1973
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Available Locations
|
|
|
|
### `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`
|
|
|
|
**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.
|
|
|
|
#### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"id": "5",
|
|
"uuid": "...",
|
|
"type": "personal",
|
|
"clinic_id": null,
|
|
"clinic_name": null,
|
|
"name": "مطب شخصی",
|
|
"address": "تهران، ...",
|
|
"telephone": "09121234567",
|
|
"map": { "latitude": "35.7", "longitude": "51.4" },
|
|
"city": { "id": "1", "name": "تهران" },
|
|
"province": { "id": "8", "name": "تهران" }
|
|
},
|
|
{
|
|
"id": "12",
|
|
"uuid": "...",
|
|
"type": "clinic",
|
|
"clinic_id": "3",
|
|
"clinic_name": "کلینیک نور",
|
|
"name": "شعبه مرکزی",
|
|
"address": "تهران، خیابان ولیعصر...",
|
|
"telephone": "02112345678",
|
|
"map": { "latitude": "35.699", "longitude": "51.337" },
|
|
"city": { "id": "1", "name": "تهران" },
|
|
"province": { "id": "8", "name": "تهران" }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
| Field | Description |
|
|
|-------|-------------|
|
|
| `type` | `personal` = doctor's own address; `clinic` = clinic address |
|
|
| `clinic_id` | ID of the clinic (only for type=clinic) |
|
|
| `clinic_name` | Name of the clinic (only for type=clinic) |
|
|
|
|
> Use `id` as the `location_id` value in weekly schedule sessions or date override sessions.
|
|
|
|
#### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_VALIDATION_002` | 404 | Doctor not found |
|
|
|
|
|
|
---
|
|
|
|
## Context additions (2026-07)
|
|
|
|
### Response fields
|
|
|
|
`WeeklySchedule.toArray()` now also returns:
|
|
|
|
| Field | Type | Meaning |
|
|
|---|---|---|
|
|
| `clinic_uuid` | `string\|null` | the clinic this schedule belongs to; `null` = personal practice |
|
|
| `context` | `"personal" \| "clinic"` | convenience mirror of the above |
|
|
|
|
`DateOverride.toArray()` returns the same two fields. `Holiday.toArray()` returns `clinic_uuid`
|
|
plus `scope` (`"global" | "clinic"`), and the list endpoint adds `editable` (see below).
|
|
|
|
### Holidays are global by default
|
|
|
|
A holiday means "the doctor is not there", which is a physical fact — so unlike schedules and date
|
|
overrides it is **not** per-context by default:
|
|
|
|
| `clinic_id` | Meaning |
|
|
|---|---|
|
|
| `NULL` | the doctor is absent **everywhere** — applies to the personal practice and every clinic |
|
|
| set | the doctor is absent in that clinic only |
|
|
|
|
`GET /holidays/list/{doctorUuid}?clinic_uuid=…` returns the **union**: the clinic's own holidays plus
|
|
the doctor's global ones. Global rows come back with `editable: false` — a clinic must see that the
|
|
doctor is away but may not delete that fact.
|
|
|
|
`POST /holidays` **without** `clinic_uuid` creates a global holiday and is restricted to the doctor
|
|
themselves and admins → otherwise `403 ERR_ACCESS_DENIED` («کلینیک فقط میتواند تعطیلی مخصوص خودش را
|
|
ثبت کند»). A clinic closing for *all* its doctors is not expressible in this model and needs a
|
|
separate `ClinicHoliday` entity — **not implemented**.
|
|
|
|
### Date overrides are always per-context
|
|
|
|
`GET /date-override/list/{doctorUuid}?clinic_uuid=…` returns only that context's overrides — no
|
|
union, because an override changes working hours and working hours are themselves per-context.
|
|
|
|
### `GET /available-locations/{doctorUuid}`
|
|
|
|
Now takes `?clinic_uuid=`. Without it, only the doctor's `personal` addresses are returned; with it,
|
|
only that clinic's addresses. The two sets are never merged (they used to be).
|