# ماژول دندانپزشکی ClinicPro

> **دامنه:** `clinicpro` — بک‌اند Symfony و پنل ادمین React
> **وضعیت:** سند طراحی. کد نوشته نشده.
> **تاریخ:** 2026-08
> **خارج از دامنه:** بیمه. فقط نقطهٔ اتصال رزرو می‌شود.

این سند حاصل یک جلسهٔ تصمیم‌گیری روی کد واقعی ریپو است.
هر بند «چه» را می‌گوید و «چرا» را کنارش.
تصمیم‌ها در بخش ۳ فهرست‌اند و بقیهٔ سند نتیجهٔ آن‌هاست.

منبع اولیه‌اش سند `clinicpro-dental-module-technical-spec.md` در ریشهٔ workspace بود.
آن سند بدون دسترسی به کد نوشته شده بود و بخش بزرگی از چیزی که پیشنهاد داده، از قبل ساخته شده است.
بخش ۲ تفاوت‌ها را نشان می‌دهد.

---

## ۱. مسئله

خواسته یک جمله است.
وقتی یک کلینیک حوزهٔ فعالیت «دندانپزشکی» را انتخاب می‌کند، باید دسته‌بندی‌ها و تنظیمات دندانپزشکی برایش ساخته شود و داشبورد مخصوص خودش را ببیند.

امروز انتخاب حوزهٔ فعالیت فقط یک کلید خارجی روی جدول کلینیک می‌نویسد.
هیچ دسته‌ای، هیچ خدمتی، هیچ تنظیمی ساخته نمی‌شود.
یعنی کلینیک بعد از انتخاب حوزه، دقیقاً همان‌قدر خالی است که قبلش بود.

سه تکهٔ خواسته:

۱. با انتخاب دندانپزشکی، کاتالوگ خدمات دندانپزشکی ساخته شود.
۲. دندانپزشک بتواند وضعیت دندان‌های بیمار را ثبت و ببیند.
۳. مدیر و پزشک و پذیرش، شاخص‌های دندانپزشکی را در داشبورد ببینند.

---

## ۲. آنچه امروز واقعاً هست

این بخش از روی کد نوشته شده، نه از روی مستندات.

### ۲.۱ حوزهٔ فعالیت — هست، ولی فقط برچسب است

| چیز | مسیر |
|---|---|
| موجودیت | `src/PracticeDomain/Entity/PracticeDomain.php` |
| کنترلر | `src/PracticeDomain/Controller/PracticeDomainController.php` |
| مستندات | `docs/api/practice-domain.md` |
| اتصال به کلینیک | `src/Clinic/Entity/Clinic.php` ستون `practice_domain_id` |
| صفحهٔ پنل | `assets/admin/pages/PracticeDomainSettingsPage.tsx` |

جدول سراسری است و در `GlobalTables::ENTITIES` ثبت شده.
انتخاب حوزه از `PATCH` روی کلینیک با کلید `practice_domain_uuid` انجام می‌شود.

**نکتهٔ مهم:** `Doctor` این فیلد را ندارد.
ولی مالکیت داده در کل پروژه دو حالته است، `entity_type` برابر `doctor` یا `clinic`.
یعنی مطب تک‌پزشک امروز اصلاً نمی‌تواند حوزهٔ فعالیت انتخاب کند.
این یک نقص عمومی است و فقط به دندانپزشکی مربوط نیست؛ کلینیک زیبایی تک‌پزشکه هم همین مشکل را دارد.

### ۲.۲ رفتار مخصوص هر حوزه — الگویش ساخته شده

| چیز | مسیر |
|---|---|
| اینترفیس | `src/Treatment/Workflow/TreatmentWorkflow.php` |
| رجیستری | `src/Treatment/Workflow/TreatmentWorkflowRegistry.php` |
| پیش‌فرض | `src/Treatment/Workflow/DefaultTreatmentWorkflow.php` |
| نمونهٔ حوزه‌ای | `src/Treatment/Workflow/LaserTreatmentWorkflow.php` |
| تصمیم ثبت‌شده | `docs/adr/0005-treatment-workflows-are-tagged-services.md` |

سرویس‌ها با تگ `app.treatment_workflow` ثبت می‌شوند و رجیستری بر اساس کد حوزه انتخاب می‌کند.
افزودن دندانپزشکی یعنی یک کلاس تازه، نه دست‌بردن در مسیر رزرو.

### ۲.۳ کاتالوگ خدمات — کامل است

