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