- 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
18 KiB
گزارش وضعیت فعلی سیستم در برابر مستند «موتور نوبتدهی Clinic Pro»
مرجع: 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،
سند کامل: docs/architecture/tenancy.md.
| سطح مستند | معادل امروز |
|---|---|
| کلینیک (Tenant) | ✅ clinic یا doctor (مطب شخصی) |
| شعبه (Branch) | ⚠️ نیمبند — DoctorAddress نقش «محل» را بازی میکند و در location_id هر شیفت مینشیند |
| اتاق (Room) | ❌ وجود ندارد |
Clinic هیچ فیلد شعبهای ندارد (src/Clinic/Entity/Clinic.php).
ساعت کاری شعبه هم وجود ندارد؛ ساعت کاری فقط روی برنامهٔ پزشک است.
۲-۲ تعریف خدمات
ServiceSection (بخش) → ServiceItem (سرویس)، هر دو tenant-دار.
src/ClinicService/Entity/ServiceItem.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 — نام، سمت، فعال/غیرفعال،
اتصال اختیاری به User. تقویم ندارد، ظرفیت ندارد، مهارت ندارد.
| مستند | وضعیت |
|---|---|
resource_type تعریفشده توسط کلینیک |
❌ |
| منبع با ظرفیت همزمان | ❌ |
مهارتها (skill / resource_skill) |
❌ |
| استخر منابع | ❌ |
| ویژگی آزاد (جنسیت، مدل دستگاه، طبقه) | ❌ |
| زمان آمادهسازی/تمیزکاری per منبع | ❌ (فقط buffer_minutes سراسری روی برنامه) |
| نیازمندی منبع per بخش | ❌ |
| قید همجنس بودن | ❌ |
۲-۴ تقویم و اسلات
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 انجام میشود و از هفت لایهٔ مستند، چهار لایه را دارد:
ساعت کاری شعبه ❌ (ساعت کاری فقط روی برنامه پزشک است)
– شیفت منبع ❌
– تعطیلات رسمی کشور ❌ (جدول تعطیلات ملی نداریم؛ Holiday دستی است)
– مرخصی/غیبت ⚠️ فقط از راه Holiday و DateOverride پزشک
– سرویس دورهای دستگاه ❌
– نوبتهای ثبتشده ✅ isSlotTaken / findBusyIntervals
– رزروهای موقت ✅ pending با expires_at
– آمادهسازی و تمیزکاری ⚠️ فقط buffer_minutes ثابت
۲-۵ نوبتدهی سرویسی که امروز داریم
جریان فعلی (همانی که کاربر اشاره کرد):
GET /api/v1/appointment-booking-services/{doctorUuid}→booking_mode+ سرویسهایbookableGET /api/v1/appointment-service-slots?doctor_uuid&date&service_item_uuids[]&durations[]→SlotCalculatorService::getServiceStartTimes()— جمع سادهٔ مدت سرویسها، سپس پر کردن فضای خالی هر شیفت باduration + bufferPOST /api/v1/appointment→ یک ردیفappointmentsباslot_start/slot_endوappointment_service_items(ManyToMany چند سرویس)
محدودیتهای ساختاری این جریان نسبت به مستند:
$totalMinutes += $durationبرای هر سرویس (AppointmentController.php:236) — دقیقاً همان «فرمول قدیمی» که مستند بند ۵ ردش میکند: آمادهسازی چند بار حساب میشود.- زمان اشغال یک بلوک پیوسته است؛ اپراتور در زمان انتظار آزاد نمیشود (بند ۷).
- تنها منبعی که تداخلش بررسی میشود پزشک است؛ اگر دو سرویس همزمان به یک پرسنل یا یک دستگاه نیاز داشته باشند، سیستم متوجه نمیشود.
۲-۵ب حالت سرویسی نیمهکاره است — پنج شکاف در چرخهٔ عمر نوبت
مسیر رزرو کار میکند، ولی بقیهٔ چرخهٔ عمر نه. اینها پیشنیاز موتور چندمنبعیاند و تسکهای ۰۰ و ۰۰ب میبندندشان:
| # | شکاف | محل |
|---|---|---|
| ۱ | PATCH /appointment/{uuid} مدت دلخواه میپذیرد؛ بافر را نادیده میگیرد؛ فقط service_item_uuid تکی را بهروز میکند در حالی که service_items (ManyToMany) دستنخورده میماند |
AppointmentController.php:1077 |
| ۲ | AppointmentEditPage سه فیلد آزاد date/start/end دارد و هیچ ServiceSlotPicker ای ندارد — منشی نوبت ۴۵ دقیقهای را ۲۰ دقیقه میکند و سیستم قبول میکند |
AppointmentEditPage.tsx:74 |
| ۳ | نوبت رزرو (is_reserve) صریحاً از حالت سرویسی حذف شده (serviceMode = mode === 'service' && !isReserve) و مسیر تبدیل رزرو به نوبت سرویسی وجود ندارد |
NewAppointmentDrawer.tsx:72 |
| ۴ | سایت عمومی چهار رنگ 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:
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):
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* هستند). پس:
Doctorرا به یکResourceتبدیل نمیکنیم، بلکه کنارش میگذاریم. پزشک منبعی باresource_type = doctorمیشود که به رکوردDoctorلینک دارد.appointments.doctor_idسر جایش میماند تا API عمومی نشکند.- حالت سوم نوبتدهی اضافه میشود:
WeeklySchedule.meta.booking_mode = resourceکنارslotوserviceموجود. دو حالت قبلی دستنخورده کار میکنند و مسیر مهاجرت داوطلبانه است، نه اجباری. resource_occupancyتنها مرجع اشغال میشود ولیactive_slot_keyفعلی هم تا حذف کامل حالتslotمیماند (دو تور ایمنی، نه صفر).- قوانین روی الگوی
DiscountRuleساخته میشوند — enum بسته + priority + بازهٔ اعتبار، نه DSL آزاد. همانطور که مستند بند ۸ اصرار دارد. - همهٔ جدولهای جدید از روز اول
TenantOwnedTraitمیگیرند، وگرنهTenantSchemaCoverageTestقرمز میشود. - MariaDB محدودیت بازهای (
EXCLUDE) ندارد. جلوگیری از تداخل با کلید یکتای «سطل زمانی» (resource_id + slot_bucket) انجام میشود — جزئیات در تسک ۰۷.
۵. ترتیب اجرا
۰۰ تکمیل سرویسی (clinicpro) ── ۰۰ب سازگارسازی سایت ← فاز ۰، پیشنیاز بقیه
│
├─ ۰۱ شعبه/اتاق ─┬─ ۰۲ منابع و مهارت ── ۰۳ تقویم منبع ─┐
│ └─ ۰۴ کاتالوگ v2 ── ۰۵ بخشهای نوبت ─┴─ ۰۶ جستجوی وقت ── ۰۷ رزرو و ثبت
│ │
│ ۰۸ قیمتگذاری و snapshot ───────────────┘
│ │
│ ۰۹ موتور قوانین ── ۱۰ فرم و sandbox قانون
│ │
│ ۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدمحضور/انتظار
│ │
└──────────────────────── ۱۴ رویدادها و گزارش بهرهوری
فاز ۰ اختیاری نیست. اگر حالت resource روی حالت service نیمهکاره ساخته شود، هر
باگ موجود سرویسی به موتور جدید ارث میرسد و تشخیص منبعش غیرممکن میشود.
۶. سه قاعدهٔ حاکم بر همهٔ تسکها
| سند | چه میگوید |
|---|---|
| _shared/red-lines.md | منطق اسلاتی به هیچ عنوان دستکاری نمیشود · فهرست کامل فایلهای قفلشده · تست --group=slot-mode-frozen |
| _shared/ui-conventions.md | هر صفحه یا بخش جدید عیناً با دیزاینسیستم موجود — توکنها، کامپوننتهای ui/، پنج قاعدهٔ غیرقابلمذاکره |
| _shared/definition-of-done.md | هیچ تسکی بدون تکمیل چکلیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |