- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks. - Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content. - Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
30 KiB
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 + services.view, محدود به محیط جاری — سرویس محیط دیگر
404 میگیرد. پروتکل خاصیتِ سرویس است، پس همان مجوزِ services را میگیرد، نه مجوز treatment
که برای پروندهٔ درمان است. گیتِ مجوز پیش از واکشی سرویس اجرا میشود، پس منشیِ بدون
services.view روی uuid ناموجود هم 403 میگیرد نه 404.
data: null یعنی سوییچ خاموش است، نه اینکه چیزی پیدا نشد.
وقتی پروتکل هست، یک کلید کمکی هم میآید:
| Field | توضیح |
|---|---|
service_has_resources |
آیا هیچ ResourceServiceOffering برای این سرویس هست |
false رزرو را قفل نمیکند — قاعدهٔ «سرویس بدون offering روی هر منبعی مجاز است» عمدی و
تستشده است. ولی مدیر باید ببیند، وگرنه تازه وقتی اپراتور جلوی بیمار میرسد معلوم میشود هیچ
دستگاهی وصل نشده و فرم درست نمیآید.
Response 200
{
"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 + services.update, محدود به محیط جاری.
Request Body (application/json)
| Field | Type | Required | توضیح |
|---|---|---|---|
steps |
array | ✅ | ۲ تا ۶۰ گام |
steps[].step_number |
int | ❌ | پیوسته از ۱؛ نبودنش یعنی ترتیب آرایه |
steps[].offset_days |
int | ✅ | فاصله از جلسهٔ قبلی |
staff_uuids |
string[] | ✅ | حداقل یکی، همه از محیط جاری و فعال؛ تکراریها حذف میشوند |
supervisor_doctor_uuid |
string | ❌ | پزشک ناظر؛ باید عضو همین محیط باشد |
{
"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:
{"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 + services.update, محدود به محیط جاری. عمداً update
است نه delete: سرویس حذف نمیشود، فقط سوییچِ «طول درمان» روی همان سرویس خاموش میشود.
Response 200
{ "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).
مقدارِ بدفرم بیصدا نادیده گرفته میشود، نه خطا — فیلتر است نه ورودی فرم.
{
"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 (خروجی واقعی)
{
"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 جلسه
مینشیند. بازنویسی گذشته یعنی مقایسهٔ پیشبینی با واقعیت برای همیشه از بین میرود.
ثبت یک ناحیه
{
"resource_uuid": "…",
"parameters": { "energy": 18, "pulse": 3, "shots": 212 },
"note": "بدون عارضه"
}
resource_uuid اختیاری است: نبودنش یعنی همان دستگاهِ ارثرسیده، و فرستادنش یعنی اپراتور برای این
ناحیه دستگاه دیگری گذاشته.
درمانِ بیدستگاه مجاز است. بوتاکس تزریق است نه دستگاه؛ اجبارِ دستگاه یعنی کلینیک برای هر
تزریق یک منبع ساختگی بسازد. پس ناحیهای که نه دستگاه دارد و نه مقداری برایش آمده، بسته میشود.
ولی فرستادن parameters بدون دستگاه ⇒ 422 — با چه schemaیی سنجیده شود؟
پاسخِ GET دو کلید کمکی دارد: devices فهرست دستگاههای فعالِ محیط، و forms نگاشت
uuid دستگاه → فیلدهایش. پنل با همین دو، انتخابگر دستگاه و فرم متناظرش را میسازد بدون اینکه
چیزی دربارهٔ لیزر بداند.
parameters با field_schemaِ نوع همان منبع سنجیده میشود — قواعدش در
resource.md. کلید ناشناخته، مقدار خارج از گزینهها و فیلد
الزامیِ نیامده هر سه 422 میگیرند.
ناحیهای که یک بار بسته شده (completed یا skipped) دوباره بسته نمیشود ⇒ 422.
اتمام جلسه
{ "note": "یادداشت کلی جلسه" }
ناحیهٔ ناتمام مانع نیست — اپراتور جلوی بیمار ایستاده و نباید در نرمافزار گیر کند — ولی تعدادشان در پاسخ میآید تا پنل هشدار بدهد:
{ "unsettled_areas": 2 }
وضعیت نوبت completed میشود. اگر گذار مجاز نباشد (منشی وضعیت را دستی عوض کرده) جلسه
بسته میشود و نوبت دستنخورده میماند؛ ماجرا لاگ میشود، خطای ۵۰۰ داده نمیشود.
سپس TreatmentWorkflow حوزهٔ فعالیت سررسید جلسهٔ بعد را از تاریخ واقعیِ همین جلسه
حساب میکند، و اگر جلسهٔ دیگری نمانده باشد دوره بسته میشود.