| موجودیت | نقش |
|---|---|
| `ServiceSection` | بخش سازمانی کلینیک |
| `CatalogCategory` | تاکسونومی درختی خدمات تا عمق ۴ |
| `ServiceItem` | خدمت با قیمت، مدت، تعداد جلسه، دستهٔ کاتالوگ |
| `ItemGroup` و `ItemGroupMember` | گروه‌بندی آیتم |
| `ServiceItemRelation` | رابطهٔ بین خدمات |
| `ServiceBranchOverride` | بازنویسی قیمت در شعبه |

مسیرها در `src/ClinicService/`.
اندپوینت‌های موجود در `docs/api/clinic-services.md`.
صفحات پنل: `ClinicServicesPage.tsx` و `CatalogCategoriesPage.tsx`.

### ۲.۴ یونیت و صندلی — لازم نیست ساخته شود

سند اولیه پیشنهاد جدول `chairs` و ستون `chairId` روی نوبت داده بود.
هر دو از قبل هستند و بهترند:

| چیز | مسیر |
|---|---|
| نوع منبع با `CODE_ROOM` | `src/Resource/Entity/ResourceType.php` |
| خود منبع | `src/Resource/Entity/ClinicResource.php` |
| تقویم و استثنا | `src/Resource/Entity/ResourceCalendar.php` و `ResourceException.php` |
| اتصال به نوبت | `src/Appointment/Entity/Appointment.php` ستون `resource_id` |
| اشغال واقعی | `resource_occupancy` |
| تصمیم ثبت‌شده | `docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md` |

منبع، ظرفیت و زمان آماده‌سازی و زمان تمیزکاری هم دارد.
یعنی «۱۵ دقیقه بین دو بیمار برای ضدعفونی یونیت» بدون کد جدید قابل تنظیم است.

### ۲.۵ دوره درمان و جلسه — هست، ولی معنایش با دندانپزشکی یکی نیست

| موجودیت | معنی امروز |
|---|---|
| `TreatmentProtocol` | قالب دوره‌ای یک خدمت، تعریف‌شده توسط مدیر |
| `TreatmentCase` | یک بیمار، **یک خدمت**، از اولین رزرو تا پایان دوره |
| `TreatmentSession` | جلسهٔ شماره‌دار همان دوره، بدون هیچ ستون پولی |
| `TreatmentCaseArea` | ناحیهٔ بدن، اسنپ‌شات از `CatalogCategory` |
| `SessionAreaRecord` | آنچه اپراتور روی هر ناحیه انجام داد |
| `PatientSession` | سابقهٔ مالی یک ویزیت انجام‌شده |
| `SessionService` | ردیف خدمت همان ویزیت با قیمت |

واژگان در `CONTEXT.md` تعریف شده و صریحاً «Treatment Plan» را ممنوع کرده، چون بین قالب و دوره ابهام می‌ساخت.

فرق بنیادی: `TreatmentCase` یک خدمت دارد.
طرح درمان دندانپزشکی ده‌ها خدمت روی دندان‌های مختلف دارد که بیمار بخشی را می‌پذیرد.
این دو یکی نیستند و نباید یکی شوند.

### ۲.۶ داشبورد — چهار اندپوینت نقشی

`src/Dashboard/Controller/DashboardController.php` با ۸۰۳ خط و ۱۵ وابستگی، چهار مسیر دارد:

```
GET /api/v1/dashboard/clinic
GET /api/v1/dashboard/doctor
GET /api/v1/dashboard/secretary
GET /api/v1/dashboard/staff
```

هیچ‌کدام مفهوم حوزهٔ فعالیت را نمی‌شناسند.

### ۲.۷ آنچه واقعاً غایب است

فقط چهار چیز:

۱. حوزهٔ فعالیت روی مطب پزشک.
۲. ساخته‌شدن خودکار دادهٔ پیش‌فرض هنگام انتخاب حوزه.
۳. دندان به‌عنوان یک مفهوم — چارت، سطح، هدف‌گیری خدمت روی دندان.
۴. برآورد چندخدمتی و نرخ پذیرش آن.

هر چیز دیگری که سند اولیه پیشنهاد داده بود، از قبل هست.

---

## ۳. تصمیم‌ها

