The tab was called 'نوبتهای بعدی' but shows the whole course — finished sessions with their recorded readings as much as upcoming ones. It is 'دورههای درمان' now. Booking from a session still made the user search for a patient the page already had open. The plan response carries the patient's national code (from the profile, falling back to the user — the same COALESCE PatientController uses, because users.national_code is routinely empty), and the modal takes a patient prop that seeds the lookup and hides the search step. The old 'بیمار یافت شد' card is suppressed in that mode; saying it twice is noise. Sessions are now searchable and paged. A protocol allows up to 60 steps and a patient can hold several courses, so an unbounded list was only ever going to work for the small cases. Search filters on what the card actually shows — service, staff, status, session number, area names — and runs in the page, since /plan already returns the whole course and a round trip would add latency and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
541 lines
29 KiB
Markdown
541 lines
29 KiB
Markdown
# 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` حوزهٔ فعالیت سررسید جلسهٔ بعد را از **تاریخ واقعیِ همین جلسه**
|
||
حساب میکند، و اگر جلسهٔ دیگری نمانده باشد دوره بسته میشود.
|