# فاز ۳ — داشبورد دندانپزشکی > پیش‌نیاز: فاز ۱ و ۲. > خروجی قابل تست: مدیر و پزشک و پذیرش، شاخص‌های دندانپزشکی را در همان داشبورد فعلی خودشان می‌بینند. --- ## ۱. هدف شاخص‌های مخصوص حوزهٔ فعالیت به داشبورد نقشی اضافه شوند، بدون دست‌زدن به منطق داشبورد عمومی و بدون کند کردن داشبورد حوزه‌های دیگر. --- ## ۲. الگو همان الگوی `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 کلید شاخص‌هایی که این نقش می‌بیند */ 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: { : { 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. **دو عدد متفاوت برای یک شاخص.** مهار: هیچ فرمولی در فرانت نوشته نمی‌شود.