feat(docs): add laser treatment plan and related ADRs for multi-session treatments
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
# طول درمان و پرونده درمان چندجلسهای (فاز اول: لیزر)
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (backend + پنل ادمین). هیچ تغییری در `nobat724_front` و `clinic-pro-tauri` لازم نیست.
|
||||
|
||||
## زمینه
|
||||
|
||||
این سند خروجی یک جلسه grilling است. ۲۳ تصمیم قفل شد، شش ADR و یک glossary نوشته شد.
|
||||
پیش از شروع اینها را بخوان:
|
||||
|
||||
- `clinicpro/CONTEXT.md` — واژگان رسمی این دامنه
|
||||
- `clinicpro/docs/adr/0001-treatment-sessions-are-not-appointments.md`
|
||||
- `clinicpro/docs/adr/0002-treatment-areas-are-snapshotted.md`
|
||||
- `clinicpro/docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md`
|
||||
- `clinicpro/docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md`
|
||||
- `clinicpro/docs/adr/0005-treatment-workflows-are-tagged-services.md`
|
||||
- `clinicpro/docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md`
|
||||
- `clinicpro/docs/architecture/resource-first-model.md`
|
||||
|
||||
از واژگان `CONTEXT.md` استفاده کن. مترادف نساز.
|
||||
|
||||
## هدف
|
||||
|
||||
کلینیک بتواند سرویسی تعریف کند که درمانش چند جلسه طول میکشد، و سیستم برای هر بیمار
|
||||
پرونده درمان بسازد، جلسات را بشمارد، سررسید جلسه بعد را حساب کند، و اپراتور بتواند
|
||||
برای هر ناحیه بدن، دستگاه و پارامترهایش را ثبت کند.
|
||||
|
||||
فاز اول فقط لیزر. ولی هیچجای کد نباید کلمه «لیزر» را بداند، جز پیادهسازی workflow.
|
||||
|
||||
## آنچه از قبل هست و باید استفاده شود
|
||||
|
||||
| چیز | کجا | نکته |
|
||||
|---|---|---|
|
||||
| دستهبندی گرافی خدمات | `CatalogCategory` + `CatalogCategoryInclude` | نواحی بدن همینجا تعریف میشوند |
|
||||
| بستار گذرا و تشخیص تعارض | `CategoryClosureResolver` | `descendants()` و `overlaps()` |
|
||||
| سرویس | `ServiceItem` با `catalogCategory` | قیمت و مدت اینجاست |
|
||||
| نوع منبع داینامیک | `ResourceType` | کدهای سیستمی `doctor` / `staff` / `room` |
|
||||
| منبع با پزشک ناظر | `ClinicResource.supervisor` | تعریف شده ولی در مسیر رزرو خوانده نمیشود |
|
||||
| اشغال منبع در سطح دیتابیس | `OccupancyBucket` | `uniq_bucket_resource_seat(resource_id, bucket_at, seat)` |
|
||||
| پرسنل و نقش | `ClinicStaff` + `ROLE_STAFF` | `ClinicStaff.user` اختیاری است |
|
||||
| داشبورد پرسنل | `GET /api/v1/dashboard/staff` | فیلتر روی `a.staff` |
|
||||
| صفحه پرسنل | `assets/admin/pages/StaffMyServicesPage.tsx` | فقط سرویسهای تخصیصیافته |
|
||||
| مراجعه مالی | `PatientSession` | در `AppointmentConfirmationService::onConfirmed` ساخته میشود |
|
||||
|
||||
## آنچه نیست
|
||||
|
||||
هیچ موجودیتی برای پرونده درمان، جلسه درمان، ثبت ناحیه، پروتکل، یا حوزه فعالیت.
|
||||
`ServiceItem.sessionCount` هست ولی هیچ منطقی از آن استفاده نمیکند — عملاً مرده.
|
||||
|
||||
---
|
||||
|
||||
## تصمیمهای قفلشده
|
||||
|
||||
اینها در جلسه grilling تصمیمگیری شدهاند. اجرایشان کن، دوباره طراحی نکن.
|
||||
اگر جایی از کد با یکی از اینها تناقض داشت، **متوقف شو و بپرس**؛ خودت تصمیم را عوض نکن.
|
||||
|
||||
1. **حوزه فعالیت از `Specialty` جداست.** `Specialty` قرارداد سایت عمومی است و دست نمیخورد.
|
||||
2. **هر کلینیک یک حوزه فعالیت دارد.** نال یعنی تنظیمنشده و رفتار امروز.
|
||||
3. **جلسه درمان موجودیت مستقل است**، نه `Appointment`.
|
||||
4. **زمانبندی با لیست صریح گامها.** فاصله ثابت نداریم.
|
||||
5. **دوره پایان مشخص دارد.** دوره بیپایان وجود ندارد.
|
||||
6. **همه جلسات از ابتدا ساخته میشوند**، ولی فقط جلسه بعدی نوبت میگیرد.
|
||||
7. **ناحیه درمان همان `CatalogCategory` است.** هیچ entity جدیدی برای ناحیه ساخته نشود.
|
||||
8. **نواحی از دسته سرویس مشتق میشوند**، هنگام رزرو انتخاب نمیشوند.
|
||||
9. **فهرست نواحی هنگام ساخت پرونده قفل میشود** (snapshot).
|
||||
10. **پزشک ناظرِ منبع روی نوبت مینشیند.** پرسنل انجام میدهد.
|
||||
11. **نوبتی که منبع دارد `active_slot_key` ندارد.** حفاظت فقط با `OccupancyBucket`.
|
||||
12. **`TreatmentProtocol` موجودیت جداست**، یکبهیک با `ServiceItem`.
|
||||
13. **پارامترهای دستگاه در JSON**، تعریفشان روی `ResourceType`.
|
||||
14. **Workflow با tagged service.** موتور دادهمحور نداریم.
|
||||
15. **حوزه فعالیت را فقط ادمین پلتفرم میسازد.**
|
||||
16. **جلسه وضعیت مستقل دارد** و نوبت را همگام میکند. بستن جلسه با ناحیه ناتمام مجاز است.
|
||||
17. **`TreatmentSession` بالینی است، `PatientSession` مالی.** هیچ فیلد پولی روی جلسه درمان.
|
||||
18. **قیمت هر جلسه با قیمت روز.** پکیج قیمت نداریم. قیمت هیچوقت قفل نمیشود.
|
||||
19. **جلسه بعد پیشنهاد میشود، منشی تأیید میکند.** رزرو خودکار بدون انسان نداریم.
|
||||
20. **حوزه فعالیت فقط workflow را انتخاب میکند.** داشبورد اختصاصی بهازای تخصص نداریم.
|
||||
21. **سررسید هر جلسه نسبی به تاریخ واقعی جلسه قبل است.**
|
||||
22. **no-show جلسه را نمیسوزاند.** تعداد جلسات ثابت میماند.
|
||||
23. **زمان واقعی جدا ثبت میشود.** `slotStart` و `slotEnd` هرگز بازنویسی نمیشوند.
|
||||
|
||||
---
|
||||
|
||||
## وظایف
|
||||
|
||||
هر وظیفه را جداگانه پیاده کن، تست بنویس، و بعد سراغ بعدی برو.
|
||||
|
||||
### ۱. حوزه فعالیت کلینیک
|
||||
|
||||
موجودیت جدید در `src/PracticeDomain/Entity/PracticeDomain.php`:
|
||||
|
||||
```php
|
||||
// جدول سراسری (global) — نه per-tenant. در GlobalTables ثبت شود.
|
||||
id, uuid, code (unique), name, sort_order, active, created_at, updated_at
|
||||
```
|
||||
|
||||
`code` پایدار است و workflow به آن bind میشود. بعد از ساخت قابل ویرایش نیست.
|
||||
|
||||
روی `Clinic` یک `ManyToOne` نالپذیر اضافه کن:
|
||||
|
||||
```php
|
||||
#[ORM\ManyToOne(targetEntity: PracticeDomain::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?PracticeDomain $practiceDomain = null;
|
||||
```
|
||||
|
||||
migration نباید مقدار پیشفرض برای کلینیکهای موجود بگذارد. نال بماند.
|
||||
|
||||
اندپوینتها:
|
||||
|
||||
- `GET /api/v1/practice-domains` — فهرست فعالها. برای همه نقشهای پنلی.
|
||||
- `POST /api/v1/admin/practice-domains` — فقط `ROLE_ADMIN`.
|
||||
- `PATCH /api/v1/admin/practice-domains/{uuid}` — فقط `name` و `active` و `sort_order`.
|
||||
- `PATCH /api/v1/clinic/{uuid}/practice-domain` — مدیر کلینیک انتخاب میکند.
|
||||
|
||||
در پاسخ هر حوزه یک فیلد `has_workflow` بگذار که از `TreatmentWorkflowRegistry` میآید.
|
||||
پنل ادمین پلتفرم باید بتواند نشان دهد کدام حوزه هنوز workflow ندارد.
|
||||
|
||||
seed اولیه با migration: `beauty` = «کلینیک زیبایی».
|
||||
|
||||
### ۲. پروتکل درمان
|
||||
|
||||
سه موجودیت در `src/Treatment/Entity/`:
|
||||
|
||||
```php
|
||||
// TreatmentProtocol — وجود این ردیف یعنی سوییچ «طول درمان» روشن است
|
||||
id, uuid, service_item_id (unique, ON DELETE CASCADE),
|
||||
supervisor_doctor_id (nullable), active, created_at, updated_at
|
||||
|
||||
// TreatmentProtocolStep — گامهای دوره
|
||||
id, protocol_id, step_number, offset_days, created_at
|
||||
// UNIQUE(protocol_id, step_number)
|
||||
// offset_days یعنی «فاصله از جلسهٔ قبل»، نه از شروع دوره. گام ۱ همیشه offset_days = 0.
|
||||
|
||||
// TreatmentProtocolStaff — پرسنل مجاز به انجام
|
||||
id, protocol_id, staff_id
|
||||
// UNIQUE(protocol_id, staff_id)
|
||||
```
|
||||
|
||||
قواعد اعتبارسنجی:
|
||||
|
||||
- حداقل دو گام. پروتکل یکجلسهای معنی ندارد؛ آن یعنی سوییچ خاموش.
|
||||
- `step_number` پیوسته از ۱.
|
||||
- گام اول `offset_days = 0`. بقیه بزرگتر از صفر.
|
||||
- حداقل یک پرسنل مجاز.
|
||||
- `supervisor_doctor_id` باید پزشکِ همان محیط باشد.
|
||||
|
||||
`ServiceItem::$sessionCount` را `@deprecated` علامت بزن، از `toArray()` بیرون **نبر**
|
||||
(پنل و تایپ TS به آن وابستهاند) ولی هیچ منطق جدیدی از آن نخوان. تعداد جلسات
|
||||
همیشه `count(protocol.steps)` است.
|
||||
|
||||
اندپوینتها زیر `/api/v1/clinic-services/{serviceUuid}/treatment-protocol`:
|
||||
`GET`، `PUT` (کل پروتکل با گامها و پرسنل یکجا)، `DELETE` (خاموش کردن سوییچ).
|
||||
|
||||
### ۳. پرونده و جلسه درمان
|
||||
|
||||
```php
|
||||
// TreatmentCase
|
||||
id, uuid, entity_type, entity_id, // tenant
|
||||
patient_record_id, service_item_id, protocol_id,
|
||||
supervisor_doctor_id (nullable),
|
||||
status, // active | completed | abandoned
|
||||
total_sessions, // snapshot از تعداد گامها
|
||||
opened_at, closed_at (nullable),
|
||||
created_at, updated_at
|
||||
|
||||
// TreatmentCaseArea — snapshot نواحی (تصمیم ۹)
|
||||
id, case_id, catalog_category_id, name_snapshot, sort_order
|
||||
// UNIQUE(case_id, catalog_category_id)
|
||||
|
||||
// TreatmentSession
|
||||
id, uuid, case_id, session_number,
|
||||
appointment_id (nullable, ON DELETE SET NULL),
|
||||
performed_by_staff_id (nullable), // تصمیم ۲۳
|
||||
status, // planned | booked | in_progress | done | cancelled | no_show
|
||||
due_at (nullable), // تخمینی، بعد از هر جلسه بازمحاسبه میشود
|
||||
started_at (nullable), finished_at (nullable),
|
||||
note (nullable),
|
||||
created_at, updated_at
|
||||
// UNIQUE(case_id, session_number)
|
||||
|
||||
// SessionAreaRecord
|
||||
id, uuid, session_id, case_area_id,
|
||||
resource_id (nullable), // دستگاه — در سطح ناحیه
|
||||
status, // pending | in_progress | completed | skipped
|
||||
parameters JSON (nullable), // { "energy": 18, "pulse": 3, "shots": 212 }
|
||||
started_at (nullable), finished_at (nullable),
|
||||
note (nullable),
|
||||
created_at, updated_at
|
||||
// UNIQUE(session_id, case_area_id)
|
||||
```
|
||||
|
||||
**هیچ ستون پولی روی این جدولها نگذار.** ADR-0006.
|
||||
|
||||
`name_snapshot` روی `TreatmentCaseArea` عمدی است: اگر مدیر بعداً اسم دسته را عوض کند،
|
||||
سابقه درمان نباید تغییر کند.
|
||||
|
||||
نواحی هنگام ساخت پرونده اینطور حساب میشوند:
|
||||
|
||||
```
|
||||
leaves = برگهای CategoryClosureResolver::descendants(service.catalogCategory)
|
||||
اگر descendants خالی بود → خودِ service.catalogCategory تنها ناحیه است
|
||||
```
|
||||
|
||||
«برگ» یعنی دستهای که خودش `descendants` ندارد. دستههای میانی فقط گروهبندیاند و
|
||||
ناحیه درمان نیستند.
|
||||
|
||||
### ۴. اتصال پزشک ناظر به مسیر رزرو
|
||||
|
||||
در `AppointmentController` بلوکی که پزشک را از منبع استنتاج میکند (حدود خط ۴۷۱)
|
||||
یک fallback اضافه کن:
|
||||
|
||||
```php
|
||||
if ($doctorUuid === '' && $resource->subject() instanceof Doctor) {
|
||||
$doctorUuid = $resource->subject()->getUuid();
|
||||
}
|
||||
// جدید:
|
||||
if ($doctorUuid === '' && $resource->getSupervisor() !== null) {
|
||||
$doctorUuid = $resource->getSupervisor()->getUuid();
|
||||
}
|
||||
```
|
||||
|
||||
اگر منبعی نه پزشک است نه ناظر دارد، خطای واضح بده:
|
||||
«این منبع پزشک ناظر ندارد؛ ابتدا در تنظیمات منابع پزشک ناظر را مشخص کنید».
|
||||
پیام فعلی (`doctor_uuid یا resource_uuid ...`) گمراهکننده است.
|
||||
|
||||
### ۵. آزادسازی کلید اسلات برای نوبتهای منبعدار
|
||||
|
||||
در `Appointment::refreshActiveSlotKey()`:
|
||||
|
||||
```php
|
||||
$this->activeSlotKey = (!$this->isReserve
|
||||
&& $this->resource === null // ← شرط جدید
|
||||
&& in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true))
|
||||
? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)
|
||||
: null;
|
||||
```
|
||||
|
||||
`setResource()` باید `refreshActiveSlotKey()` را صدا بزند، وگرنه نوبتی که اول ساخته
|
||||
و بعد منبعش ست میشود کلیدش باقی میماند.
|
||||
|
||||
**قبل از این تغییر:** همه مسیرهای ساخت نوبت را فهرست کن و مشخص کن کدامها `resource`
|
||||
ست نمیکنند. آنها بعد از این تغییر همچنان با کلید پزشک محافظت میشوند — این درست است،
|
||||
ولی باید مستند شود که کدامها هستند. ADR-0003 روی همین هشدار داده.
|
||||
|
||||
migration لازم نیست؛ ستون بدون تغییر میماند و فقط منطق پرشدنش عوض میشود.
|
||||
|
||||
### ۶. تعریف فیلد روی نوع منبع
|
||||
|
||||
ستون JSON روی `ResourceType`:
|
||||
|
||||
```php
|
||||
#[ORM\Column(name: 'field_schema', type: 'json', nullable: true)]
|
||||
private ?array $fieldSchema = null;
|
||||
```
|
||||
|
||||
قالب هر فیلد:
|
||||
|
||||
```json
|
||||
{ "key": "energy", "label": "انرژی", "type": "select",
|
||||
"options": [7, 8, 9, 10, 12, 14, 16, 18], "required": true, "sort_order": 1 }
|
||||
```
|
||||
|
||||
`type` مجاز: `select` و `number` و `text`. همین سه تا، نه بیشتر.
|
||||
|
||||
اعتبارسنجی مقادیر `SessionAreaRecord.parameters` از همین schema میآید. یک سرویس
|
||||
`FieldSchemaValidator` بنویس که هم در ذخیره اندپوینت استفاده شود هم در تست.
|
||||
|
||||
کلیدهای ناشناخته که در schema نیستند رد شوند، نه اینکه بیصدا ذخیره شوند.
|
||||
|
||||
seed اولیه: یک `ResourceType` با کد `laser_device` و نام «دستگاه لیزر» و همان سه فیلد
|
||||
بالا بهعلاوه `shots` از نوع `number`. `is_system = false` تا مدیر بتواند ویرایشش کند.
|
||||
|
||||
### ۷. Workflow قابل توسعه
|
||||
|
||||
```php
|
||||
// src/Treatment/Workflow/TreatmentWorkflow.php
|
||||
#[AutoconfigureTag('app.treatment_workflow')]
|
||||
interface TreatmentWorkflow
|
||||
{
|
||||
public function supports(?string $practiceDomainCode): bool;
|
||||
|
||||
/** بعد از تأیید اولین نوبتِ یک سرویسِ پروتکلدار */
|
||||
public function openCase(Appointment $appointment, TreatmentProtocol $protocol): TreatmentCase;
|
||||
|
||||
/** بعد از بسته شدن یک جلسه — سررسید جلسه بعد را حساب میکند */
|
||||
public function onSessionFinished(TreatmentSession $session): void;
|
||||
}
|
||||
```
|
||||
|
||||
`TreatmentWorkflowRegistry` با `#[TaggedIterator('app.treatment_workflow')]` ساخته شود و
|
||||
اولین workflow ای که `supports()` بدهد را برگرداند. اگر هیچکدام، `DefaultTreatmentWorkflow`
|
||||
که رفتار عمومی دارد.
|
||||
|
||||
**هسته نباید بداند لیزر چیست.** `AppointmentConfirmationService` فقط این را میکند:
|
||||
|
||||
```
|
||||
اگر سرویسِ نوبت پروتکل فعال دارد و بیمار پروندهٔ باز برای همان سرویس ندارد
|
||||
→ registry->for(clinic.practiceDomain?.code)->openCase(...)
|
||||
```
|
||||
|
||||
`LaserTreatmentWorkflow` در فاز اول تقریباً همان `DefaultTreatmentWorkflow` است.
|
||||
جدا نگهش دار حتی اگر خالی باشد — نقطه اتصال آینده است.
|
||||
|
||||
### ۸. سررسید و جلسه بعد
|
||||
|
||||
قاعده محاسبه (تصمیم ۲۱):
|
||||
|
||||
```
|
||||
due_at(session n) = finished_at(session n-1) + protocol.step[n].offset_days
|
||||
جلسه ۱ سررسید ندارد؛ تاریخش همان نوبت اول است.
|
||||
```
|
||||
|
||||
بعد از `finished_at` شدن هر جلسه، فقط `due_at` **جلسه بعدی** بازمحاسبه شود، نه کل دوره.
|
||||
جلسات دورتر تخمین قبلیشان را نگه میدارند تا نوبتشان برسد.
|
||||
|
||||
no-show (تصمیم ۲۲): جلسه به `no_show` میرود، `session_number` عوض نمیشود،
|
||||
`total_sessions` عوض نمیشود. یک جلسه جایگزین با همان شماره **ساخته نمیشود**؛ همان جلسه
|
||||
دوباره به `planned` برمیگردد و `due_at` از تاریخ جلسه قبلِ **انجامشده** حساب میشود.
|
||||
|
||||
رزرو جلسه بعد (تصمیم ۱۹) **خودکار نیست**. اندپوینت پیشنهاد بده:
|
||||
|
||||
```
|
||||
GET /api/v1/treatment-sessions/{uuid}/slot-suggestions
|
||||
→ اسلاتهای آزاد منبع، از due_at به بعد، با استفاده از ResourceFreeTimeCalculator
|
||||
```
|
||||
|
||||
منشی یکی را انتخاب میکند و مسیر عادی ساخت نوبت اجرا میشود، سپس نوبت به جلسه وصل میشود.
|
||||
|
||||
### ۹. اندپوینتهای اجرای جلسه
|
||||
|
||||
همه زیر `ROLE_STAFF` یا بالاتر. علاوه بر نقش، بررسی کن پرسنل واقعاً به این جلسه دسترسی
|
||||
دارد — الگویش در `DashboardController::staff` هست (`findActiveByUserAndEntity`).
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/staff/treatment-sessions جلسات امروز پرسنل
|
||||
GET /api/v1/treatment-sessions/{uuid} جزئیات + نواحی + فیلدهای دستگاه
|
||||
POST /api/v1/treatment-sessions/{uuid}/start → in_progress، started_at
|
||||
POST /api/v1/treatment-sessions/{uuid}/finish → done، finished_at، note
|
||||
POST /api/v1/session-areas/{uuid}/start → in_progress، started_at
|
||||
POST /api/v1/session-areas/{uuid}/complete → completed، parameters، finished_at
|
||||
POST /api/v1/session-areas/{uuid}/skip → skipped
|
||||
GET /api/v1/treatment-cases فهرست پروندهها با فیلتر
|
||||
GET /api/v1/treatment-cases/{uuid} پرونده + همه جلسات
|
||||
```
|
||||
|
||||
همگامسازی وضعیت نوبت (تصمیم ۱۶):
|
||||
|
||||
```
|
||||
session start → Appointment::STATUS_SALON
|
||||
session finish → Appointment::STATUS_COMPLETED
|
||||
```
|
||||
|
||||
از `ALLOWED_TRANSITIONS` عبور کن، مستقیم `setStatus` نزن. اگر گذار مجاز نبود، جلسه را
|
||||
ببند ولی نوبت را دست نزن و لاگ بگذار — خطای ۵۰۰ نده.
|
||||
|
||||
بستن جلسه با ناحیه ناتمام **مجاز است** (تصمیم ۱۶). فقط در پاسخ تعداد ناتمامها را برگردان
|
||||
تا پنل هشدار نشان دهد.
|
||||
|
||||
`started_at` و `finished_at` هرگز روی `Appointment` نوشته نشوند (تصمیم ۲۳).
|
||||
|
||||
### ۱۰. پنل ادمین
|
||||
|
||||
قواعد اجباری این پروژه:
|
||||
|
||||
- هر `select` باید `components/ui/SearchableSelect` باشد. `<select>` بومی ممنوع.
|
||||
- هر بولین باید `components/ui/Switch` باشد. checkbox بومی ممنوع.
|
||||
- صفحه جدید با تم و Layout و کامپوننتهای موجود ساخته شود. طراحی جدید نکن.
|
||||
- تاریخها شمسی. متنها فارسی. RTL.
|
||||
|
||||
صفحهها و تغییرها:
|
||||
|
||||
1. **فرم سرویس** — سوییچ «طول درمان». روشن که شد: جدول گامها (شماره و فاصله از جلسه قبل)،
|
||||
`SearchableSelect` چندتایی پرسنل مجاز، `SearchableSelect` پزشک ناظر.
|
||||
2. **تنظیمات کلینیک** — `SearchableSelect` حوزه فعالیت.
|
||||
3. **صفحه نوع منابع** — ویرایشگر `fieldSchema`. افزودن و حذف فیلد، انتخاب نوع، گزینهها.
|
||||
4. **پنل ادمین پلتفرم** — CRUD حوزههای فعالیت با نشان «workflow دارد / ندارد».
|
||||
5. **فهرست پروندههای درمان** — نام بیمار، سرویس، جلسه چندم از چند، وضعیت، سررسید بعدی، اپراتور.
|
||||
6. **صفحه اجرای جلسه** — کارت هر ناحیه با دکمه شروع، تایمر زنده، فرم داینامیک از `fieldSchema`،
|
||||
دکمه اتمام ناحیه، دکمه لغو ناحیه. پایین صفحه یادداشت کلی و دکمه اتمام جلسه با هشدار
|
||||
نواحی ناتمام. اسکرینشاتهای مرجع در تیکت این کار هست.
|
||||
7. **صف «جلسات بدون نوبت»** — جلساتی که `status = planned` و `due_at` گذشته یا نزدیک است.
|
||||
از هر ردیف مستقیم به انتخاب اسلات پیشنهادی.
|
||||
|
||||
تایمر فقط نمایشی است. زمان معتبر همان `started_at` و `finished_at` سرور است.
|
||||
|
||||
---
|
||||
|
||||
## ترتیب اجرا
|
||||
|
||||
```
|
||||
۱ → ۲ → ۳ → ۶ → ۷ → ۴ → ۵ → ۸ → ۹ → ۱۰
|
||||
```
|
||||
|
||||
وظیفه ۵ (کلید اسلات) روی پرترافیکترین جدول سیستم است. بعد از وظیفه ۴ انجامش بده و
|
||||
قبل و بعدش تست رگرسیون رزرو را کامل اجرا کن.
|
||||
|
||||
## تست
|
||||
|
||||
- unit برای `CategoryClosureResolver` در حالت برگیابی نواحی
|
||||
- unit برای محاسبه `due_at` با گامهای نامساوی (سناریوی بوتاکس: ۰، ۱۵، ۳۰، ۳۰)
|
||||
- unit برای `FieldSchemaValidator` با کلید ناشناخته و مقدار خارج از `options`
|
||||
- functional: پزشک ناظر مشترک بین دو دستگاه، دو رزرو همساعت، هر دو باید موفق شوند
|
||||
- functional: اتاق با `capacity = 3`، سه رزرو همساعت، هر سه باید موفق شوند
|
||||
- functional: چرخه کامل یک دوره سهجلسهای شامل یک no-show
|
||||
- رگرسیون: رزرو بدون منبع همچنان با کلید پزشک از دوبار رزرو جلوگیری کند
|
||||
|
||||
## خارج از دامنه فاز اول
|
||||
|
||||
- seeder هوشمند با AI برای پیشنهاد دستهبندی و منابع
|
||||
- گزارشهای تحلیلی روی `parameters` (کوئری JSON بدون ایندکس)
|
||||
- حوزه فعالیت دوم غیر از زیبایی
|
||||
- داشبورد اختصاصی بهازای هر تخصص
|
||||
- قیمت پکیج و قفل قیمت
|
||||
|
||||
## مستندسازی
|
||||
|
||||
طبق قانون ثابت این پروژه، `docs/api/*` در همان session بهروز شود.
|
||||
اگر تصمیمی در حین اجرا عوض شد، ADR مربوطه بهروز شود یا ADR جدید نوشته شود.
|
||||
Reference in New Issue
Block a user