| # | تصمیم | چرا |
|---|---|---|
| ۱ | ماژول داده‌محور است، نه یک دامنهٔ موازی | موتور درمان و منبع و کاتالوگ از قبل هست. دامنهٔ موازی یعنی دو منبع حقیقت برای جلسه و اتاق. |
| ۲ | seed خودکار وقتی کاتالوگ خالی است، وگرنه دکمهٔ صریح با پیش‌نمایش | کلینیک خالی حالت اصلی است. کلینیک پرداده نباید بی‌اجازه ادغام شود، چون بعداً تشخیص «این ردیف را من ساختم یا سیستم» ممکن نیست. |
| ۳ | قالب پیش‌فرض‌ها در کد است، در فایل نسخه‌دار | محتوای محصول است نه دادهٔ کلینیک. باید در git تاریخچه و review و تست داشته باشد. تغییرش نادر است. |
| ۴ | دندان مفهوم مستقل است با شمارهٔ FDI از نوع `smallint` | FDI واقعیت جهانی است نه تاکسونومی هر کلینیک. دندان به‌عنوان دستهٔ کاتالوگ یعنی ۳۲ ردیف تکراری در هر کلینیک، و «سطح دندان» و «وضعیت ماندگار» جایی برای نشستن ندارند. |
| ۵ | فاز ۱ بدون برآورد چندخدمتی جلو می‌رود | زودتر به خروجی قابل استفاده می‌رسیم. هزینه‌اش این است که نرخ پذیرش تا فاز ۴ وجود ندارد و این باید در سند صریح بماند. |
| ۶ | داشبورد از یک registry متریک بر اساس حوزه سرو می‌شود | همان الگوی `TreatmentWorkflow` که پروژه از قبل دارد. شرط‌گذاری داخل کنترلر ۸۰۳ خطی، چهار مسیر کد را دوبرابر می‌کند. |
| ۷ | حوزهٔ فعالیت به سطح محیط منتقل می‌شود، هم کلینیک هم مطب پزشک | مطب تک‌پزشک بخش بزرگ بازار است. این نقص امروز حوزهٔ زیبایی را هم خراب می‌کند، پس اصلاحش عمومی است نه دندانی. |
| ۸ | شاخص‌ها در فاز ۱ زنده محاسبه می‌شوند | هیچ اندازه‌گیری‌ای نشان نداده کند است. جدول تجمیع یعنی job شبانه، مسیر backfill، و منبع حقیقت دومی که می‌تواند واگرا شود. |
| ۹ | چارت هم از خدمت ویزیت پر می‌شود هم دستی ویرایش می‌شود | چارت باید وضعیت‌هایی را نشان دهد که هرگز در این کلینیک فاکتور نشده‌اند، مثل دندان کشیده‌شدهٔ سال‌ها قبل. پس مشتق کامل ممکن نیست. ثبت فقط دستی هم یعنی ثبت دوباره و واگرایی از صورتحساب. |
| ۱۰ | ویژگی‌های دندانی خدمت در جدول توسعهٔ یک‌به‌یک می‌نشیند | `ServiceItem` مشترک همهٔ حوزه‌هاست. ستون دندانی روی آن یعنی هر حوزهٔ بعدی هم ستون خودش را اضافه می‌کند. |
| ۱۱ | بستهٔ seed شامل دسته‌ها، خدمات با قیمت صفر، نوع منبع یونیت و پروتکل‌هاست | دسته بدون خدمت کلینیک را دست‌خالی می‌گذارد. نمونهٔ یونیت ساختن، دادهٔ ساختگی در محیط واقعی جا می‌گذارد. تعداد یونیت را فقط خود مدیر می‌داند. |
| ۱۲ | نصب seed در یک جدول نگاشت در دامنهٔ Dental ثبت می‌شود؛ تغییر حوزه چیزی را حذف نمی‌کند | با تصمیم ۱۰ قرار شد جدول‌های مشترک آلوده نشوند. خدمتی که یک بار در ویزیت استفاده شده اصلاً حذف‌شدنی نیست، کلید خارجی `RESTRICT` است. |
| ۱۳ | چارت یک تب تازه در پروندهٔ بیمار است؛ شاخص‌های دندانی بخشی از همان داشبورد فعلی | دندانپزشک همیشه داخل پروندهٔ بیمار کار می‌کند. منوی جدا یعنی خروج مکرر از کانتکست بیمار. |
| ۱۴ | فهرست خدمات پیش‌نویس است و قبل از merge باید دندانپزشک تأییدش کند | فهرست غلط بدتر از نبودن فهرست است، چون پاک‌کردنش از ده‌ها کلینیک دیگر ممکن نیست. |

---

## ۴. واژگان تازه

این‌ها به `CONTEXT.md` اضافه شده‌اند. اینجا فقط خلاصه است.

**Tooth Chart** — نمای وضعیت جاری همهٔ دندان‌های یک بیمار در یک محیط. اسنپ‌شات است نه تاریخچه.
پرهیز از: dental chart record, odontogram record.

