# فاز ۳ — داشبورد دندانپزشکی

> پیش‌نیاز: فاز ۱ و ۲.
> خروجی قابل تست: مدیر و پزشک و پذیرش، شاخص‌های دندانپزشکی را در همان داشبورد فعلی خودشان می‌بینند.

---

## ۱. هدف

شاخص‌های مخصوص حوزهٔ فعالیت به داشبورد نقشی اضافه شوند، بدون دست‌زدن به منطق داشبورد عمومی و بدون کند کردن داشبورد حوزه‌های دیگر.

---

## ۲. الگو

همان الگوی `TreatmentWorkflow` که در `docs/adr/0005` ثبت شده.

```
src/Dashboard/Metric/DomainMetricProvider.php     اینترفیس، با تگ app.domain_metric_provider
src/Dashboard/Metric/DomainMetricRegistry.php     انتخاب بر اساس کد حوزه
src/Dashboard/Metric/MetricRequest.php            بازه، نقش، محیط، فیلترها
src/Dashboard/Metric/MetricSet.php                خروجی استاندارد
src/Dental/Metric/DentalMetricProvider.php        پیاده‌سازی دندانپزشکی
```

اینترفیس:

```php
interface DomainMetricProvider
{
    public function supports(?string $practiceDomainCode): bool;

    /** @return list<string> کلید شاخص‌هایی که این نقش می‌بیند */
    public function keysFor(string $role): array;

    public function collect(MetricRequest $request): MetricSet;
}
```

محیطی که حوزه‌اش `null` است یا provider ندارد، هیچ بخش تازه‌ای نمی‌گیرد.
برخلاف `TreatmentWorkflowRegistry` اینجا پیاده‌سازی پیش‌فرض لازم نیست؛ نبودن provider یعنی بخش دندانی رندر نمی‌شود.
دلیل: شاخص خالی بدتر از نبودن بخش است.

هر متد `collect` باید تعداد کوئری ثابت داشته باشد، مستقل از تعداد شاخص.
یعنی شاخص‌های هم‌منبع در یک کوئری جمع شوند.

---

## ۳. رجیستری شاخص‌ها

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

### شاخص‌های قابل محاسبه در فاز ۳

| کلید | تعریف | منبع |
|---|---|---|
| `production` | جمع مبلغ ناخالص ویزیت‌های انجام‌شده در بازه | `patient_sessions` |
| `collection` | جمع پرداخت‌های ثبت‌شده در بازه | `payments` و `session_payments` |
| `collection_rate` | وصولی تقسیم بر تولید | مشتق |
| `avg_per_visit` | تولید تقسیم بر تعداد ویزیت | مشتق |
| `production_per_doctor` | تولید به تفکیک پزشک | `patient_sessions` |
| `service_mix` | سهم هر گروه کاتالوگ از تولید | `session_services` و `service_catalog_categories` |
| `chair_utilization` | دقایق رزروشدهٔ منابع نوع اتاق تقسیم بر دقایق ظرفیت | `resource_occupancy` و `resource_calendars` |
| `no_show_rate` | نوبت‌های حاضرنشده تقسیم بر کل نوبت‌ها | `appointments` |
| `cancellation_rate` | نوبت‌های لغوشده تقسیم بر کل | `appointments` |
| `new_patients` | بیماران با اولین ویزیت در بازه | `patient_sessions` |
| `ar_outstanding` | جمع بدهی معوق بیماران | `patient_sessions` |
| `treatments_by_group` | تعداد خدمت انجام‌شده به تفکیک گروه دندانی | `session_services` |
| `teeth_treated` | تعداد دندان‌های درمان‌شدهٔ یکتا در بازه | `session_services` |

### شاخص‌هایی که در این فاز وجود ندارند

| کلید | چرا |
|---|---|
| `case_acceptance_rate` | ورودی‌اش برآورد درمان است که فاز ۴ ساخته می‌شود |
| `unscheduled_treatment` | همان |
| `redo_rate` | نیازمند حالت درمان مجدد روی ردیف برآورد |
| `lab_cost_ratio` | فاز ۵ |
| `consumable_cost_ratio` | فاز ۵ |
| `recall_response_rate` | نیازمند سازوکار ریکال که پروژه هنوز ندارد |

این فهرست عمداً در سند مانده تا کسی فکر نکند فراموش شده‌اند.

### دو تعریف که نباید قاطی شوند

**تولید** جمع مبلغ کاری است که انجام شده.
**وصولی** جمع پولی است که رسیده.

این دو از دو جدول متفاوت می‌آیند و هیچ‌کدام نباید از دیگری استنتاج شود.
فاصلهٔ بینشان همان چیزی است که نرخ وصول را معنادار می‌کند.

---

## ۴. نماها

| نقش | شاخص‌های صفحهٔ اصلی |
|---|---|
| مدیر کلینیک یا مطب | `collection`, `collection_rate`, `chair_utilization`, `new_patients`, `ar_outstanding`, `service_mix` |
| پزشک | تولید شخصی، `avg_per_visit` خودش، `treatments_by_group` خودش، `teeth_treated` |
| پذیرش | اشغال یونیت امروز، `no_show_rate`, `cancellation_rate`, صف نوبت امروز |

حداکثر شش کارت در صفحهٔ اصلی هر نقش.
بقیه پشت drill-down.
دلیل: داشبورد با بیست کارت خوانده نمی‌شود و کاربر به‌جای تصمیم، اسکرول می‌کند.

