- Created checklist for task 11: Package and Credit Ledger - Created checklist for task 12: Treatment Course - Created checklist for task 13: Cancellation Policy, No-Show, and Waitlist - Created checklist for task 14: Domain Events and Utilization Reports
268 lines
18 KiB
Markdown
268 lines
18 KiB
Markdown
# گزارش وضعیت فعلی سیستم در برابر مستند «موتور نوبتدهی Clinic Pro»
|
||
|
||
مرجع: [clinic-pro-mostanad-sade.md](../clinic-pro-mostanad-sade.md)
|
||
دامنه بررسی: `clinicpro/src/**` (Symfony 7.4) + `clinicpro/assets/admin/**` (React SPA)
|
||
|
||
---
|
||
|
||
## ۱. خلاصه اجرایی
|
||
|
||
سیستم فعلی یک موتور نوبتدهی **تکمنبعی (فقط پزشک)** است که اخیراً یک حالت
|
||
«نوبتدهی سرویسی» هم گرفته: `WeeklySchedule.meta.booking_mode = service` باعث میشود
|
||
طول نوبت از `ServiceItem.duration_minutes` گرفته شود بهجای اسلات ثابت.
|
||
|
||
این با مستند در جهت درست است، ولی فقط **یک لایه از هفت لایه** مستند را پوشش میدهد.
|
||
سه ستون اصلی مستند اصلاً وجود ندارند:
|
||
|
||
| ستون مستند | وضعیت |
|
||
|---|---|
|
||
| تقویم مال منبع است نه پزشک (بند ۲-۱، ۶) | ❌ وجود ندارد — تقویم فقط `(doctor, clinic)` است |
|
||
| نوبت از چند بخش تشکیل شده (بند ۷) | ❌ وجود ندارد — نوبت یک `slot_start/slot_end` پیوسته است |
|
||
| موتور قوانین ششدستهای (بند ۸) | ⚠️ فقط یک دسته (قیمت) به شکل `DiscountRule` |
|
||
|
||
نکته مثبت و مهم: **قانون سوم مستند («جلوگیری از رزرو تکراری کار دیتابیس است»)
|
||
از قبل رعایت شده** — `Appointment.active_slot_key` یک ستون `UNIQUE` است که فقط در
|
||
وضعیتهای اشغالکننده مقدار میگیرد. همان الگو باید به `resource_occupancy` تعمیم پیدا کند.
|
||
|
||
---
|
||
|
||
## ۲. آنچه امروز داریم (کد واقعی)
|
||
|
||
### ۲-۱ محیط (tenant)
|
||
|
||
`(entity_type, entity_id)` روی ۲۰ جدول، با `entity_type ∈ {doctor, clinic}`
|
||
— [src/Shared/Tenant/TenantOwnedTrait.php](../../../src/Shared/Tenant/TenantOwnedTrait.php)،
|
||
سند کامل: [docs/architecture/tenancy.md](../../architecture/tenancy.md).
|
||
|
||
| سطح مستند | معادل امروز |
|
||
|---|---|
|
||
| کلینیک (Tenant) | ✅ `clinic` یا `doctor` (مطب شخصی) |
|
||
| شعبه (Branch) | ⚠️ نیمبند — `DoctorAddress` نقش «محل» را بازی میکند و در `location_id` هر شیفت مینشیند |
|
||
| اتاق (Room) | ❌ وجود ندارد |
|
||
|
||
`Clinic` هیچ فیلد شعبهای ندارد ([src/Clinic/Entity/Clinic.php](../../../src/Clinic/Entity/Clinic.php)).
|
||
ساعت کاری شعبه هم وجود ندارد؛ ساعت کاری فقط روی برنامهٔ پزشک است.
|
||
|
||
### ۲-۲ تعریف خدمات
|
||
|
||
`ServiceSection` (بخش) → `ServiceItem` (سرویس)، هر دو tenant-دار.
|
||
[src/ClinicService/Entity/ServiceItem.php](../../../src/ClinicService/Entity/ServiceItem.php):
|
||
|
||
```php
|
||
private int $priceRials = 0;
|
||
private ?int $durationMinutes = null; // مدت، تخت — بدون تفکیک بخش
|
||
private bool $bookable = false; // نمایش در نوبتدهی
|
||
private Collection $staffMembers; // ManyToMany به ClinicStaff
|
||
private ?int $inventoryPackageId = null;
|
||
private Collection $consumables; // ServiceItemConsumable
|
||
```
|
||
|
||
+ `Tariff` (قیمت سالانه per سرویس) و `TenantServiceCoverage` (پوشش بیمه).
|
||
|
||
| مستند | وضعیت |
|
||
|---|---|
|
||
| دستهبندی درختی خدمات | ⚠️ `ServiceSection` تکسطحی است، درختی نیست |
|
||
| گروه آیتم با حداقل/حداکثر انتخاب | ❌ |
|
||
| آیتم با «زمان تنها» و «زمان اضافه» | ❌ — فقط یک `duration_minutes` |
|
||
| ناسازگاری / پیشنیاز بین آیتمها | ❌ |
|
||
| الگوی بخشهای نوبت (segment template) | ❌ |
|
||
| قیمت اختصاصی شعبه | ❌ |
|
||
| تکجلسه یا دورهای | ❌ |
|
||
|
||
### ۲-۳ منابع
|
||
|
||
تنها «منبع» مدلشده، پرسنل است:
|
||
[src/Staff/Entity/ClinicStaff.php](../../../src/Staff/Entity/ClinicStaff.php) — نام، سمت، فعال/غیرفعال،
|
||
اتصال اختیاری به `User`. تقویم ندارد، ظرفیت ندارد، مهارت ندارد.
|
||
|
||
| مستند | وضعیت |
|
||
|---|---|
|
||
| `resource_type` تعریفشده توسط کلینیک | ❌ |
|
||
| منبع با ظرفیت همزمان | ❌ |
|
||
| مهارتها (`skill` / `resource_skill`) | ❌ |
|
||
| استخر منابع | ❌ |
|
||
| ویژگی آزاد (جنسیت، مدل دستگاه، طبقه) | ❌ |
|
||
| زمان آمادهسازی/تمیزکاری per منبع | ❌ (فقط `buffer_minutes` سراسری روی برنامه) |
|
||
| نیازمندی منبع per بخش | ❌ |
|
||
| قید همجنس بودن | ❌ |
|
||
|
||
### ۲-۴ تقویم و اسلات
|
||
|
||
[src/Appointment/Entity/WeeklySchedule.php](../../../src/Appointment/Entity/WeeklySchedule.php):
|
||
JSON هفتگی per `(doctor, clinic)`، هر روز چند `session` با
|
||
`start_time/end_time/duration_per_patient/has_rest/patient_limit/location_id`.
|
||
`meta`: `online_booking_enabled`, `booking_window_value|unit`, `booking_mode`, `buffer_minutes`.
|
||
|
||
`DateOverride` (روز خاص)، `Holiday` (بازه تعطیلی per پزشک/محیط).
|
||
|
||
کسر لایهها در [SlotCalculatorService](../../../src/Appointment/Service/SlotCalculatorService.php)
|
||
انجام میشود و از هفت لایهٔ مستند، چهار لایه را دارد:
|
||
|
||
```
|
||
ساعت کاری شعبه ❌ (ساعت کاری فقط روی برنامه پزشک است)
|
||
– شیفت منبع ❌
|
||
– تعطیلات رسمی کشور ❌ (جدول تعطیلات ملی نداریم؛ Holiday دستی است)
|
||
– مرخصی/غیبت ⚠️ فقط از راه Holiday و DateOverride پزشک
|
||
– سرویس دورهای دستگاه ❌
|
||
– نوبتهای ثبتشده ✅ isSlotTaken / findBusyIntervals
|
||
– رزروهای موقت ✅ pending با expires_at
|
||
– آمادهسازی و تمیزکاری ⚠️ فقط buffer_minutes ثابت
|
||
```
|
||
|
||
### ۲-۵ نوبتدهی سرویسی که امروز داریم
|
||
|
||
جریان فعلی (همانی که کاربر اشاره کرد):
|
||
|
||
1. `GET /api/v1/appointment-booking-services/{doctorUuid}` → `booking_mode` + سرویسهای `bookable`
|
||
2. `GET /api/v1/appointment-service-slots?doctor_uuid&date&service_item_uuids[]&durations[]`
|
||
→ `SlotCalculatorService::getServiceStartTimes()` — **جمع سادهٔ مدت سرویسها**، سپس پر کردن
|
||
فضای خالی هر شیفت با `duration + buffer`
|
||
3. `POST /api/v1/appointment` → یک ردیف `appointments` با `slot_start/slot_end` و
|
||
`appointment_service_items` (ManyToMany چند سرویس)
|
||
|
||
محدودیتهای ساختاری این جریان نسبت به مستند:
|
||
|
||
- **`$totalMinutes += $duration` برای هر سرویس** ([AppointmentController.php:236](../../../src/Appointment/Controller/AppointmentController.php)) —
|
||
دقیقاً همان «فرمول قدیمی» که مستند بند ۵ ردش میکند: آمادهسازی چند بار حساب میشود.
|
||
- زمان اشغال یک بلوک پیوسته است؛ اپراتور در زمان انتظار آزاد نمیشود (بند ۷).
|
||
- تنها منبعی که تداخلش بررسی میشود پزشک است؛ اگر دو سرویس همزمان به یک پرسنل
|
||
یا یک دستگاه نیاز داشته باشند، سیستم متوجه نمیشود.
|
||
|
||
### ۲-۵ب حالت سرویسی **نیمهکاره** است — پنج شکاف در چرخهٔ عمر نوبت
|
||
|
||
مسیر **رزرو** کار میکند، ولی بقیهٔ چرخهٔ عمر نه. اینها پیشنیاز موتور چندمنبعیاند و
|
||
تسکهای [۰۰](task-00-service-mode-completion/) و [۰۰ب](task-00b-nobat724-service-mode/)
|
||
میبندندشان:
|
||
|
||
| # | شکاف | محل |
|
||
|---|---|---|
|
||
| ۱ | `PATCH /appointment/{uuid}` مدت دلخواه میپذیرد؛ بافر را نادیده میگیرد؛ فقط `service_item_uuid` تکی را بهروز میکند در حالی که `service_items` (ManyToMany) دستنخورده میماند | [AppointmentController.php:1077](../../../src/Appointment/Controller/AppointmentController.php) |
|
||
| ۲ | `AppointmentEditPage` سه فیلد آزاد `date`/`start`/`end` دارد و هیچ `ServiceSlotPicker` ای ندارد — منشی نوبت ۴۵ دقیقهای را ۲۰ دقیقه میکند و سیستم قبول میکند | [AppointmentEditPage.tsx:74](../../../assets/admin/pages/AppointmentEditPage.tsx) |
|
||
| ۳ | نوبت رزرو (`is_reserve`) صریحاً از حالت سرویسی حذف شده (`serviceMode = mode === 'service' && !isReserve`) و مسیر تبدیل رزرو به نوبت سرویسی وجود ندارد | [NewAppointmentDrawer.tsx:72](../../../assets/admin/components/NewAppointmentDrawer.tsx) |
|
||
| ۴ | سایت عمومی چهار رنگ hard-code در مرحلهٔ انتخاب سرویس دارد (`#5559CE`, `#3B3B3B`, `#7A7A7A`, `bg-white`) و در دارکمود میشکند؛ همچنین مدت را **موازی با بکاند** حساب میکند | `nobat724_front/components/appointment/service/index.js` |
|
||
| ۵ | پنل کاربر سایت نام سرویس و مدت نوبت را نشان نمیدهد و مسیر جابهجایی سرویسآگاه ندارد | `nobat724_front/.../turns/Card.js` · `isTurnsDetails/*` |
|
||
|
||
نکتهٔ ۴ دو مشکل در یک فایل است: انحراف از دیزاینسیستم، و منبع دوم حقیقت برای مدت.
|
||
دومی مهمتر است — وقتی تسک ۰۴ فرمول را به «زمان تنها / زمان اضافه» عوض کند، سایت عدد
|
||
قدیمی نشان میدهد و بیمار مدتی میبیند که با مدت واقعی نوبتش نمیخواند.
|
||
|
||
### ۲-۶ ثبت نوبت و همزمانی
|
||
|
||
[src/Appointment/Entity/Appointment.php](../../../src/Appointment/Entity/Appointment.php):
|
||
|
||
```php
|
||
public const PAYMENT_TTL = 900; // رزرو موقت ۱۵ دقیقهای
|
||
#[ORM\Column(name:'active_slot_key', unique:true, nullable:true)]
|
||
private ?string $activeSlotKey = null; // "{doctorId}:{slotStart}" یا NULL
|
||
#[ORM\Version] private int $version = 1; // optimistic locking
|
||
```
|
||
|
||
✅ سه مرحله جستجو → رزرو موقت (`pending` + `expires_at`) → ثبت نهایی (`confirmed`) از قبل هست،
|
||
و یکتایی در سطح دیتابیس تضمین میشود — نه در کد.
|
||
❌ ولی کلید فقط `doctor + slot_start` است. با چند منبع، به یک جدول `resource_occupancy`
|
||
با محدودیت بازهای نیاز است.
|
||
|
||
وضعیتها: `pending, confirmed, completed, cancelled_by_doctor, cancelled_by_user, expired,
|
||
no_show, following_up, salon` + `AppointmentEvent` برای تاریخچه. تقریباً کامل؛ `rescheduled` ندارد.
|
||
|
||
### ۲-۷ قیمت
|
||
|
||
`ServiceItem.price_rials` → `Tariff` (سالانه) → `TenantServiceCoverage`/`TenantInsurance` (بیمه)
|
||
→ `DiscountRule` + `DiscountEngine` → `Invoice`/`InvoiceItem` → `Payment`.
|
||
بیعانه هم روی نوبت هست (`deposit_required`, `deposit_amount_rials`).
|
||
|
||
| مستند | وضعیت |
|
||
|---|---|
|
||
| لیست قیمت با بازهٔ تاریخ | ⚠️ `Tariff` فقط «سال» دارد، بازهٔ دقیق ندارد |
|
||
| قیمت per شعبه | ❌ |
|
||
| snapshot فاکتور روی نوبت | ⚠️ `visit_price_rials` تکعدد است، تفکیکشده نیست |
|
||
| پکیج و دفتر اعتبار جلسات | ❌ |
|
||
| بیعانه | ✅ |
|
||
|
||
### ۲-۸ قوانین
|
||
|
||
تنها موتور قانونِ موجود `DiscountRule` است
|
||
([src/Discount/Entity/DiscountRule.php](../../../src/Discount/Entity/DiscountRule.php)):
|
||
`type` از یک enum بسته، `priority`، `combinable`، `valid_from/valid_to`، tenant-دار.
|
||
|
||
این دقیقاً الگوی درستی است که مستند میخواهد (شرط از فهرست بسته، نه کد دلخواه) — ولی
|
||
فقط برای دستهٔ «قیمت». پنج دستهٔ دیگر (انتخاب، صلاحیت بیمار، منبع، زمان، فاصله زمانی)
|
||
و همچنین نسخهبندی و محیط آزمایش وجود ندارند.
|
||
|
||
### ۲-۹ دوره درمان
|
||
|
||
❌ کامل غایب. نه `course_protocol`، نه `treatment_course`، نه `course_session`.
|
||
`PatientSession` وجود دارد ولی «مراجعهٔ انجامشده» است، نه جلسهٔ برنامهریزیشدهٔ یک دوره.
|
||
|
||
---
|
||
|
||
## ۳. جدول شکاف (خلاصه)
|
||
|
||
| بخش مستند | دارد | ندارد | تسک |
|
||
|---|---|---|---|
|
||
| — نوبتدهی سرویسی موجود | مسیر رزرو (پنل + سایت) | ویرایش، جابهجایی، رزرو، پنل بیمار، دیزاینسیستم سایت | **۰۰، ۰۰ب** |
|
||
| ۴ کلینیک/شعبه/اتاق | tenant دوسطحی | Branch، Room، ساعت کاری شعبه | ۰۱ |
|
||
| ۵ تعریف خدمات | سرویس، قیمت، مدت، بیمه | گروه آیتم، دو نوع زمان، ناسازگاری، override شعبه | ۰۴ |
|
||
| ۶ منابع | پرسنل بدون تقویم | نوع منبع، ظرفیت، مهارت، استخر، نیازمندی | ۰۲، ۰۳ |
|
||
| ۷ بخشهای نوبت | — | کل بخش | ۰۵ |
|
||
| ۸ قوانین | فقط تخفیف | ۵ دستهٔ دیگر، نسخهبندی، sandbox | ۰۹، ۱۰ |
|
||
| ۹ تقویم | برنامهٔ پزشک، override، تعطیلی | تقویم منبع، تعطیلات ملی، سرویس دستگاه | ۰۳ |
|
||
| ۱۰ جستجوی وقت | تکمنبعی و پیوسته | چندمنبعی، چندبخشی، کش، استراتژی انتخاب | ۰۶ |
|
||
| ۱۱ ثبت نوبت | سهمرحلهای + یکتایی DB | resource_occupancy، قفل چندمنبعی | ۰۷ |
|
||
| ۱۲ قیمت | تعرفه، بیمه، تخفیف، بیعانه | price_list بازهدار، snapshot تفکیکشده، پکیج، دفتر اعتبار | ۰۸، ۱۱ |
|
||
| ۱۳ دوره درمان | — | کل بخش | ۱۲ |
|
||
| ۱۶ رویدادها | AppointmentEvent | bus عمومی دامنه | ۱۴ |
|
||
|
||
---
|
||
|
||
## ۴. تصمیم معماری پیشنهادی: توسعه، نه بازنویسی
|
||
|
||
مستند PostgreSQL و یک سیستم نو فرض کرده. پروژه روی **MariaDB 11.8 + Doctrine ORM 3.6**
|
||
است و یک جریان نوبتدهی زنده دارد (سایت عمومی `nobat724_front` و اپ `clinic-pro-tauri`
|
||
هر دو مصرفکنندهٔ `/api/v1/appointment*` هستند). پس:
|
||
|
||
1. **`Doctor` را به یک `Resource` تبدیل نمیکنیم، بلکه کنارش میگذاریم.**
|
||
پزشک منبعی با `resource_type = doctor` میشود که به رکورد `Doctor` لینک دارد.
|
||
`appointments.doctor_id` سر جایش میماند تا API عمومی نشکند.
|
||
2. **حالت سوم نوبتدهی اضافه میشود:** `WeeklySchedule.meta.booking_mode = resource`
|
||
کنار `slot` و `service` موجود. دو حالت قبلی دستنخورده کار میکنند و مسیر مهاجرت
|
||
داوطلبانه است، نه اجباری.
|
||
3. **`resource_occupancy` تنها مرجع اشغال میشود** ولی `active_slot_key` فعلی هم تا
|
||
حذف کامل حالت `slot` میماند (دو تور ایمنی، نه صفر).
|
||
4. **قوانین روی الگوی `DiscountRule` ساخته میشوند** — enum بسته + priority + بازهٔ اعتبار،
|
||
نه DSL آزاد. همانطور که مستند بند ۸ اصرار دارد.
|
||
5. **همهٔ جدولهای جدید از روز اول `TenantOwnedTrait` میگیرند**، وگرنه
|
||
`TenantSchemaCoverageTest` قرمز میشود.
|
||
6. **MariaDB محدودیت بازهای (`EXCLUDE`) ندارد.** جلوگیری از تداخل با
|
||
کلید یکتای «سطل زمانی» (`resource_id + slot_bucket`) انجام میشود — جزئیات در تسک ۰۷.
|
||
|
||
---
|
||
|
||
## ۵. ترتیب اجرا
|
||
|
||
```
|
||
۰۰ تکمیل سرویسی (clinicpro) ── ۰۰ب سازگارسازی سایت ← فاز ۰، پیشنیاز بقیه
|
||
│
|
||
├─ ۰۱ شعبه/اتاق ─┬─ ۰۲ منابع و مهارت ── ۰۳ تقویم منبع ─┐
|
||
│ └─ ۰۴ کاتالوگ v2 ── ۰۵ بخشهای نوبت ─┴─ ۰۶ جستجوی وقت ── ۰۷ رزرو و ثبت
|
||
│ │
|
||
│ ۰۸ قیمتگذاری و snapshot ───────────────┘
|
||
│ │
|
||
│ ۰۹ موتور قوانین ── ۱۰ فرم و sandbox قانون
|
||
│ │
|
||
│ ۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدمحضور/انتظار
|
||
│ │
|
||
└──────────────────────── ۱۴ رویدادها و گزارش بهرهوری
|
||
```
|
||
|
||
**فاز ۰ اختیاری نیست.** اگر حالت `resource` روی حالت `service` نیمهکاره ساخته شود، هر
|
||
باگ موجود سرویسی به موتور جدید ارث میرسد و تشخیص منبعش غیرممکن میشود.
|
||
|
||
## ۶. سه قاعدهٔ حاکم بر همهٔ تسکها
|
||
|
||
| سند | چه میگوید |
|
||
|---|---|
|
||
| [_shared/red-lines.md](_shared/red-lines.md) | منطق اسلاتی به هیچ عنوان دستکاری نمیشود · فهرست کامل فایلهای قفلشده · تست `--group=slot-mode-frozen` |
|
||
| [_shared/ui-conventions.md](_shared/ui-conventions.md) | هر صفحه یا بخش جدید عیناً با دیزاینسیستم موجود — توکنها، کامپوننتهای `ui/`، پنج قاعدهٔ غیرقابلمذاکره |
|
||
| [_shared/definition-of-done.md](_shared/definition-of-done.md) | هیچ تسکی بدون تکمیل چکلیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |
|