The appointments page only ever showed one doctor's row, but in the resource-first model a single appointment can hold a room and a device at the same time, and that — not the doctor's schedule — is what runs the capacity out. An hour could look free on the doctor's lane while the only alexandrite laser was already taken. A third view, "منابع", draws one lane per resource for the selected day. Blocks come from resource_occupancy rather than the appointment: that range includes the device's setup and cleanup minutes and is the same range the availability engine treats as busy. A multi-segment appointment therefore shows up on every resource it holds, and each block links to the appointment it belongs to. GET /api/v1/resources/timeline keeps a fixed query count — one for occupancy, one for shifts, one for the patient names — instead of one per resource. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
26 KiB
Resource API — منابع، نوع منبع، مهارت و استخر
Base:
/api/v1· Auth: JWT روی همهٔ اندپوینتها مجوز:appointment_settings(viewخواندن،updateنوشتن) — همان مجوز تنظیمات نوبتدهی؛ مجوز تازهای ساخته نشده.
منبع چیست
قانون طلایی اول مستند: «تقویم مال منبع است، نه مال پزشک.» منبع هر چیزی است که ممکن است اشغال باشد: پزشک، اپراتور، دستیار، دستگاه، اتاق، تخت، یونیت.
هر منبع مال یک شعبه است و شعبه همان رکورد آدرس محل نوبتدهی است
(doctor_addresses — رجوع به branch.md). پس همهجا address_uuid است،
نه branch_id.
پل، نه ادغام
Doctor، ClinicStaff و Room هرکدام هویت مستقل و مصرفکنندهٔ زنده دارند
(appointments.doctor_id، service_item_staff، سایت عمومی). تبدیلشان به زیرکلاسِ منبع
یعنی مهاجرت همزمان همهٔ آن مسیرها. بهجایش هر منبع حداکثر یک پل دارد:
subject_kind |
یعنی |
|---|---|
doctor / staff / room |
منبع همان موجودیت است |
null |
دستگاه یا تجهیزات — منبعی که پشتش موجودیت دیگری نیست |
قید «حداکثر یکی» در خودِ entity اجبار میشود، نه با CHECK دیتابیس: MariaDB قید
چندستونی را قابل اتکا اجرا نمیکند.
یکتایی (doctor_id, address_id) است، نه (doctor_id). یک WeeklySchedule per
جفت (پزشک، کلینیک) است ولی هر شیفتِ درونش location_id خودش را دارد، پس یک پزشک از
قبل در چند آدرسِ یک محیط کار میکند. کلید زدن فقط روی پزشک، این واقعیت را غیرقابلبیان
میکرد — و تقویمِ تسک ۰۳ دقیقاً per مکان است.
سه قرارداد که باید بدانید
۱. capacity یعنی همزمانی، نه تعداد ردیف.
اتاق تزریق سهتخته یک منبع با ظرفیت ۳ است، نه سه منبع. با سه ردیف، موتور جستجو
باید سه تقویم را ادغام کند و «کدام تخت» بشود تصمیمی که هیچکس نمیخواهد بگیرد. با
ظرفیت ۳، شرط اشغال یک شمارش ساده در برابر یک سقف است.
منبعی که یک شخص است (پزشک/پرسنل) ظرفیت بیش از ۱ نمیپذیرد.
۲. setup_minutes / cleanup_minutes جزو نوبت بیمار نیستند.
بیمار ساعت ۱۰:۰۰ میآید و ۱۰:۳۰ میرود؛ ولی یونیت از ۹:۵۵ تا ۱۰:۴۰ در دسترس نیست.
با WeeklySchedule.meta.buffer_minutes قاطی نشود: آن فاصلهٔ سراسری بین دو نوبتِ
پزشک است، این per منبع. هر دو کنار هم زندگی میکنند و آن یکی دستنخورده است.
۳. مهارت یک جدول است، نه یک قانون.
«کدام اپراتور مجاز است با کدام دستگاه کار کند» یک اطلاعات است. با ۵۰ اپراتور و ۲۰۰
سرویس، سپردنش به موتور قوانین یعنی ۱۰٬۰۰۰ قانون.
با ClinicStaff.job_title قاطی نشود: آن متن آزاد و فقط برای نمایش است و هیچجا برای
تصمیمگیری parse نمیشود.
⚠️ همهٔ اندپوینتها برای دادهٔ محیط دیگر ۴۰۴ میدهند، نه ۴۰۳ — وجود دادهٔ محیط
بیگانه لو نمیرود. مالکیت صریح سنجیده میشود و به TenantFilter تکیه نمیشود، چون
جداسازی سختِ فیلتر فقط روی محیطِ انتخابشده اعمال میشود
(tenancy.md).
نوع منبع
GET /api/v1/resource-types
خروجی واقعی (سه نوع سیستمی را app:resource:backfill ساخته):
{
"success": true,
"data": [
{ "uuid": "c39053f3-a051-4f03-942f-08a2274bf658", "code": "room", "name": "اتاق", "is_system": true, "active": true, "created_at": 1785420038, "updated_at": 1785420038, "resources_count": 0 },
{ "uuid": "c0447da1-30af-430b-8cbd-b464ccd81624", "code": "staff", "name": "پرسنل", "is_system": true, "active": true, "created_at": 1785420038, "updated_at": 1785420038, "resources_count": 0 },
{ "uuid": "dcabd1c3-d554-4613-a33d-7c69b7ae7efe", "code": "doctor", "name": "پزشک", "is_system": true, "active": true, "created_at": 1785420038, "updated_at": 1785420038, "resources_count": 1 }
]
}
resources_count با یک کوئری گروهی پر میشود، نه یکی per نوع.
POST /api/v1/resource-types
| فیلد | نوع | الزامی | قاعده |
|---|---|---|---|
code |
string | ✅ | [a-z0-9_]{1,40} · یکتا per محیط (همان کد در محیط دیگر مجاز است) |
name |
string | ✅ | نام نمایشی فارسی |
۲۰۱ (خروجی واقعی):
{
"success": true,
"data": {
"uuid": "da4d6789-1167-4acb-beac-0ea13c00a37e",
"code": "laser",
"name": "دستگاه لیزر",
"is_system": false,
"active": true,
"created_at": 1785420676,
"updated_at": 1785420676,
"resources_count": 0
}
}
۴۲۲: کد تکراری در همان محیط (field code) · کد نامعتبر (field code) · نام خالی.
PATCH /api/v1/resource-type/{uuid}
فقط name و active. code تغییر نمیکند حتی روی نوع غیرسیستمی: منابع موجود و
پل خودکار با همان کد پیدا میشوند و عوض کردنش نگاشت را بیصدا میشکند. فرستادنش خطا
نمیدهد، نادیده گرفته میشود.
DELETE /api/v1/resource-type/{uuid}
۴۲۲ وقتی is_system است («نوع منبع سیستمی حذف نمیشود») یا منبعی از آن نوع وجود
دارد («این نوع روی N منبع استفاده شده است»).
مهارت
GET /api/v1/skills · POST /api/v1/skills
بدنهٔ ساخت فقط name. ۲۰۱ (خروجی واقعی):
{
"success": true,
"data": {
"uuid": "48e60355-6788-4202-a775-a07986658662",
"name": "لیزر آلکساندرایت",
"active": true,
"created_at": 1785420692,
"updated_at": 1785420692,
"resources_count": 0
}
}
PATCH /api/v1/skill/{uuid} — name و active
DELETE /api/v1/skill/{uuid}
مهارتی که روی منبعی نشسته حذف نمیشود؛ وگرنه ON DELETE RESTRICT خطای خام دیتابیس
میداد. ۴۲۲ (خروجی واقعی):
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"این مهارت به 1 منبع داده شده است؛ اول از آنها برداشته شود"}]}
رقم لاتین در پیام عمدی است — قرارداد پیامهای درونریزیِ بکاند همین است و قالببندی فارسی کارِ نمایش در کلاینت است.
منابع
GET /api/v1/resources
| Query | توضیح |
|---|---|
address_uuid |
فقط منابع این شعبه |
type_uuid |
فقط این نوع |
skill_uuid |
فقط منابعی که این مهارت را دارند |
active |
1 / 0 |
clinic_uuid |
انتخاب صریح محیط |
skill_uuid متعلق به محیط دیگر ۴۰۴ میدهد، نه «هیچ نتیجه» — سکوت اینجا یعنی
دیباگِ کور.
POST /api/v1/resource
| فیلد | نوع | الزامی | قاعده |
|---|---|---|---|
address_uuid |
string | ✅ | شعبه؛ جفت محیطِ منبع از همین مشتق میشود، نه از بدنه |
type_uuid |
string | ✅ | |
name |
string | ✅ | حداکثر ۱۵۰ نویسه |
capacity |
int | — | پیشفرض ۱، حداقل ۱؛ روی منبعِ شخص حداکثر ۱ |
setup_minutes |
int | — | پیشفرض ۰، بازهٔ ۰..۴۸۰ |
cleanup_minutes |
int | — | پیشفرض ۰، بازهٔ ۰..۴۸۰ |
attributes |
object | — | حداکثر ۲۰ کلید · کلید [a-z_]{1,40} · مقدار فقط اسکالر |
active |
bool | — | پیشفرض true |
attributes عمداً آزاد است — کلید ناشناخته پذیرفته میشود — ولی مقدارش باید ساده
باشد. دلیل: تسک ۰۵ قید same_gender و تسک ۰۹ شرطهای منبع را با مقایسهٔ ساده روی
همین مقادیر میسنجند؛ آرایهٔ تودرتو یعنی مقایسهٔ دلخواه، همان چیزی که بند ۸ مستند
ممنوع کرده. کلیدهای قراردادی: gender, device_model, floor, brand.
۲۰۱ (خروجی واقعی):
{
"success": true,
"data": {
"uuid": "38bdd1e9-d982-4cef-9419-3d42ee6b85f2",
"name": "لیزر آلکساندرایت ۱",
"address_uuid": "d0601f79-6e4a-482e-afed-c9be3928d9e6",
"address_name": "درمانگاه شبانه روزی صدرا ",
"type_uuid": "da4d6789-1167-4acb-beac-0ea13c00a37e",
"type_code": "laser",
"type_name": "دستگاه لیزر",
"capacity": 1,
"setup_minutes": 5,
"cleanup_minutes": 10,
"attributes": { "device_model": "Candela GentleLase", "floor": "2" },
"subject_kind": null,
"subject_uuid": null,
"skills": [],
"active": true,
"created_at": 1785420692,
"updated_at": 1785420692
}
}
۴۲۲ — ظرفیت روی منبعِ شخص (خروجی واقعی):
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"منبعی که یک شخص است نمیتواند ظرفیت بیش از ۱ داشته باشد","field":"capacity"}]}
۴۲۲ — ویژگی غیر اسکالر (خروجی واقعی):
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"مقدار ویژگی «nested» باید یک مقدار ساده باشد","field":"attributes"}]}
سایر ۴۲۲ها: capacity < 1 · نام خالی · کلید ویژگی غیر snake_case ·
setup/cleanup بیرون از ۰..۴۸۰.
GET/PATCH/DELETE /api/v1/resource/{uuid}
PATCH همان فیلدهاست؛ address_uuid پذیرفته نمیشود (جفت محیط از آدرس مشتق شده
و write-once است) و type_uuid قابل تغییر است.
GET /api/v1/resource/{uuid}/services
سرویسهایی که این منبع ارائه میدهد، با مقدار مؤثر و اینکه هر عدد از کدام سطح آمده.
duration_minutes/price_rials مقدارِ ثبتشده روی همین رابطهاند و null یعنی «ارث از
سطح بالاتر»، نه صفر. effective_* نتیجهٔ زنجیرهٔ حل است و *_source میگوید کدام سطح
برنده شده — بدون آن، پنل نمیتواند کنار خانهٔ خالی بنویسد عدد از کجا میآید.
زنجیره از خاص به عام: resource_option → resource_service → branch → service_default.
خروجی واقعی (۲۰۰):
[
{
"service_uuid": "f49baba9-68e9-4d61-aa90-fc8c784607e0",
"service_name": "لیزر CO2",
"duration_minutes": null,
"price_rials": null,
"active": true,
"effective_duration_minutes": 40,
"effective_price_rials": 18000000,
"duration_source": "service_default",
"price_source": "branch"
},
{
"service_uuid": "7f13ab0d-2f64-4e8c-8e12-154172b6620a",
"service_name": "ویزیت عمومی",
"duration_minutes": null,
"price_rials": 1200000,
"active": true,
"effective_duration_minutes": 15,
"effective_price_rials": 1200000,
"duration_source": "service_default",
"price_source": "resource_option"
}
]
دسترسی: appointment_settings.view. ۴۰۴: منبع محیط دیگر.
PUT /api/v1/resource/{uuid}/services
جایگزینی کامل، مثل مهارتها: سرویسی که در بدنه نیست از این منبع برداشته میشود و
{"services":[]} همه را پاک میکند.
{
"services": [
{ "service_uuid": "f49baba9-…", "duration_minutes": 15, "price_rials": 9500000, "active": true },
{ "service_uuid": "7f13ab0d-…", "duration_minutes": "", "price_rials": null }
]
}
| فیلد | نوع | الزامی | توضیح |
|---|---|---|---|
service_uuid |
string (UUID) | ✅ | سرویس یا گزینهٔ سرویس؛ هر دو ServiceItem اند |
duration_minutes |
int | null | "" |
❌ | مدت اختصاصی این منبع. null و رشتهٔ خالی یعنی ارث، نه صفر. مقدار ≤ ۰ ⇒ 422 |
price_rials |
int | null | "" |
❌ | همان قاعده؛ منفی ⇒ 422. صفرِ صریح یعنی رایگان و ارث نمیگیرد |
active |
boolean | ❌ | پیشفرض true. غیرفعال یعنی «فعلاً این را نمیدهد» ولی اعداد ذخیرهشده میمانند |
پاسخ ۲۰۰ همان فهرست GET است (با مقادیر تازه حلشده).
۴۲۲: services غایب یا غیرآرایه · service_uuid غایب یا ناموجود · سرویسِ محیط دیگر
(«سرویس انتخابشده به این محل نوبتدهی تعلق ندارد») · مدت یا قیمت نامعتبر.
این رابطه روی انتخاب منبع اثر میگذارد. وقتی برای یک سرویس دستکم یک ردیف ثبت شده باشد،
appointment-availabilityفقط منابعی از همان نوع را کاندید میکند که ردیف فعال دارند. تا وقتی هیچ ردیفی نیست، هیچ فیلتری اعمال نمیشود — محیطی که هنوز رابطهها را پر نکرده نباید یکشبه بیوقت شود.
PUT /api/v1/resource/{uuid}/skills
جایگزینی کامل: مهارتی که در بدنه نیست، برداشته میشود. {"skills":[]} همه را
پاک میکند.
{ "skills": [ { "skill_uuid": "48e60355-…", "level": 4 } ] }
level بین ۱ و ۵ (پیشفرض ۱). از روز اول هست چون استراتژی «حفظ متخصصها» در تسک ۰۶
رویش ساخته میشود و افزودنش بعداً یعنی backfill با حدس.
پاسخ ۲۰۰ کلِ منبع است؛ بخش skills آن (خروجی واقعی):
"skills": [
{ "skill_uuid": "48e60355-6788-4202-a775-a07986658662", "skill_name": "لیزر آلکساندرایت", "level": 4 }
]
۴۲۲: level بیرون از ۱..۵ (field level) · مهارت تکراری در یک بدنه ·
skill_uuid غایب. ۴۰۴: مهارت محیط دیگر.
اتمی است. اعتبارسنجی کل فهرست پیش از هر حذفی انجام میشود، پس یک ردیف نامعتبر در انتهای فهرست، مهارتهای درستِ قبلی را پاک نمیکند و بعد ۴۲۲ برگرداند.
GET /api/v1/resources/timeline
مجوز: appointment_settings.view. پشتِ نمای منابع در صفحهٔ نوبتها
(/admin/appointments) است.
| پارامتر | پیشفرض | توضیح |
|---|---|---|
date |
امروز | YYYY-MM-DD — هر قالب دیگری ۴۲۲ |
address_uuid |
همهٔ شعبهها | فقط منابع همان شعبه |
فقط منابع فعال برمیگردند و منبعِ بیشیفت هم در فهرست میماند تا ردیفش در تایملاین دیده شود.
بازهها از resource_occupancy میآیند نه از خودِ نوبت: بازهٔ اشغال، آمادهسازی و
تمیزکاری منبع را هم در بر دارد و همان بازهای است که موتور جستجو اشغال میبیند. ردیف
released نمیآید؛ آن تاریخچه است. یک نوبتِ چندبخشی روی چند منبع، چند ردیف دارد —
همان چیزی که نمای پزشکمحور نشان نمیدهد.
خروجی واقعی (سناریوی ۲، ۲۰۲۶-۰۸-۰۵):
{
"success": true,
"data": {
"date": 1785875400, // نیمهشب همان روز
"day_of_week": 4, // ۰ = شنبه
"resources": [
{
"uuid": "…", "name": "اتاق لیزر ۱", "type_name": "اتاق درمان",
"address_name": "درمانگاه سلامت", "capacity": 1,
"shifts": [{ "start_minute": 480, "end_minute": 1260 }],
"items": [
{
"uuid": "6176e73b-df27-4cdf-815c-36f9bfbd68ca",
"starts_at": 1785931200, "ends_at": 1785931500,
"status": "booked", "segment_name": "بیحسی موضعی",
"appointment_id": 47, "patient_name": "زهرا احمدی",
"appointment_uuid": "17086c41-6cc4-4539-bb5b-4c94e8f373c6",
"appointment_status": "pending"
}
]
}
]
}
}
shifts فقط شیفتهای همان روزِ هفته است. ۴۲۲: قالب date غلط
({"code":"ERR_VALIDATION_002","message":"تاریخ باید به شکل YYYY-MM-DD باشد","field":"date"}).
تعداد کوئری ثابت است: یک کوئری اشغال، یک کوئری شیفت، یک کوئری نوبت — نه یکی بهازای هر منبع.
PUT /api/v1/resource/{uuid}/categories
مجوز: appointment_settings.update.
دستهٔ منبع از کاتالوگ سراسری انتخاب میشود (CatalogCategory) — همان دستههایی که سرویس
هم از آنها استفاده میکند. ساخت دسته اینجا ممکن نیست؛ فقط در «تنظیمات ← دستهبندیها»
(clinic-services.md).
جایگزینی کامل، مثل skills: {"category_uuids":[]} همه را پاک میکند.
{ "category_uuids": ["8ae755b5-5f27-404e-9b63-1b29693a9039"] }
پاسخ ۲۰۰ کلِ منبع است؛ بخش categories آن (خروجی واقعی):
{
"success": true,
"data": {
"uuid": "ce070910-7038-4f50-9f7a-1b35ec1a67f7",
"name": "اتاق ۱",
"categories": [
{ "uuid": "8ae755b5-5f27-404e-9b63-1b29693a9039", "name": "دست (doc)" }
]
}
}
۴۲۲: نبودِ category_uuids ({"code":"ERR_VALIDATION_002","message":"فیلد category_uuids الزامی است","field":"category_uuids"})
· دستهٔ محیط دیگر. uuid از بدنهٔ درخواست میآید و TenantFilter رویش اعمال نمیشود، پس محیطِ
هر دسته صریحاً با محیط منبع مقایسه میشود.
استخر منابع
گروهی از منابع که جایگزین کامل یکدیگرند: «لیزرهای آلکساندرایت»، «اتاقهای معاینه».
GET/POST /api/v1/resource-pools
بدنهٔ ساخت: address_uuid · type_uuid · name.
GET/PATCH/DELETE /api/v1/resource-pool/{uuid}
PATCH فقط name و active. DELETE اعضا را با CASCADE میبرد ولی خودِ منابع
دستنخورده میمانند.
PUT /api/v1/resource-pool/{uuid}/members
{ "members": [ { "resource_uuid": "38bdd1e9-…", "priority": 0 } ] }
هر عضو باید همان شعبه و همان نوعِ استخر را داشته باشد. تسک ۰۶ فرض میکند هر عضو جایگزین کامل دیگری است: عضوی از شعبهٔ دیگر یعنی بیمار در ساختمان اشتباه میایستد، و عضوی از نوع دیگر یعنی صندلی بهجای دستگاه لیزر پیشنهاد میشود.
priority ترتیب ترجیح در استراتژی انتخاب است؛ کوچکتر زودتر.
۲۰۰ (خروجی واقعی):
{
"success": true,
"data": {
"uuid": "9d50cf4a-bef6-423c-85e9-530bf3609964",
"name": "لیزرهای آلکساندرایت",
"address_uuid": "d0601f79-6e4a-482e-afed-c9be3928d9e6",
"address_name": "درمانگاه شبانه روزی صدرا ",
"type_uuid": "da4d6789-1167-4acb-beac-0ea13c00a37e",
"type_code": "laser",
"type_name": "دستگاه لیزر",
"members": [
{ "resource_uuid": "38bdd1e9-d982-4cef-9419-3d42ee6b85f2", "resource_name": "لیزر آلکساندرایت ۱", "priority": 0, "active": true }
],
"active": true,
"created_at": 1785420708,
"updated_at": 1785420708
}
}
۴۲۲: «همهٔ اعضای استخر باید در یک شعبه باشند» · «… از یک نوع منبع باشند» · عضو تکراری. مثل مهارتها، اعتبارسنجی پیش از حذف است.
استخر بدون عضو معتبر است (در حال ساخت)، ولی تسک ۰۶ آن را «هیچ منبعی» میبیند.
app:resource:backfill
هر موجودیت قابلاشغالِ موجود را به یک منبع پل میزند.
ddev exec php bin/console app:resource:backfill # dry-run
ddev exec php bin/console app:resource:backfill --force
ddev exec php bin/console app:resource:backfill --force --pair=clinic:12
| مورد | رفتار |
|---|---|
| اتاق | آدرسش را خودش دارد → منبع با همان capacity و همان active |
| پزشک | یک منبع per آدرسی که در برنامهٔ هفتگی شیفت فعال دارد (location_id) |
| پرسنل | فقط اگر محیط دقیقاً یک شعبه دارد؛ وگرنه رد و گزارش میشود |
پرسنل تنها موردی است که قابل استنتاج نیست: هیچ ستونی نمیگوید در کدام شعبه کار میکند. حدس زدنِ «اولین شعبه» او را در ساختمان اشتباه مینشاند، پس گزارش میشود تا کاربر خودش تعیین کند.
--pair هم برای عملیات است (اجرای دوباره برای یک کلینیک) و هم دامنه را محدود میکند.
دستور per محیط flush میکند، پس یک ردیف خراب کل اجرای چندهزارمحیطی را با
EntityManagerClosed از پا نمیاندازد.
idempotent است: تکیهگاهش وجود یا نبودِ منبعِ متناظر است، نه یک پرچم جداگانه.
طبقهبندی محیط
| جدول | وضعیت |
|---|---|
resource_types · clinic_resources · skills · resource_pools |
جفت (entity_type, entity_id) |
resource_skills · resource_pool_members |
AGGREGATE_CHILDREN — ریشههاشان خودشان جفت دارند |
تستها
ddev exec php bin/phpunit tests/Resource # ۵۲ تست / ۱۲۹ assertion
ddev exec php vendor/bin/phpstan analyse src/Resource
npx vitest run assets/admin/pages/ResourcesPage.test.tsx
مسدودسازی موردی
«این بعدازظهر دستگاه سرویس دارد» — یک بازهٔ مشخص که منبع در دسترس نیست.
| مسدودسازی موردی | استثنای تقویم | |
|---|---|---|
| چیست | یک بازهٔ مشخص | تغییر الگوی تکرارشوندهٔ کاری |
| کجا | resource_occupancy |
resource_exceptions |
| چقدر میماند | تا وقتی حذفش کنی | بخشی از تعریف تقویم |
ادغامشان یعنی یا تعطیلی یک بعدازظهر برای همیشه در تقویم بماند، یا تغییر ساعت کاری با یک کلیک ناپدید شود.
GET /api/v1/resource/{uuid}/blocks
| Query | Type | Description |
|---|---|---|
from / to |
int | پیشفرض: از حالا تا ۳۰ روز بعد |
فقط مسدودسازیهای دستی برمیگردند؛ اشغالِ نوبتها اینجا نمیآید.
POST /api/v1/resource/{uuid}/blocks
{ "starts_at": 1785600000, "ends_at": 1785614400, "reason": "سرویس دورهای دستگاه" }
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_002 |
422 | بازه غایب |
ERR_VALIDATION_001 |
422 | پایان قبل از شروع |
ERR_SLOT_TAKEN |
409 | در این بازه نوبت یا رزرو موقت هست |
مسدودسازی روی بازهای که نوبت دارد ظرفیت را پس نمیگیرد: نوبت سرجایش میماند و کاربر باید اول تکلیفش را روشن کند.
شمار نوبتهای آینده
GET /api/v1/resource/{uuid} علاوه بر خودِ منبع، upcoming_appointments میدهد: تعداد
نوبتهای آینده که روی این منبع نشستهاند.
در فهرست منابع نمیآید — آنجا یک کوئری per ردیف میشد. غیرفعالکردن منبع نوبتهای ثبتشده را لغو نمیکند و فقط از جستجوی وقتِ بعدی حذفش میکند، پس این عدد هشدار است نه مانع؛ پنل هنگام برداشتن تیک «منبع فعال است» نشانش میدهد.
DELETE /api/v1/resource-block/{uuid}
اشغالی که به نوبت یا رزرو موقت وصل است از این مسیر حذف نمیشود (422) — وگرنه
نوبت بیمار بیصدا منبعش را از دست میداد.
هر دو عمل رویداد دامنه ثبت میکنند: ResourceBlocked و ResourceReleased. ظرفیتی که
برمیگردد باید همانقدر شنیده شود که ظرفیتی که میرود؛ مصرفکنندهای که فقط اولی را
بشنود، منبع را برای همیشه اشغال میبیند.