فیلتر مشترک: بازهٔ تاریخ جلالی، پزشک، گروه خدمت، یونیت.

---

## ۵. API

اندپوینت‌های نقشی موجود دست نمی‌خورند.
پاسخشان یک کلید تازه می‌گیرد:

```
GET /api/v1/dashboard/clinic
    → { ..., domain_metrics: { code: "dental", metrics: { ... } } | null }
```

اگر محیط حوزه ندارد یا provider ندارد، مقدار `null` است.

یک اندپوینت تازه برای بازه و روند:

```
GET /api/v1/dashboard/domain-metrics?from&to&doctorUuid?&resourceUuid?&groupUuid?
    → { code, metrics: { <key>: { value, previous_value, change_percent } } }

GET /api/v1/dashboard/domain-metrics/trend?metric=production&from&to&interval=day|week|month
    → { points: [ { date, value } ] }
```

`date` میلادی برمی‌گردد و تبدیل جلالی در فرانت انجام می‌شود.
دلیل: رشتهٔ جلالی در پاسخ، مرتب‌سازی و بازه‌گیری را در فرانت می‌شکند.

بازهٔ پیش‌فرض سی روز.
حداکثر بازهٔ مجاز یک سال، وگرنه خطای ۴۲۲.
دلیل: بدون سقف، یک درخواست می‌تواند کل جدول ویزیت را اسکن کند.

`docs/api/dashboard.md` همان جلسه به‌روز می‌شود.

---

## ۶. پنل ادمین

فایل: `assets/admin/pages/DashboardPage.tsx`

- بخش «شاخص‌های دندانپزشکی» بعد از کارت‌های عمومی، فقط وقتی `domain_metrics` مقدار دارد.
- کارت‌ها با `StatCard` موجود.
- روند با `Recharts` که پروژه از قبل دارد.
- drill-down با `DataTable` موجود.
- انتخاب بازه با `PersianDateInput` موجود.
- فیلتر پزشک و یونیت با `SearchableSelect`.
- `TanStack Query` با `staleTime` معقول، چون این اعداد ثانیه‌ای عوض نمی‌شوند.

هیچ کامپوننت تازه‌ای ساخته نمی‌شود.

---

## ۷. تسک‌ها

| کد | تسک | فایل‌های اصلی | معیار پذیرش |
|---|---|---|---|
| DM3-01 | اینترفیس و رجیستری `DomainMetricProvider` | `src/Dashboard/Metric/` | محیط بدون حوزه، `null` می‌گیرد |
| DM3-02 | `MetricRequest` و `MetricSet` | همان | بازهٔ بزرگتر از یک سال ۴۲۲ می‌دهد |
| DM3-03 | `DentalMetricProvider` بخش مالی | `src/Dental/Metric/` | تولید و وصولی با داده‌ی ساختگی درست است |
| DM3-04 | بخش اشغال یونیت | همان | منبع بدون تقویم، صفر می‌دهد نه خطا |
| DM3-05 | بخش نوبت، حاضرنشده و لغو | همان | مخرج صفر، `null` می‌دهد نه تقسیم بر صفر |
| DM3-06 | بخش ترکیب خدمات و دندان‌های درمان‌شده | همان | خدمت بدون گروه در «سایر» می‌رود |
| DM3-07 | اتصال `domain_metrics` به چهار اندپوینت نقشی | `src/Dashboard/Controller/DashboardController.php` | داشبورد حوزه‌های دیگر کوئری اضافه نمی‌زند |
| DM3-08 | اندپوینت بازه و روند | همان | فیلترها ترکیبی کار می‌کنند |
| DM3-09 | بخش دندانی در صفحهٔ داشبورد | `assets/admin/pages/DashboardPage.tsx` | برای حوزهٔ زیبایی رندر نمی‌شود |
| DM3-10 | نمودار روند و drill-down | همان | خالی بودن داده، حالت خالی نشان می‌دهد نه خطا |
| DM3-11 | به‌روزرسانی `docs/api/dashboard.md` | `docs/api/` | نمونهٔ پاسخ با کد یکی است |

---

## ۸. تست‌ها

- محیط بدون حوزه: `domain_metrics` برابر `null` و هیچ کوئری اضافه‌ای اجرا نمی‌شود.
- محیط زیبایی: همان.
- محیط دندانپزشکی بدون هیچ ویزیت: همهٔ شاخص‌ها صفر یا `null`، بدون خطا.
- تقسیم بر صفر در هر نرخ: `null` برمی‌گردد و فرانت خط تیره نشان می‌دهد.
- پزشک فقط عدد خودش را می‌بیند، حتی با فیلتر پزشک دیگر.
- منشی به شاخص‌های مالی دسترسی ندارد.
- بازهٔ بزرگتر از یک سال: ۴۲۲.
- تست کارایی: تعداد کوئری با افزایش تعداد شاخص ثابت می‌ماند.

---

## ۹. ریسک‌ها

**کندی داشبورد با رشد داده.**
تصمیم فاز ۳ محاسبهٔ زنده است.
مهار: سقف بازه، ایندکس روی ستون‌های تاریخ و محیط، و یک تست کارایی که تعداد کوئری را قفل می‌کند.
اگر بعداً کند شد، جدول تجمیع پشت همین سرویس اضافه می‌شود بدون تغییر API.

**دو عدد متفاوت برای یک شاخص.**
مهار: هیچ فرمولی در فرانت نوشته نمی‌شود.
