Files
clinicpro/docs/api/appointment-settings.md
T
hamedandClaude Opus 5 a3fc7f5c8b feat(appointment): let a platform admin change a doctor's locked booking mode
The booking mode locks after the first save because existing appointments were
computed under that mode's rules. A lock with no key, though, traps a practice
that picked the wrong mode on day one, so ROLE_ADMIN can now open it.

The first attempt is still refused when the doctor has active appointments in
the next year, and says how many; the admin repeats the request with
force_mode_change to confirm they know what happens to those. The flag does
nothing for anyone else. GET now returns booking_mode_changeable so the panel
enables the toggle from the server's answer rather than guessing from the role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:32:52 +03:30

28 KiB

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

Breaking change (2026-08): a doctor holds exactly ONE weekly schedule, no matter how many clinics they work at. What varies from day to day is the place: Saturday at the personal practice, Monday at the clinic. The context of a shift is read from the location_id on that shift, not from the record it lives in. Consequences:

  • clinic_uuid no longer selects which record is read or written — every context reads the same one.
  • It still selects which addresses the caller may assign, and which days a booking context sees: a personal-practice secretary never sees the clinic days and cannot book on them, and vice versa.
  • A shift whose address belongs to another context is returned to the caller for display but is preserved verbatim on save — a clinic manager can neither edit nor delete the doctor's personal shifts.
  • booking_mode and the rest of meta are now doctor-wide, because there is one record. The existing "mode is locked after the first save" rule therefore applies across contexts.
  • Date overrides and holidays are unchanged and remain per-context.

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.mdClinic 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 exactly one weekly schedule (upsert keyed by doctor_id; the legacy clinic_id column stays NULL on new rows). Rows created before 2026-08 were merged by migrations/Version20260820120000.php, which appended each clinic record's sessions into the doctor's single record — the location_id already on every shift carries the context. 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

الزام آدرس: هر session با active=true باید location_id (آدرس مطب/کلینیک) داشته باشد. در غیر این صورت 422 ERR_VALIDATION_001 («برای هر شیفت فعال باید آدرس انتخاب شود»). این آدرس هنگام رزرو خودکار روی نوبت ذخیره می‌شود.

الزام مکان: آدرس هر شیفت باید در فهرست available-locations همان درخواست‌کننده باشد. پزشک هر دو محیط خودش را دارد؛ کلینیک فقط آدرس خودش. آدرس بیرون از این فهرست → 422 ERR_VALIDATION_001 («آدرس انتخاب‌شده متعلق به این کلینیک نیست»).

ادغام هنگام ذخیره: شیفت‌هایی که آدرسشان بیرون از دسترس درخواست‌کننده است، از نسخهٔ ذخیره‌شده دست‌نخورده برمی‌گردند؛ ورودی نه می‌تواند حذفشان کند نه عوضشان.

تغییر نوع نوبت‌دهی: نوع پس از اولین ثبت قفل می‌شود، چون نوبت‌های ثبت‌شده با قواعد همان نوع محاسبه شده‌اند. فقط ROLE_ADMIN می‌تواند بازش کند:

  • غیر ادمین → 422 ERR_VALIDATION_001 روی booking_mode («نوع نوبت‌دهی پس از ثبت قابل تغییر نیست»).
  • ادمین، وقتی نوبت فعالی در ۳۶۵ روز آینده هست → 422 با تعداد آن نوبت‌ها. درخواست دوم با force_mode_change: true انجام می‌شود.
  • ادمین، بدون نوبت فعال آینده → بدون نیاز به force_mode_change انجام می‌شود.

force_mode_change از سمت غیر ادمین بی‌اثر است.

نوبت‌دهی سرویسی: با meta.booking_mode = "service" صاحبِ همان context باید حداقل یک سرویس با bookable = true داشته باشد؛ وگرنه 422 ERR_VALIDATION_001 روی فیلد booking_mode. پیام در محیط کلینیک به کلینیک اشاره می‌کند.

Request Body (application/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:

{
  "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

{
  "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

Request Body

{
  "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

Note: route is /booking-setting/, not /appointment-settings/

Response 200

{ "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 (403 ERR_AUTH_006 otherwise).

Response 200

{
  "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

Request Body

{
  "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:

{
  "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):

{
  "custom_slots": [
    { "start": "14:00", "end": "18:00", "duration": 20 }
  ]
}

Response 201

{
  "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

Request Body (all optional)

{
  "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

Response 200

{ "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 (403 ERR_AUTH_006 otherwise).

Response 200

{
  "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

Request Body

{
  "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

{
  "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

Request Body (all optional)

{
  "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

Response 200

{ "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:

[
  {
    "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 (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

{
  "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 legacy, always null on a weekly schedule — the record is no longer owned by one context
context "personal" | "clinic" legacy, always "personal" for the same reason
locations array every address of this doctor (personal + each clinic they belong to), for labelling shifts the caller may not edit — GET only
selectable_location_ids int[] the subset of those the caller may assign — GET only
booking_mode_locked bool a mode has already been stored
booking_mode_changeable bool this caller may unlock it — ROLE_ADMIN only, GET only

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}

Returns the addresses the caller may assign, which is not the same as the addresses of the current context:

Caller Returned
the doctor themselves, or ROLE_ADMIN every address of theirs — personal and each clinic they belong to
anyone else (clinic manager, secretary) only the addresses of the context in ?clinic_uuid=

The doctor gets the full set because their schedule is a single one and they move between places from day to day; a clinic manager gets only its own so it cannot move a shift into the doctor's private practice.


حالت سوم: نوبت‌دهی منبع‌محور

meta.booking_mode مقدار سوم resource را هم می‌پذیرد (تسک ۰۶). در این حالت نوبت به بخش‌ها تقسیم می‌شود و وقت آزاد از تقاطع تقویم منابع می‌آید.

فیلد meta معنی
booking_mode: "resource" حالت منبع‌محور
step_minutes گام جستجوی وقت؛ پیش‌فرض ۱۵، کمینه ۵

شرط آمادگی

انتخاب این حالت برگشت‌ناپذیر است، پس پیش از ثبت سنجیده می‌شود: محیط باید حداقل یک منبع فعال داشته باشد.

{
  "success": false,
  "data": null,
  "errors": [{
    "code": "ERR_VALIDATION_001",
    "message": "برای نوبت‌دهی منبع‌محور حداقل یک منبع فعال لازم است؛ اول اتاق، اپراتور یا دستگاه تعریف کنید",
    "field": "booking_mode"
  }]
}

بدون این نگهبان، کلینیک حالتی را برای همیشه قفل می‌کرد که هیچ وقتی در آن محاسبه نمی‌شود.

پنل همین شرط‌ها را پیش از ثبت به‌صورت ✓/✗ نشان می‌دهد تا کاربر به ۴۲۲ نخورد.

has_segments روی فهرست سرویس‌ها

GET /api/v1/service-items حالا فیلد has_segments هم می‌دهد — با یک کوئری تجمعی برای کل فهرست، نه یکی per سرویس. تنظیمات نوبت‌دهی از همین می‌فهمد آمادگیِ حالت منبع‌محور هست یا نه.

جزئیات بخش‌ها: appointment-plan.md · منابع: resource.md