**Tooth Site** — یک دندان مشخص با شمارهٔ FDI، به‌همراه سطوح درگیر. هدف یک خدمت دندانی.
پرهیز از: Treatment Area, tooth record.

**Dental Preset** — بستهٔ دادهٔ پیش‌فرض حوزهٔ دندانپزشکی که با انتخاب حوزه در محیط نصب می‌شود.
پرهیز از: seed, fixture, template.

**Preset Install** — رکورد نصب یک کلید قالب روی یک ردیف واقعی در یک محیط. مبنای idempotency.
پرهیز از: migration, sync record.

---

## ۵. تصمیم‌های ثبت‌شده

| ADR | موضوع |
|---|---|
| `docs/adr/0007-practice-domain-is-tenant-level.md` | حوزهٔ فعالیت مال محیط است نه فقط کلینیک |
| `docs/adr/0008-teeth-are-not-treatment-areas.md` | دندان دستهٔ کاتالوگ نیست |
| `docs/adr/0009-dental-service-attributes-live-in-an-extension-table.md` | ویژگی دندانی خدمت در جدول جدا |
| `docs/adr/0010-domain-metrics-come-from-tagged-providers.md` | شاخص حوزه‌ای از provider تگ‌خورده |

---

## ۶. فازها

| فاز | محتوا | سند |
|---|---|---|
| ۱ | حوزه در سطح محیط، نصب پیش‌فرض‌ها، پروفایل دندانی خدمت | [phase-1-preset.md](phase-1-preset.md) |
| ۲ | چارت دندان، هدف‌گیری دندان روی خدمت ویزیت، پروجکتور | [phase-2-tooth-chart.md](phase-2-tooth-chart.md) |
| ۳ | registry متریک و شاخص‌های دندانپزشکی روی داشبورد | [phase-3-dashboard.md](phase-3-dashboard.md) |
| ۴ | برآورد درمان و نرخ پذیرش | [phase-4-treatment-estimate.md](phase-4-treatment-estimate.md) |
| ۵ | پریو، لابراتوار، استریلیزاسیون، مواد مصرفی | [phase-5-clinical-ops.md](phase-5-clinical-ops.md) |

محتوای پیشنهادی بستهٔ پیش‌فرض در [preset-content.md](preset-content.md).

هر فاز بدون تست موفق و خطا و مرزی، و بدون به‌روزرسانی `docs/api/`، تمام‌شده نیست.

---

## ۷. قواعد مشترک همهٔ فازها

از `CLAUDE.md` پروژه، اینجا فقط یادآوری:

- شناسه عددی به‌همراه `uuid` نسخهٔ ۴. `uuid` در پاسخ API، `id` هرگز.
- تایم‌استمپ از نوع `integer` یونیکس.
- نام جدول جمع و snake_case.
- پاسخ از `BaseController`، خطا با `AppException` و `ErrorCodes`.
- هر entity تازه باید جفت `entity_type` و `entity_id` داشته باشد، وگرنه `TenantSchemaCoverageTest` قرمز می‌شود.
- کوئری‌های لیست با `getArrayResult()`.
- در پنل: `SearchableSelect` به‌جای `select` بومی، `Switch` به‌جای checkbox، رنگ فقط از `var(--...)`.
- تاریخ در دیتابیس میلادی ذخیره، در نمایش جلالی.

---

## ۸. مرزهای رزروشده

**بیمه:** دامنهٔ `Insurance` از قبل کامل است و شامل `TenantServiceCoverage` و `TenantInsuranceCategoryCoverage` و `EntityInsurancePricing` می‌شود.
هیچ منطق بیمه‌ای در این ماژول نوشته نمی‌شود.
تنها اثرش این است که ردیف برآورد در فاز ۴ باید ستون `payer_type` داشته باشد تا بعداً سند بیمه رویش بنشیند.

**اپ Tauri:** در فاز ۱ تا ۳ هیچ تغییری نمی‌گیرد.
اگر بعداً چارت آفلاین لازم شد، `tooth_chart` و وضعیت دندان کاندیدهای خوبی هستند چون تک‌نویسنده و کم‌تعارض‌اند.
برآورد درمان چون مالی است نباید `last-write-wins` بگیرد.
این موضوع به سند sync ارجاع می‌شود، نه اینجا.

**سایت عمومی nobat724:** خدمات دندانپزشکی مثل هر خدمت دیگری در سایت دیده می‌شوند.
هیچ تغییر اختصاصی لازم نیست مگر اینکه بخواهیم انتخاب دندان در رزرو آنلاین باشد، که خارج از این سند است.
