# Treatment API > **Prefixes:** `/api/v1/service-item/{uuid}/treatment-protocol` · `/api/v1/treatment-case[s]` · > `/api/v1/treatment-session[s]` · `/api/v1/dashboard/staff/…` A **treatment protocol** is the "طول درمان" of a service: it says a course of that service runs over several sessions, when each one falls due, which doctor supervises it, and which staff may perform it. A service without a protocol row is single-session — the row's existence *is* the switch, which is why there is no separate boolean. `ServiceItem.session_count` is **deprecated**. It never had logic behind it; session count now comes from the protocol's step list. The column still appears in service payloads so existing clients do not break, but nothing should read it. Superseded design note: an earlier `src/Course/` module (`CourseProtocol` / `TreatmentCourse`) was deleted in `65d5831c` and its docs removed. It modelled the gap between sessions as one min/ideal/max triple for the whole course, which cannot express a course whose intervals differ per session — a botox course is session 1, then +15 days, then monthly. This design stores an explicit offset per step instead. --- ## The step model Each step carries `offset_days`, and that offset is measured **from the previous session**, not from the start of the course. The spacing of a laser course is a clinical requirement — hair regrows relative to the last treatment, not relative to when the file was opened — so a patient who arrives late shifts the rest of their course rather than getting the next session too early. | Rule | چرا | |---|---| | حداقل ۲ گام | یک گام یعنی سرویس تک‌جلسه‌ای؛ سوییچ اصلاً نباید روشن باشد | | حداکثر ۶۰ گام | سقف عقلانی، جلوی ورودی اشتباه را می‌گیرد | | `step_number` پیوسته از ۱ | «جلسهٔ ۳ از ۸» فقط وقتی معنی دارد که گامی جا نیفتاده باشد | | گام ۱ → `offset_days = 0` | لنگر دوره است و «جلسهٔ قبل» ندارد | | گام‌های بعدی → `offset_days > 0` | فاصلهٔ صفر یعنی دو جلسه در یک روز | | حداقل یک پرسنل مجاز | بدون آن هر پرسنلی پای هر دستگاهی می‌نشیند | --- ## GET `/api/v1/service-item/{uuid}/treatment-protocol` **Permission:** `IS_AUTHENTICATED_FULLY`, محدود به محیط جاری — سرویس محیط دیگر `404` می‌گیرد. `data: null` یعنی سوییچ خاموش است، نه اینکه چیزی پیدا نشد. وقتی پروتکل هست، یک کلید کمکی هم می‌آید: | Field | توضیح | |---|---| | `service_has_resources` | آیا هیچ `ResourceServiceOffering` برای این سرویس هست | `false` رزرو را **قفل نمی‌کند** — قاعدهٔ «سرویس بدون offering روی هر منبعی مجاز است» عمدی و تست‌شده است. ولی مدیر باید ببیند، وگرنه تازه وقتی اپراتور جلوی بیمار می‌رسد معلوم می‌شود هیچ دستگاهی وصل نشده و فرم درست نمی‌آید. ### Response `200` ```json { "success": true, "data": { "uuid": "8de51c47-ddd1-44ea-bae1-83cf6457b182", "service_uuid": "cc0b11ec-1c39-45f0-bc1d-5514619bc74a", "active": true, "total_sessions": 4, "supervisor": null, "steps": [ { "step_number": 1, "offset_days": 0 }, { "step_number": 2, "offset_days": 15 }, { "step_number": 3, "offset_days": 30 }, { "step_number": 4, "offset_days": 30 } ], "staff": [ { "uuid": "53acc523-48a7-4f4a-89b1-745e8e7a69bd", "name": "پرسنل۱" } ] } } ``` **Errors:** | Code | HTTP | توضیح | |------|------|-------| | ERR_NOT_FOUND_001 | 404 | سرویس یافت نشد یا مال محیط دیگری است | | ERR_AUTH_001 | 401 | بدون توکن | --- ## PUT `/api/v1/service-item/{uuid}/treatment-protocol` Replace the whole protocol. Creates it on first call, so this doubles as "turn the switch on". Everything is validated **before** anything is written: an invalid step at the end of the list must not wipe the valid steps already stored. Steps and staff are then cleared and rewritten inside one transaction. **Permission:** `IS_AUTHENTICATED_FULLY`, محدود به محیط جاری. ### Request Body (`application/json`) | Field | Type | Required | توضیح | |---|---|---|---| | `steps` | array | ✅ | ۲ تا ۶۰ گام | | `steps[].step_number` | int | ❌ | پیوسته از ۱؛ نبودنش یعنی ترتیب آرایه | | `steps[].offset_days` | int | ✅ | فاصله از جلسهٔ **قبلی** | | `staff_uuids` | string[] | ✅ | حداقل یکی، همه از محیط جاری و فعال؛ تکراری‌ها حذف می‌شوند | | `supervisor_doctor_uuid` | string | ❌ | پزشک ناظر؛ باید عضو همین محیط باشد | ```json { "staff_uuids": ["53acc523-48a7-4f4a-89b1-745e8e7a69bd"], "supervisor_doctor_uuid": null, "steps": [ { "step_number": 1, "offset_days": 0 }, { "step_number": 2, "offset_days": 15 }, { "step_number": 3, "offset_days": 30 }, { "step_number": 4, "offset_days": 30 } ] } ``` ### Response `200` Same shape as GET. **Errors:** | Code | HTTP | Field | توضیح | |------|------|-------|-------| | ERR_VALIDATION_001 | 422 | `steps` | کمتر از ۲ یا بیشتر از ۶۰ گام | | ERR_VALIDATION_002 | 422 | `steps` | فیلد `steps` نیست یا آرایه نیست | | ERR_VALIDATION_002 | 422 | `offset_days` | `offset_days` یک گام نیست یا عدد نیست | | ERR_VALIDATION_001 | 422 | `offset_days` | گام اول صفر نیست، یا گام بعدی صفر/منفی است | | ERR_VALIDATION_001 | 422 | `step_number` | شماره‌ها پیوسته از ۱ نیستند | | ERR_VALIDATION_002 | 422 | `staff_uuids` | فهرست خالی است یا uuid نامعتبر دارد | | ERR_NOT_FOUND_001 | 404 | `staff_uuids` | پرسنل یافت نشد، غیرفعال است، یا مال محیط دیگری است | | ERR_NOT_FOUND_001 | 404 | — | پزشک ناظر عضو این محیط نیست | Real 422 responses: ```json {"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"دورهٔ درمان حداقل 2 جلسه دارد؛ کمتر از آن یعنی سرویس تک‌جلسه‌ای","field":"steps"}]} {"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_002","message":"حداقل یک پرسنل مجاز الزامی است","field":"staff_uuids"}]} {"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"شمارهٔ گام‌ها باید پیوسته از ۱ باشد؛ گام 2 انتظار می‌رفت","field":"step_number"}]} ``` --- ## Opening a case — what happens on confirm There is no endpoint that opens a treatment case; it happens as a side effect of confirming an appointment, in `AppointmentConfirmationService::onConfirmed`: ``` نوبت تأیید شد → PatientSession ساخته می‌شود (مالی، مثل همیشه) → اگر سرویسِ نوبت پروتکل فعال دارد: TreatmentWorkflowRegistry::for(clinic.practice_domain.code)->openCase(...) ``` The booking core never names a specialty. A `TreatmentWorkflow` is selected by the clinic's practice domain code through a tagged-service registry, so adding dentistry is a new class rather than a change in the booking path. `LaserTreatmentWorkflow` handles `beauty`; `DefaultTreatmentWorkflow` answers for everything else, including a clinic that has chosen no domain at all — `null` means "behave as today", never an error. What opening a case does: | | | |---|---| | پروندهٔ باز موجود | برگردانده می‌شود؛ پروندهٔ دوم برای همان بیمار و همان سرویس ساخته نمی‌شود | | نواحی | برگ‌های دستهٔ سرویس، با نامشان، در همان لحظه کپی می‌شوند | | جلسات | همهٔ گام‌های پروتکل ساخته می‌شوند، همه `planned` | | نوبت | به **اولین جلسهٔ بدون نوبت** می‌چسبد و آن جلسه `booked` می‌شود | | سررسید | جلسهٔ رزروشده ساعت نوبت را می‌گیرد؛ بقیه `null` می‌مانند | Failure to open a case is logged and swallowed — the appointment is booked and possibly paid for, and losing that is worse than losing the case file, which can be rebuilt. ### Session due dates `due_at` of session *n* is `finished_at` of session *n−1* plus that step's `offset_days`. Only the **next** session is recomputed when one finishes; sessions further out keep their earlier estimate, because a number that is not yet anchored to anything real does not get more accurate by being recalculated. A no-show does not burn the session: its status becomes `no_show`, its appointment link is cleared, `total_sessions` is untouched, and the same session comes back to the front of the booking queue. --- ## DELETE `/api/v1/service-item/{uuid}/treatment-protocol` Turn the switch off — the protocol, its steps and its staff list are removed and the service is single-session again. Idempotent: deleting a service that has no protocol still answers `200`. **Permission:** `IS_AUTHENTICATED_FULLY`, محدود به محیط جاری. ### Response `200` ```json { "success": true, "data": null } ``` --- # Treatment cases ## GET `/api/v1/treatment-cases` پرونده‌های درمانِ محیط جاری، تازه‌ترین اول. **Permission:** `IS_AUTHENTICATED_FULLY`, محدود به محیط جاری. | Query | توضیح | |---|---| | `status` | `active` \| `completed` \| `abandoned` — نبودش یعنی همه | | `q` | جستجو روی نام بیمار، موبایل، کد ملی، شمارهٔ پرونده، نام سرویس و **نام پرسنل** | | `from` | `YYYY-MM-DD` میلادی — پرونده‌هایی که از ابتدای این روز به بعد باز شده‌اند | | `to` | `YYYY-MM-DD` میلادی — تا انتهای این روز | | `record` | uuid پروندهٔ بیمار — فقط دوره‌های همان بیمار. نمای «پروندهٔ بیمار» همین فهرست است. | بازه روی `opened_at` است نه سررسید جلسه. تایم‌زون تهران (`config/bootstrap_tz.php`). مقدارِ بدفرم بی‌صدا نادیده گرفته می‌شود، نه خطا — فیلتر است نه ورودی فرم. ```json { "uuid": "…", "status": "active", "total_sessions": 4, "completed_sessions": 1, "opened_at": 1785660000, "closed_at": null, "service": { "uuid": "…", "name": "لیزر توتال" }, "supervisor": { "uuid": "…", "name": "دکتر ناظر" }, "patient": { "record_uuid": "…", "name": "محمد رسولی", "mobile": "09120001111", "record_number": "۱۲" }, "areas": [ { "uuid": "…", "name": "بیکینی", "category_uuid": "…" } ], "assigned_staff": [ { "uuid": "…", "name": "پرسنل۱" } ], "performed_by": [ { "uuid": "…", "name": "پرسنل۱" } ] } ``` `assigned_staff` برنامه است و `performed_by` سابقه — اولی از خودِ پرونده می‌آید و دومی از `TreatmentSession.performedBy`. جستجوی `q` هر دو را می‌گیرد، چون «کارهای این نفر» شامل کارِ انجام‌شده هم هست. **پروندهٔ بدون اختصاص یعنی «هر کسی که پروتکل مجاز دانسته»، نه «هیچ‌کس».** پرونده‌ای که اپراتور دارد، جلساتش فقط در صفِ همان افراد دیده می‌شود. `areas[].uuid` شناسهٔ همان ردیفِ ناحیه است و `category_uuid` شناسهٔ دستهٔ کاتالوگ. ویرایش با دومی کار می‌کند؛ `null` یعنی دسته حذف شده و ناحیه فقط در سابقه مانده. ## GET `/api/v1/treatment-case/{uuid}` همان شکل، به‌علاوهٔ `sessions` و `available_areas` — نواحیِ قابل انتخاب برای همین سرویس، تا فرم ویرایش اندپوینت دومی نخواهد. پروندهٔ محیط دیگر `404` می‌گیرد. ## PATCH `/api/v1/treatment-case/{uuid}` ویرایش پروندهٔ درمان. هر فیلد اختیاری است؛ فقط کلیدهای فرستاده‌شده اعمال می‌شوند. | فیلد | توضیح | |---|---| | `status` | `active` \| `completed` \| `abandoned`. برگرداندن به `active` پروندهٔ بسته را باز می‌کند و `closed_at` را پاک می‌کند | | `supervisor_doctor_uuid` | پزشک ناظر؛ `null` یعنی بدون ناظر | | `area_uuids` | فهرست **دستهٔ کاتالوگ**، جایگزین کامل. حداقل یکی | | `staff_uuids` | اپراتورهای این پرونده، جایگزین کامل. فهرست خالی مجاز است | | `total_sessions` | بین `TreatmentProtocol::MIN_STEPS` و `MAX_STEPS`. کم‌کردن جلسات را از انتها حذف می‌کند | مرزِ ثابت: **هیچ ویرایشی سابقهٔ انجام‌شده را بازنویسی نمی‌کند.** | کد | HTTP | فیلد | شرط | |---|---|---|---| | ERR_VALIDATION_001 | 422 | `status` | وضعیت نامعتبر | | ERR_VALIDATION_001 | 422 | `area_uuids` | فهرست خالی یا نامعتبر | | ERR_VALIDATION_001 | 422 | `total_sessions` | خارج از بازهٔ مجاز | | ERR_NOT_FOUND_001 | 404 | `supervisor_doctor_uuid` / `area_uuids` | پزشک یا ناحیه یافت نشد | | ERR_NOT_FOUND_001 | 404 | `staff_uuids` | پرسنل یافت نشد، غیرفعال است، یا مال محیط دیگری است | | ERR_CONFLICT_001 | 409 | `area_uuids` | ناحیه در جلسه‌ای ثبت شده و حذف نمی‌شود | | ERR_CONFLICT_001 | 409 | `total_sessions` | کمتر از جلساتی که نوبت دارند یا انجام شده‌اند | قواعدش در `TreatmentCaseEditor` است نه کنترلر. --- ## GET `/api/v1/treatment-case/{uuid}/plan` تقویمِ **کل** دوره به‌علاوهٔ آنچه در هر جلسه انجام شده. مصرف‌کننده‌اش تب «نوبت‌های بعدی» در پروندهٔ بیمار است. جدا از `GET /treatment-case/{uuid}` است نه اضافه به آن: آن پاسخ مصرف‌کنندهٔ دیگری دارد (مودال ویرایش) که نه تقویم لازم دارد نه نواحی. | فیلد هر جلسه | توضیح | |---|---| | `planned_at` | زمان جلسه — قطعی یا تخمینی | | `is_estimate` | `false` یعنی به واقعیتی گره خورده، `true` یعنی محاسبهٔ لحظهٔ نمایش | | `areas[]` | نواحی با `parameters` (خوانده‌های دستگاه)، `resource`، `note`، زمان‌ها | `case.patient_national_code` هم می‌آید: از پروفایل و در نبودش از کاربر (همان COALESCE که `PatientController` می‌کند). فرم ثبت نوبت با آن بیمار را از پیش پر می‌کند تا منشی کسی را که همین‌جا معلوم است دوباره جستجو نکند. پاسخ یک `resource` هم دارد: دستگاهی که جلسهٔ قبلِ همین دوره رویش انجام شده (`NextSessionSlotFinder::preferredResource`). فرم ثبت نوبت بدونش کار نمی‌کند — سرویسِ دوره روی تقویم منبع رزرو می‌شود نه روی برنامهٔ پزشک. `null` یعنی هنوز هیچ جلسه‌ای روی دستگاهی انجام نشده. **`planned_at` ذخیره نمی‌شود.** `TreatmentScheduler` فقط سررسید جلسهٔ بعدی را می‌نویسد؛ بقیهٔ زنجیره را `TreatmentPlanProjector` در لحظهٔ خواندن می‌سازد. لنگرِ هر جلسه به‌ترتیب: زمان اتمام، زمان نوبت، `due_at` نوشته‌شده. دوره‌ای که هیچ‌کدام را ندارد از `opened_at` شروع می‌شود. یعنی تاریخ‌های تخمینی ممکن است بین دو بار خواندن عوض شوند — این عمدی است و در UI با برچسب «تخمینی» اعلام می‌شود. ### Response `200` (خروجی واقعی) ```json { "success": true, "data": { "case": { "uuid": "2a8b1e28-…", "status": "active", "patient": { "name": "محمد رستمی" } }, "sessions": [ { "session_number": 1, "status": "done", "planned_at": 1786106340, "is_estimate": false, "areas": [ { "area": { "name": "دست" }, "status": "completed", "parameters": { "energy": 8, "pulse": 3, "shots": 23 } } ] }, { "session_number": 2, "status": "planned", "planned_at": 1787402340, "is_estimate": false, "areas": [] }, { "session_number": 3, "status": "planned", "planned_at": 1789994340, "is_estimate": true, "areas": [] } ] } } ``` **Errors:** | Code | HTTP | شرط | |---|---|---| | ERR_NOT_FOUND_001 | 404 | پرونده یافت نشد یا مال محیط دیگری است | | ERR_AUTH_001 | 401 | بدون توکن | --- ## GET `/api/v1/treatment-session/{uuid}` یک جلسه به‌تنهایی، به‌علاوهٔ `case_uuid`، `service` و `patient` — برای فرمِ «ثبت نوبت این جلسه». جلسهٔ محیط دیگر `404` می‌گیرد. --- ## بستنِ نوبت به یک جلسهٔ مشخص یک بیمار می‌تواند چند دورهٔ باز داشته باشد. تا پیش از این، اتصال **ضمنی** بود: هنگام قطعی‌شدن، از روی سرویسِ نوبت پروندهٔ باز پیدا می‌شد و نوبت به اولین جلسهٔ بدون‌نوبتِ آن می‌چسبید. انتخابِ سرویسِ اشتباه بی‌صدا یک پروندهٔ موازی می‌ساخت. `POST /api/v1/my/appointment` حالا `treatment_session_uuid` اختیاری می‌گیرد: | کد | HTTP | شرط | |---|---|---| | ERR_NOT_FOUND_001 | 404 | جلسه یافت نشد یا مال محیط دیگری است | | ERR_CONFLICT_001 | 409 | جلسه از قبل نوبت دارد | | ERR_CONFLICT_001 | 409 | پروندهٔ جلسه باز نیست | | ERR_CONFLICT_001 | 409 | جلسه مالِ بیمار دیگری است | وقتی فرستاده شود، همان جلسه رزرو می‌شود و منطقِ «اولین جلسهٔ بدون نوبت» هنگام تأیید کنار می‌رود. نبودنش یعنی همان رفتار قبلی. ### آزاد شدن جلسه `cancelled_by_user` · `cancelled_by_doctor` · `no_show` جلسه را از نوبت جدا می‌کنند و به `planned` برمی‌گردانند، پس دوباره در «جلسات بدون نوبت» دیده می‌شود. غیبت در `CANCEL_STATUSES` نیست ولی برای دوره فرقی ندارد — سابقهٔ غیبت روی خودِ نوبت می‌ماند. جلسهٔ `done` استثناست: سابقه است و آزاد نمی‌شود. --- ## GET `/api/v1/treatment-sessions/unbooked` صفِ «جلسات بدون نوبت» — جلسه‌ای که سررسیدش رسیده و کسی رزروش نکرده. رزرو جلسهٔ بعد عمداً خودکار نیست: سیستم نمی‌داند بیمار پنجشنبه‌ها سر کار است و «اولین وقت آزاد» معمولاً بدترین وقت است چون کسی نخواسته‌اش. پس کار در صفی دیده می‌شود که منشی از رویش عمل می‌کند، نه رفتاری که بی‌صدا اتفاق بیفتد. | Query | پیش‌فرض | توضیح | |---|---|---| | `within_days` | `7` | تا چند روز آینده؛ سقف ۹۰ | جلسه‌ای که از قبل نوبت دارد در این فهرست نمی‌آید. --- ## GET `/api/v1/treatment-session/{uuid}/slot-suggestions` اسلات‌های آزادِ منبع، از سررسید جلسه به بعد. **پیشنهاد است، نه رزرو**؛ ثبت نوبت از مسیر عادی انجام می‌شود. | Query | پیش‌فرض | توضیح | |---|---|---| | `resource_uuid` | منبعِ جلسهٔ قبلی | ادامهٔ دوره روی همان دستگاه، هم یکنواخت‌تر است هم یک انتخاب کمتر | | `days` | `14` | افق جست‌وجو؛ سقف ۶۰ | سررسیدِ گذشته یعنی بیمار دیر کرده، پس جست‌وجو از امروز شروع می‌شود نه از تاریخی که رد شده. مدتِ نوبت روی همان منبع حل می‌شود، نه از پیش‌فرض خام سرویس. **۴۲۲** وقتی نه `resource_uuid` آمده و نه جلسه‌ای از قبل رزرو شده — فهرست خالی برنمی‌گردد، چون «منبعی مشخص نیست» با «وقتی نیست» یکی نیست. **۴۰۴** برای منبع محیط دیگر. --- # Staff panel — running a session همهٔ این مسیرها زیر `/api/v1/dashboard/staff` هستند چون `StaffRouteGuardSubscriber` کاربرِ فقط-پرسنل را بیرون از همان پیشوند می‌بندد؛ باز کردن راهِ تازه با allowlist یعنی مرزِ دسترسی در دو جا تعریف شود. **Permission:** `ROLE_STAFF` **به‌علاوهٔ** ردیف فعالِ پرسنل در محیط جاری. نقش به‌تنهایی کافی نیست: توکن تا انقضا معتبر می‌ماند و غیرفعال‌شدنِ پرسنل باید همان لحظه دسترسی را ببندد. | Method | Path | کار | |---|---|---| | GET | `/dashboard/staff/treatment-sessions` | صفِ امروزِ همین پرسنل — پایین را ببینید | | GET | `/dashboard/staff/treatment-session/{uuid}` | جزئیات جلسه + نواحی + `devices` + `forms` | | POST | `/dashboard/staff/treatment-session/{uuid}/start` | شروع جلسه | | POST | `/dashboard/staff/treatment-session/{uuid}/finish` | اتمام جلسه | | POST | `/dashboard/staff/session-area/{uuid}/start` | شروع یک ناحیه | | POST | `/dashboard/staff/session-area/{uuid}/complete` | ثبت خوانده‌های دستگاه | | POST | `/dashboard/staff/session-area/{uuid}/skip` | صرف‌نظر از ناحیه — `note` اختیاری | | POST | `/dashboard/staff/session-area/{uuid}/reopen` | باز کردن دوبارهٔ ناحیهٔ بسته‌شده | ### «جلسات امروز من» یک صف است هر ردیف علاوه بر فیلدهای معمولِ جلسه، این‌ها را هم دارد: | فیلد | توضیح | |---|---| | `case_uuid` | پروندهٔ درمانِ همین جلسه | | `service_name` | نام سرویس | | `patient_name` | نام بیمار، از نوبتِ متصل — `null` اگر جلسه نوبت ندارد | | `resource_name` | دستگاهِ نوبت — `null` اگر نوبت روی منبع نبوده | `patient_name` و `resource_name` فقط در همین اندپوینت اضافه می‌شوند، نه در `TreatmentSession::toArray()`؛ صفِ اپراتور بدون نام بیمار بی‌معنی است ولی هویت بیمار نباید در هر مصرف‌کنندهٔ دیگرِ آن متد هم بنشیند. جلسهٔ امروز از سه راه به یک اپراتور می‌رسد: - جلسه‌ای که خودش برداشته — `TreatmentSession.performedBy` هنگام «شروع جلسه» ست می‌شود. - نوبتی که منشی از قبل به او داده — `staff_uuid` هنگام ثبت نوبت. - کارِ بی‌صاحبِ امروز، اگر پروتکلِ آن سرویس نامش را برده باشد. پروتکلی که هیچ پرسنلی برایش تعریف نشده یعنی همه مجازند، نه هیچ‌کس — همان قاعدهٔ `ResourceServiceOffering` که نبودِ رکورد را محدودیت حساب نمی‌کند. نتیجه همیشه به محیطِ خودِ پرسنل محدود است. ### صرف‌نظر و برگرداندن ناحیه `skip` یک `note` اختیاری می‌گیرد. «چرا این ناحیه انجام نشد» بخشی از سابقهٔ درمان است؛ بدونش جلسهٔ بعد فقط یک خلأ بی‌توضیح می‌بیند. `reopen` ناحیهٔ بسته‌شده — چه `completed` چه `skipped` — را برمی‌گرداند، برای اشتباهی که حین کار معلوم می‌شود. وضعیت به `in_progress` برمی‌گردد (یا `pending` اگر هرگز شروع نشده بود) و `finished_at` پاک می‌شود. `parameters` و `note` می‌مانند تا اپراتور ببیند چه ثبت شده بود و رویش بنویسد. | کد | HTTP | شرط | |---|---|---| | ERR_VALIDATION_001 | 422 | ناحیه باز است و چیزی برای برگرداندن ندارد | | ERR_CONFLICT_001 | 409 | جلسه بسته شده؛ رکورد دیگر سابقه است نه فرم | ### شروع جلسه رکوردِ هر ناحیهٔ پرونده یک بار ساخته می‌شود، پس فراخوانی دوباره ناحیهٔ تکراری نمی‌سازد. **دستگاه از نوبت به ارث می‌رسد.** هر رکورد ناحیه با `Appointment.resource` ساخته می‌شود؛ منشی همان لحظهٔ رزرو انتخابش کرده و پرسیدن دوباره‌اش از اپراتور یعنی یک تصمیم را دو بار گرفتن. اپراتور می‌تواند per ناحیه عوضش کند — همان کاری که لازم است وقتی بیکینی با الکساندرایت و زیر بغل با دایود انجام می‌شود. پرسنلِ فراخوان به‌عنوان **انجام‌دهندهٔ واقعی** ثبت می‌شود — ممکن است با پرسنلِ برنامه‌ریزی‌شدهٔ نوبت فرق کند، و سابقهٔ پزشکی باید بگوید چه کسی واقعاً دستگاه را دست گرفت. وضعیت نوبت به `salon` می‌رود. **زمان نوبت هرگز بازنویسی نمی‌شود**: `slot_start`/`slot_end` تعهدِ رزرو و ورودیِ محاسبهٔ اشغال‌اند، و «چقدر طول کشید» در `started_at`/`finished_at` جلسه می‌نشیند. بازنویسی گذشته یعنی مقایسهٔ پیش‌بینی با واقعیت برای همیشه از بین می‌رود. ### ثبت یک ناحیه ```json { "resource_uuid": "…", "parameters": { "energy": 18, "pulse": 3, "shots": 212 }, "note": "بدون عارضه" } ``` `resource_uuid` اختیاری است: نبودنش یعنی همان دستگاهِ ارث‌رسیده، و فرستادنش یعنی اپراتور برای این ناحیه دستگاه دیگری گذاشته. **درمانِ بی‌دستگاه مجاز است.** بوتاکس تزریق است نه دستگاه؛ اجبارِ دستگاه یعنی کلینیک برای هر تزریق یک منبع ساختگی بسازد. پس ناحیه‌ای که نه دستگاه دارد و نه مقداری برایش آمده، بسته می‌شود. ولی فرستادن `parameters` بدون دستگاه ⇒ `422` — با چه schemaیی سنجیده شود؟ پاسخِ `GET` دو کلید کمکی دارد: `devices` فهرست دستگاه‌های فعالِ محیط، و `forms` نگاشت `uuid دستگاه → فیلدهایش`. پنل با همین دو، انتخابگر دستگاه و فرم متناظرش را می‌سازد بدون اینکه چیزی دربارهٔ لیزر بداند. `parameters` با `field_schema`ِ **نوع همان منبع** سنجیده می‌شود — قواعدش در [resource.md](./resource.md#فرم-ثبت-درمان). کلید ناشناخته، مقدار خارج از گزینه‌ها و فیلد الزامیِ نیامده هر سه `422` می‌گیرند. ناحیه‌ای که یک بار بسته شده (`completed` یا `skipped`) دوباره بسته نمی‌شود ⇒ `422`. ### اتمام جلسه ```json { "note": "یادداشت کلی جلسه" } ``` **ناحیهٔ ناتمام مانع نیست** — اپراتور جلوی بیمار ایستاده و نباید در نرم‌افزار گیر کند — ولی تعدادشان در پاسخ می‌آید تا پنل هشدار بدهد: ```json { "unsettled_areas": 2 } ``` وضعیت نوبت `completed` می‌شود. اگر گذار مجاز نباشد (منشی وضعیت را دستی عوض کرده) جلسه بسته می‌شود و نوبت دست‌نخورده می‌ماند؛ ماجرا لاگ می‌شود، خطای ۵۰۰ داده نمی‌شود. سپس `TreatmentWorkflow` حوزهٔ فعالیت سررسید جلسهٔ بعد را از **تاریخ واقعیِ همین جلسه** حساب می‌کند، و اگر جلسهٔ دیگری نمانده باشد دوره بسته می‌شود.