# گزارش وضعیت فعلی سیستم در برابر مستند «موتور نوبت‌دهی 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) | هیچ تسکی بدون تکمیل چک‌لیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |
