Add dental module phase 4 and phase 5 documentation, including treatment estimates, clinical operations, and preset content

- Introduced phase 4 documentation detailing treatment estimates, data models, state machines, API endpoints, and new dashboard metrics.
- Added phase 5 documentation covering consumables, lab operations, periodontal charts, sterilization cycles, clinical images, and a checklist for professional review.
- Created a preset content document outlining default dental service packages and protocols for clinics.
This commit is contained in:
hamed
2026-08-20 16:21:08 +03:30
parent 601d211d6f
commit 2f030edef1
12 changed files with 1794 additions and 3 deletions
+32 -3
View File
@@ -8,9 +8,9 @@ ClinicPro product. This glossary records the language the backend uses for its o
### Clinic configuration
**Practice Domain**:
The single field of practice a clinic declares it operates in — beauty, dental, orthopaedics. It is a
configuration key: it selects which dashboard, forms and treatment workflows the clinic gets. A
clinic has exactly one.
The single field of practice a tenant declares it operates in — beauty, dental, orthopaedics. It is a
configuration key: it selects which dashboard, forms and treatment workflows the tenant gets. A clinic
or a solo doctor's practice has exactly one.
_Avoid_: Specialty, clinic type, field, discipline
**Specialty**:
@@ -67,3 +67,32 @@ _Avoid_: Zone, body part, region
What the operator actually did to one Treatment Area in one Treatment Session — the device used, the
parameters it was set to, how long it took, and any note.
_Avoid_: Treatment log, area result, shot record
### Dental
**Dental Preset**:
The package of default data a tenant receives when it declares the dental practice domain — service
groups, services, resource types and protocols. Defined in code and versioned; it is product content,
not tenant data.
_Avoid_: Seed, fixture, template, sample data
**Preset Install**:
The record that one preset template key became one real row in one tenant. It is what makes installing
a preset twice a no-op.
_Avoid_: Migration, sync record, import log
**Tooth Chart**:
The current condition of every tooth of one patient in one tenant. It is a snapshot, never a history,
and it holds conditions that predate the tenant — a tooth lost years before the first visit.
_Avoid_: Odontogram record, dental record, chart entry
**Tooth Site**:
One tooth, identified by its two-digit FDI number, together with the surfaces of it a service targets.
It is what a dental service is performed on, and it is not a Treatment Area.
_Avoid_: Treatment Area, tooth record, position, location
**Treatment Estimate**:
The priced list of services proposed to a patient across their teeth, which the patient accepts in whole
or in part. In Persian it is «طرح درمان»; the English name stays distinct because Treatment Plan is
ambiguous between a Treatment Protocol and a Treatment Case.
_Avoid_: Treatment plan, quote, proposal, estimate sheet
@@ -0,0 +1,13 @@
# Practice domain belongs to the tenant, not only to the clinic
`practice_domain_id` originally lived on `clinics`, but every piece of operational data in this
codebase is owned by an `(entity_type, entity_id)` pair where `entity_type` is `doctor` or `clinic`.
A solo practice is a `doctor` tenant, so it could never declare a practice domain at all — the beauty
domain has the same hole, it simply had not been noticed. The column is therefore added to `doctors`
as well and read through a single `PracticeDomainResolver` that takes an `EntityContext`, so no
caller has to know which kind of tenant it is looking at.
## Considered Options
Letting a doctor inherit the domain of a clinic they work at was rejected: a doctor with no clinic
would stay domainless, and a doctor working at two clinics with different domains would be ambiguous.
@@ -0,0 +1,15 @@
# Teeth are not Treatment Areas
A Treatment Area is a `CatalogCategory` snapshotted onto a case, which made "one category per tooth"
look like a free way to get dental charting. It was rejected: FDI tooth numbering is a universal fact,
not a per-clinic taxonomy, so it would duplicate 32 to 52 identical rows into every clinic's service
tree, surfaces would need a further level below that, and the persistent condition of a tooth — missing,
crowned, implanted years before the patient ever arrived — has nowhere to live on a settings row.
A tooth is instead an FDI `smallint` on the record that targets it, and tooth condition is its own
snapshot table in the Dental context.
## Consequences
Tooth condition must be updated after each visit, and that projection lives in exactly one class rather
than being spread across controllers. In exchange, rendering a chart is one query and never a replay of
history.
@@ -0,0 +1,15 @@
# Dental attributes of a service live in an extension table, not on ServiceItem
Whether a service is priced per tooth, per surface or per canal — and whether booking it must ask for a
tooth at all — is dental-only knowledge, but `ServiceItem` is shared by every practice domain. Those
attributes therefore sit in a one-to-one `dental_service_profiles` row in the Dental context, keyed by
`service_item_id`, so a beauty clinic carries no dental columns and the next domain is not invited to add
its own set to the shared table. The cost is a join whenever the dental profile is needed, which is the
same pattern the codebase already uses elsewhere.
## Consequences
The reverse choice was made deliberately one level down: the tooth and surfaces a visit line was actually
billed for are columns on `SessionService` itself, because that row is the clinical and financial record
of the visit rather than shared configuration, and splitting it would allow a billed line to lose its
target unnoticed.
@@ -0,0 +1,13 @@
# Domain-specific dashboard metrics come from tagged providers
`DashboardController` already serves four role dashboards from 803 lines and fifteen dependencies, so
branching each of them on practice domain would double four code paths and make every clinic pay for
queries only dentists need. Domain metrics instead come from a `DomainMetricProvider` interface resolved
by a tagged-service registry keyed on the practice domain code — the same shape as `TreatmentWorkflow`
in ADR 0005 — and the role endpoints simply attach whatever the provider returns.
## Consequences
Unlike the workflow registry there is no default implementation: a tenant with no domain, or a domain with
no provider, gets `null` and the panel renders no extra section. An empty metrics block is worse than an
absent one.
+239
View File
@@ -0,0 +1,239 @@
# ماژول دندانپزشکی 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:** خدمات دندانپزشکی مثل هر خدمت دیگری در سایت دیده می‌شوند.
هیچ تغییر اختصاصی لازم نیست مگر اینکه بخواهیم انتخاب دندان در رزرو آنلاین باشد، که خارج از این سند است.
@@ -0,0 +1,273 @@
# فاز ۱ — حوزه در سطح محیط و نصب پیش‌فرض‌های دندانپزشکی
> پیش‌نیاز: ندارد.
> خروجی قابل تست: مطب یا کلینیک، حوزهٔ دندانپزشکی را انتخاب می‌کند و کاتالوگ خدمات دندانپزشکی‌اش ساخته می‌شود.
---
## ۱. هدف
سه چیز:
۱. مطب تک‌پزشک هم بتواند حوزهٔ فعالیت انتخاب کند، نه فقط کلینیک.
۲. با انتخاب دندانپزشکی، دسته‌بندی‌ها و خدمات و نوع منبع یونیت و پروتکل‌ها ساخته شوند.
۳. هر خدمت دندانی بداند روی دندان انجام می‌شود یا روی فک یا روی کل دهان.
---
## ۲. مدل داده
### ۲.۱ تغییر روی دامنهٔ موجود
روی `Doctor` یک رابطهٔ اختیاری به `PracticeDomain` اضافه می‌شود:
```
doctors.practice_domain_id → practice_domains.id nullable, ON DELETE SET NULL
```
مقدار `NULL` یعنی «تنظیم نشده» و همان رفتار امروز است. هرگز خطا نیست.
خواندن حوزهٔ محیط باید از یک نقطه باشد، نه دو `if` پراکنده:
```
src/PracticeDomain/Service/PracticeDomainResolver.php
```
ورودی‌اش `EntityContext` است و خروجی‌اش کد حوزه یا `null`.
دلیل جداکردنش: هر کسی که حوزه را لازم دارد نباید بداند محیط کلینیک است یا مطب.
### ۲.۲ موجودیت‌های تازه در دامنهٔ Dental
همه در `src/Dental/Entity`.
#### `DentalServiceProfile` — جدول `dental_service_profiles`
توسعهٔ یک‌به‌یک روی `ServiceItem`.
| ستون | نوع | توضیح |
|---|---|---|
| `id` | int | |
| `uuid` | string 36 | |
| `entity_type` و `entity_id` | | جفت محیط، از `TenantOwnedTrait` |
| `service_item_id` | int | یکتا، `ON DELETE CASCADE` |
| `target_scope` | string 20 | دامنهٔ هدف |
| `pricing_basis` | string 20 | مبنای واحد قیمت |
| `tooth_scope` | string 20 | دائمی، شیری، یا هر دو |
| `requires_lab` | bool | فاز ۵ از آن استفاده می‌کند |
| `created_at` و `updated_at` | int | |
مقدار `target_scope` تعیین می‌کند فرم ثبت خدمت در ویزیت چه چیزی بپرسد:
| مقدار | معنی | ورودی لازم در ویزیت |
|---|---|---|
| `none` | خدمت بدون هدف | هیچ |
| `tooth` | یک دندان | شمارهٔ دندان |
| `tooth_surface` | سطوح یک دندان | شمارهٔ دندان و حداقل یک سطح |
| `quadrant` | یک ناحیهٔ فک | کد ناحیه از ۱ تا ۴ |
| `arch` | یک فک | بالا یا پایین |
| `mouth` | کل دهان | هیچ |
مقادیر `pricing_basis`:
```
per_tooth | per_surface | per_canal | per_unit | per_arch | per_quadrant | per_session | flat
```
مقادیر `tooth_scope`:
```
any | permanent_only | primary_only
```
هر سه به‌صورت `enum` PHP در `src/Dental/Enum` تعریف می‌شوند و در ستون به‌صورت رشته ذخیره می‌شوند.
دلیل رشته‌بودن: افزودن مقدار تازه نباید migration بخواهد.
#### `PresetInstall` — جدول `dental_preset_installs`
| ستون | نوع | توضیح |
|---|---|---|
| `id` | int | |
| `uuid` | string 36 | |
| `entity_type` و `entity_id` | | جفت محیط |
| `preset_code` | string 40 | مثلاً `dental` |
| `preset_version` | int | نسخهٔ قالبی که نصب شد |
| `template_key` | string 80 | کلید ثابت ردیف در قالب |
| `target_type` | string 30 | `catalog_category`, `service_item`, `resource_type`, `treatment_protocol` |
| `target_id` | int | شناسهٔ ردیف واقعی ساخته‌شده |
| `installed_at` | int | |
یکتایی: `(entity_type, entity_id, preset_code, template_key)`.
این جدول تنها مبنای idempotency است.
اجرای دوم seed، هر کلید قالبی را که اینجا ثبت شده دوباره نمی‌سازد.
**چرا این جدول و نه یک ستون `origin` روی کاتالوگ:**
جدول‌های کاتالوگ مشترک همهٔ حوزه‌هایند.
یک ستون منشأ روی آن‌ها یعنی هر حوزهٔ بعدی هم چیزی به جدول مشترک اضافه می‌کند.
جدول نگاشت با حذف ماژول تمیز برداشته می‌شود.
---
## ۳. قالب پیش‌فرض
مسیر: `src/Dental/Preset/DentalPreset.php`
یک کلاس `final` با آرایه‌های ثابت و یک `const VERSION`.
محتوایش در [preset-content.md](preset-content.md).
ساختار هر بخش:
```
GROUPS : [ template_key, name, sort_order, parent_key|null ]
SERVICES : [ template_key, name, group_key, duration_minutes, session_count,
target_scope, pricing_basis, tooth_scope, requires_lab ]
RESOURCE_TYPES : [ template_key, code, name, field_schema|null ]
PROTOCOLS : [ template_key, service_key, session_count, interval_days ]
```
قیمت همهٔ خدمات صفر است.
دلیل: قیمت جعلی که کسی اصلاحش نکند، به بیمار نشان داده می‌شود.
صفر بودن در پنل قابل دیدن و قابل فیلتر کردن است.
`VERSION` عدد صحیح است و با هر تغییر محتوا یکی زیاد می‌شود.
نسخه در `dental_preset_installs` ثبت می‌شود تا بعداً بشود گفت کدام محیط با کدام نسخه نصب شده.
---
## ۴. سرویس نصب
مسیر: `src/Dental/Service/DentalPresetInstaller.php`
```
install(EntityContext $context, bool $force = false): PresetInstallReport
preview(EntityContext $context): PresetInstallReport
```
قواعد:
- همه‌چیز در یک تراکنش. نصب نیمه‌کاره نداریم.
- هر ردیف قبل از ساخت، در `dental_preset_installs` جستجو می‌شود.
- ردیفی که کلیدش قبلاً نصب شده، دست نمی‌خورد. حتی اگر مدیر نامش را عوض کرده باشد.
- `preview` هیچ چیزی نمی‌نویسد و همان گزارش را بدون ساخت برمی‌گرداند.
- بخش سازمانی: اگر محیط هیچ `ServiceSection` نداشت، یکی با نام «دندانپزشکی» ساخته می‌شود، وگرنه اولین بخش فعال استفاده می‌شود. دلیل: `ServiceItem` بدون بخش `NOT NULL` نمی‌شود.
`PresetInstallReport` یک شیء ساده است با تعداد ساخته‌شده و تعداد ردشده به تفکیک نوع.
### تصمیم لحظهٔ اجرا
هنگام `PATCH` روی کلینیک یا پزشک، اگر حوزه به دندانپزشکی تغییر کرد:
```
کاتالوگ محیط خالی است → نصب خودکار، بدون پرسش
کاتالوگ محیط داده دارد → هیچ چیز ساخته نمی‌شود، پرچم «پیش‌فرض نصب‌نشده» برای پنل برمی‌گردد
```
تعریف «خالی»: هیچ `ServiceItem` و هیچ `CatalogCategory` فعالی در آن محیط نباشد.
---
## ۵. API
مستندات در `docs/api/dental.md` نوشته می‌شود. فایل تازه است.
```
GET /api/v1/dental/preset/status
→ { installed: bool, installed_version: int|null,
current_version: int, catalog_empty: bool }
GET /api/v1/dental/preset/preview
→ { groups: n, services: n, resource_types: n, protocols: n, skipped: n }
POST /api/v1/dental/preset/install
→ همان گزارش، بعد از نصب واقعی
```
دسترسی: `ROLE_CLINIC` یا `ROLE_DOCTOR`. محیط از `EntityContextResolver` می‌آید، نه از بدنهٔ درخواست.
خطاها:
| کد | HTTP | حالت |
|---|---|---|
| `ERR_VALIDATION_002` | 422 | حوزهٔ فعالیت محیط دندانپزشکی نیست |
| `ERR_FORBIDDEN_001` | 403 | نقش مجاز نیست |
اندپوینت خدمت هم گسترش پیدا می‌کند:
```
POST /api/v1/service-item بدنه کلید اختیاری dental می‌گیرد
PATCH /api/v1/service-item/{uuid} همان
GET /api/v1/service-item/{uuid} پاسخ کلید dental دارد اگر پروفایل داشته باشد
```
`docs/api/clinic-services.md` همان جلسه به‌روز می‌شود.
---
## ۶. پنل ادمین
### صفحهٔ حوزهٔ فعالیت
فایل: `assets/admin/pages/PracticeDomainSettingsPage.tsx`
- برای کاربر پزشک هم کار کند، نه فقط کلینیک.
- بعد از انتخاب دندانپزشکی، وضعیت نصب پیش‌فرض نشان داده شود.
- اگر نصب نشده، کارت با پیش‌نمایش تعداد و دکمهٔ نصب.
- بعد از نصب موفق، پیام با تعداد ردیف ساخته‌شده و لینک به صفحهٔ خدمات.
### فرم خدمت
فایل: `assets/admin/pages/ClinicServicesPage.tsx`
- اگر حوزهٔ محیط دندانپزشکی است، بخش «ویژگی‌های دندانی» در فرم خدمت اضافه شود.
- سه انتخاب: دامنهٔ هدف، مبنای قیمت، نوع دندان. هر سه با `SearchableSelect`.
- سوییچ «نیاز به لابراتوار» با `Switch`.
- برای حوزه‌های دیگر این بخش اصلاً رندر نشود.
---
## ۷. تسک‌ها
| کد | تسک | فایل‌های اصلی | معیار پذیرش |
|---|---|---|---|
| DM1-01 | افزودن `practice_domain_id` به `Doctor` با migration | `src/Doctor/Entity/Doctor.php`, `migrations/` | migration روی دیتابیس تست اجرا و برگشت می‌خورد |
| DM1-02 | `PracticeDomainResolver` بر اساس `EntityContext` | `src/PracticeDomain/Service/` | تست: محیط کلینیک، محیط پزشک، محیط بدون حوزه |
| DM1-03 | اندپوینت تنظیم حوزه برای پزشک | `src/Doctor/Controller/`, `docs/api/practice-domain.md` | پزشک حوزه را ست می‌کند و در `GET` پروفایل می‌بیند |
| DM1-04 | `enum`های دندانی | `src/Dental/Enum/` | مقدار نامعتبر `AppException` می‌دهد |
| DM1-05 | موجودیت و مخزن `DentalServiceProfile` | `src/Dental/Entity/`, `src/Dental/Repository/` | `TenantSchemaCoverageTest` سبز |
| DM1-06 | موجودیت و مخزن `PresetInstall` | همان | یکتایی کلید قالب در محیط تست می‌شود |
| DM1-07 | فایل قالب `DentalPreset` با محتوای پیش‌نویس | `src/Dental/Preset/` | تست ساختاری: هر خدمت به گروه موجود اشاره می‌کند |
| DM1-08 | `DentalPresetInstaller` با `install` و `preview` | `src/Dental/Service/` | اجرای دوم هیچ ردیف تازه‌ای نمی‌سازد |
| DM1-09 | اتصال نصب خودکار به تغییر حوزه | `src/Clinic/Controller/ClinicController.php` و معادل پزشک | کاتالوگ خالی نصب می‌شود، کاتالوگ پرداده نمی‌شود |
| DM1-10 | سه اندپوینت `preset` | `src/Dental/Controller/DentalPresetController.php` | تست موفق، بدون دسترسی، حوزهٔ اشتباه |
| DM1-11 | گسترش `service-item` برای کلید `dental` | `src/ClinicService/Controller/ClinicServiceController.php` | ساخت خدمت با پروفایل و بدون آن، هر دو کار می‌کند |
| DM1-12 | `docs/api/dental.md` و به‌روزرسانی دو سند موجود | `docs/api/` | مسیرها و خطاها با کد یکی است |
| DM1-13 | کارت نصب پیش‌فرض در صفحهٔ حوزهٔ فعالیت | `assets/admin/pages/PracticeDomainSettingsPage.tsx` | تست: نصب‌نشده، نصب‌شده، حوزهٔ غیر دندانی |
| DM1-14 | بخش ویژگی دندانی در فرم خدمت | `assets/admin/pages/ClinicServicesPage.tsx` | برای حوزهٔ زیبایی رندر نمی‌شود |
| DM1-15 | بازبینی تخصصی فهرست خدمات توسط دندانپزشک | `preset-content.md` | بدون این، فاز ۱ بسته نمی‌شود |
---
## ۸. تست‌ها
- نصب روی محیط خالی: همهٔ ردیف‌ها ساخته می‌شوند.
- نصب دوم: صفر ردیف تازه، گزارش می‌گوید چند تا رد شد.
- نصب روی محیطی که حوزه‌اش دندانپزشکی نیست: خطای ۴۲۲.
- نصب توسط نقش بدون دسترسی: خطای ۴۰۳.
- شکست وسط نصب: هیچ ردیفی باقی نمی‌ماند، تراکنش برگشت می‌خورد.
- محیط پزشک و محیط کلینیک، هر دو مسیر.
- خدمتی که پروفایل دندانی ندارد، در حوزهٔ دندانپزشکی هم بدون خطا کار می‌کند.
---
## ۹. ریسک‌ها
**فهرست خدمات غلط.**
اثرش در همهٔ کلینیک‌های نصب‌کننده پخش می‌شود و جمع کردنش ممکن نیست.
مهار: تسک `DM1-15` مسدودکننده است.
**نصب خودکار روی محیطی که کاربر نمی‌خواست.**
مهار: فقط وقتی کاتالوگ خالی است، و ردیف‌ها با قیمت صفر و قابل غیرفعال کردن.
**تعریف «کاتالوگ خالی» ناپایدار.**
اگر کلینیکی یک دستهٔ آزمایشی ساخته باشد، نصب خودکار انجام نمی‌شود و کاربر گیج می‌شود.
مهار: پنل همیشه وضعیت نصب و دکمه را نشان می‌دهد، پس مسیر دوم همیشه در دسترس است.
@@ -0,0 +1,299 @@
# فاز ۲ — چارت دندان و هدف‌گیری دندان روی خدمت ویزیت
> پیش‌نیاز: فاز ۱.
> خروجی قابل تست: دندانپزشک وضعیت دندان‌های بیمار را می‌بیند، ثبت خدمت روی دندان انجام می‌دهد و چارت خودکار به‌روز می‌شود.
---
## ۱. هدف
سه چیز:
۱. هر بیمار در هر محیط یک چارت دندان داشته باشد.
۲. ثبت خدمت در ویزیت بتواند دندان و سطح را هدف بگیرد.
۳. چارت بعد از ثبت خدمت خودکار به‌روز شود، و وضعیت‌های قدیمی هم دستی قابل ثبت باشند.
---
## ۲. شماره‌گذاری دندان
استاندارد `FDI` دو رقمی، همان `ISO 3950`.
```
دائمی : 1118, 2128, 3138, 4148
شیری : 5155, 6165, 7175, 8185
```
ذخیره به‌صورت `smallint`، نه رشته.
دلیل: مقایسه و بازه و ایندکس روی عدد کار می‌کند و «۱۱» و «11» دو مقدار جدا نمی‌سازد.
اعتبارسنجی در یک نقطه:
```
src/Dental/Validator/ToothNumberValidator.php
```
استانداردهای `Universal` و `Palmer` در لایهٔ داده استفاده نمی‌شوند.
اگر بعداً لازم شد، فقط لایهٔ نمایش تبدیل می‌کند.
سطوح دندان:
```
M مزیال · D دیستال · O اکلوزال · B باکال · L لینگوال · P پالاتال · I اینسایزال
```
---
## ۳. مدل داده
همه در `src/Dental/Entity`.
### `ToothChart` — جدول `dental_tooth_charts`
| ستون | نوع | توضیح |
|---|---|---|
| `id`, `uuid` | | |
| `entity_type`, `entity_id` | | جفت محیط |
| `patient_record_id` | int | یکتا در هر محیط |
| `dentition_type` | string 20 | `permanent`, `primary`, `mixed` |
| `last_examined_at` | int, nullable | |
| `created_at`, `updated_at` | int | |
یکتایی: `(entity_type, entity_id, patient_record_id)`.
چارت با اولین نیاز ساخته می‌شود، نه با ساخت پرونده.
دلیل: پروندهٔ بیماری که هرگز درمان دندانی نمی‌گیرد نباید ردیف خالی بسازد.
### `ToothStatus` — جدول `dental_tooth_statuses`
| ستون | نوع | توضیح |
|---|---|---|
| `id`, `uuid` | | |
| `chart_id` | int | `ON DELETE CASCADE` |
| `tooth_number` | smallint | FDI |
| `condition` | string 30 | وضعیت کلی دندان |
| `surface_map` | json, nullable | وضعیت هر سطح |
| `note` | string 500, nullable | |
| `source` | string 20 | `manual` یا `visit` |
| `recorded_by_user_id` | int, nullable | |
| `updated_at` | int | |
یکتایی: `(chart_id, tooth_number)`.
مقادیر `condition`:
```
healthy | caries | filled | crown | bridge_pontic | root_canal
implant | missing | extracted | impacted | to_extract | unerupted
```
`surface_map` شکل ثابت دارد:
```json
{ "O": "filled", "M": "caries", "D": "healthy" }
```
**چرا وضعیت جدا از تاریخچه ذخیره می‌شود:**
رندر چارت باید با یک کوئری انجام شود.
اگر وضعیت هر بار از بازپخش تاریخچهٔ ویزیت‌ها ساخته شود، هر باز کردن تب یک محاسبهٔ سنگین است و وضعیت قبل از اولین مراجعه اصلاً قابل ثبت نیست.
هزینهٔ پذیرفته‌شده: این جدول باید بعد از هر ویزیت به‌روز شود و این کار فقط در یک کلاس انجام می‌شود، نه پراکنده در کنترلرها.
### `ToothStatusLog` — جدول `dental_tooth_status_logs`
هر تغییر وضعیت یک ردیف اضافه می‌کند. فقط افزودنی است.
| ستون | نوع |
|---|---|
| `id`, `uuid` | |
| `chart_id`, `tooth_number` | |
| `from_condition`, `to_condition` | string 30 |
| `surface_map_before`, `surface_map_after` | json, nullable |
| `session_service_id` | int, nullable |
| `changed_by_user_id` | int, nullable |
| `changed_at` | int |
دلیل وجودش: چارت سند پزشکی است.
«چه کسی دندان ۱۶ را کشیده‌شده علامت زد» باید قابل جواب دادن باشد.
---
## ۴. هدف‌گیری دندان روی خدمت ویزیت
### تغییر روی دامنهٔ موجود
روی `SessionService` سه ستون اختیاری اضافه می‌شود:
```
tooth_number smallint nullable
surfaces json nullable
target_code string 10 nullable کد فک یا ناحیه
```
**چرا اینجا و نه در جدول دندانی جدا:**
این‌ها ویژگی همان ردیف خدمتِ فاکتورشده‌اند.
جدا کردنشان یعنی برای هر ردیف فاکتور یک join اضافه، و امکان اینکه ردیف فاکتور بدون هدف بماند بدون اینکه کسی بفهمد.
برخلاف `ServiceItem` که تنظیمات است و مشترک همهٔ حوزه‌هاست، `SessionService` سند یک ویزیت است و این سه ستون بخشی از همان سند.
### اعتبارسنجی
در `src/Dental/Service/ToothTargetValidator.php`.
قاعده بر اساس `target_scope` پروفایل خدمت:
| `target_scope` | لازم | ممنوع |
|---|---|---|
| `none` و `mouth` | — | هر سه |
| `tooth` | `tooth_number` | `surfaces`, `target_code` |
| `tooth_surface` | `tooth_number` و حداقل یک سطح | `target_code` |
| `quadrant` | `target_code` از ۱ تا ۴ | `tooth_number`, `surfaces` |
| `arch` | `target_code` برابر `upper` یا `lower` | `tooth_number`, `surfaces` |
قاعدهٔ دوم: `tooth_scope` خدمت با شمارهٔ دندان بخواند.
خدمت `permanent_only` روی دندان ۵۱ خطا می‌دهد.
قاعدهٔ سوم: خدمتی که پروفایل دندانی ندارد، هیچ هدفی نمی‌پذیرد.
خطا با `ERR_VALIDATION_002` و نام فیلد.
### پروجکتور چارت
مسیر: `src/Dental/Service/ToothChartProjector.php`
بعد از ثبت یا ویرایش ردیف خدمت با هدف دندانی:
```
سرویس ترمیمی روی سطوح → همان سطوح در surface_map مقدار filled می‌گیرند
سرویس کشیدن دندان → condition برابر extracted
سرویس درمان ریشه → condition برابر root_canal
سرویس روکش → condition برابر crown
سرویس ایمپلنت → condition برابر implant
بقیه → وضعیت دست نمی‌خورد، فقط لاگ ثبت می‌شود
```
نگاشت خدمت به اثر، در همان `DentalPreset` تعریف می‌شود با کلید `chart_effect`.
دلیل: مدیر می‌تواند خدمت دلخواه بسازد و اثرش را انتخاب کند، بدون اینکه کد عوض شود.
حذف ردیف خدمت، وضعیت را به عقب برنمی‌گرداند.
دلیل: دندان کشیده‌شده با حذف یک ردیف فاکتور برنمی‌گردد.
به‌جایش یک لاگ با توضیح ثبت می‌شود و اصلاح دستی می‌ماند.
---
## ۵. API
`docs/api/dental.md` گسترش پیدا می‌کند.
```
GET /api/v1/dental/chart/{patientRecordUuid}
→ { chart: {...}, teeth: [ { tooth_number, condition, surfaces, note } ] }
PUT /api/v1/dental/chart/{patientRecordUuid}/tooth/{toothNumber}
→ ثبت یا اصلاح دستی وضعیت یک دندان
GET /api/v1/dental/chart/{patientRecordUuid}/tooth/{toothNumber}/history
→ لاگ تغییرات همان دندان
```
دسترسی:
| عملیات | clinic | doctor | secretary | staff |
|---|---|---|---|---|
| دیدن چارت | بله | بیماران خودش | خواندنی | نه |
| ویرایش دستی چارت | نه | بله | نه | نه |
| ثبت هدف دندانی در ویزیت | نه | بله | نه | نه |
خطاها:
| کد | HTTP | حالت |
|---|---|---|
| `ERR_VALIDATION_002` | 422 | شمارهٔ دندان نامعتبر یا هدف ناسازگار |
| `ERR_NOT_FOUND_001` | 404 | پرونده در این محیط نیست |
| `ERR_FORBIDDEN_001` | 403 | نقش مجاز نیست |
اندپوینت ثبت خدمت ویزیت هم کلیدهای تازه می‌گیرد و `docs/api/patient.md` همان جلسه به‌روز می‌شود.
---
## ۶. پنل ادمین
### تب تازه
فایل: `assets/admin/pages/PatientDetailPage.tsx`
- کلید تب: `dental`، برچسب «چارت دندان».
- فقط وقتی حوزهٔ محیط دندانپزشکی است رندر می‌شود.
- بین «پرونده پزشکی» و «ضمیمه» می‌نشیند.
### کامپوننت چارت
فایل: `assets/admin/components/dental/ToothChart.tsx`
این تنها جایی است که ساخت کامپوننت تازه موجه است، چون هیچ کامپوننت موجودی این کار را نمی‌کند.
قواعد:
- `SVG` دست‌نویس، بدون کتابخانهٔ بیرونی.
- هر دندان یک گروه قابل کلیک با شمارهٔ FDI.
- هر سطح یک مسیر جدا، تا کلیک روی سطح جدا از کلیک روی دندان باشد.
- رنگ‌ها فقط از توکن‌های `styles.css`. هیچ رنگ ثابتی در کد کامپوننت نیست.
- چیدمان `RTL` و سازگار با تم تیره.
- فک بالا در ردیف بالا، فک پایین در ردیف پایین، سمت راست بیمار در سمت راست تصویر. این قرارداد در بالای فایل به‌صورت کامنت نوشته شود چون خطای رایج همین است.
- حالت شیری و مختلط: دندان‌های شیری در همان گرید، کوچکتر.
- بدون تعامل هم باید خوانا باشد، چون در چاپ پرونده استفاده می‌شود.
### فرم ثبت خدمت در ویزیت
فایل: `assets/admin/pages/EditSessionPage.tsx`
- بعد از انتخاب خدمت، اگر پروفایل دندانی دارد، انتخابگر هدف نشان داده شود.
- انتخاب دندان از روی همان `ToothChart` انجام شود، نه از یک `select` با ۳۲ گزینه.
- انتخاب سطح فقط وقتی `target_scope` برابر `tooth_surface` است.
---
## ۷. تسک‌ها
| کد | تسک | فایل‌های اصلی | معیار پذیرش |
|---|---|---|---|
| DM2-01 | `ToothNumberValidator` و ثابت‌های FDI | `src/Dental/Validator/` | همهٔ شماره‌های معتبر و نامعتبر تست می‌شوند |
| DM2-02 | موجودیت `ToothChart` | `src/Dental/Entity/` | یکتایی پرونده در محیط |
| DM2-03 | موجودیت `ToothStatus` با `surface_map` | همان | شکل json اعتبارسنجی می‌شود |
| DM2-04 | موجودیت `ToothStatusLog` | همان | فقط افزودنی، بدون متد حذف |
| DM2-05 | سه ستون هدف روی `SessionService` با migration | `src/Patient/Entity/SessionService.php` | ردیف بدون هدف مثل قبل کار می‌کند |
| DM2-06 | `ToothTargetValidator` | `src/Dental/Service/` | هر پنج حالت `target_scope` تست می‌شود |
| DM2-07 | `ToothChartProjector` و نگاشت `chart_effect` | `src/Dental/Service/`, `src/Dental/Preset/` | ثبت کشیدن دندان، وضعیت را عوض می‌کند و لاگ می‌زند |
| DM2-08 | سه اندپوینت چارت | `src/Dental/Controller/DentalChartController.php` | موفق، بدون دسترسی، پروندهٔ محیط دیگر |
| DM2-09 | گسترش ثبت خدمت ویزیت برای هدف دندانی | `src/Patient/Controller/PatientController.php` | هدف ناسازگار ۴۲۲ می‌دهد |
| DM2-10 | کامپوننت `ToothChart` | `assets/admin/components/dental/` | تست: کلیک دندان، کلیک سطح، حالت فقط‌خواندنی |
| DM2-11 | تب چارت در پروندهٔ بیمار | `assets/admin/pages/PatientDetailPage.tsx` | برای حوزهٔ غیر دندانی رندر نمی‌شود |
| DM2-12 | انتخابگر هدف در فرم ثبت خدمت | `assets/admin/pages/EditSessionPage.tsx` | خدمت بدون پروفایل، انتخابگر نشان نمی‌دهد |
| DM2-13 | به‌روزرسانی `docs/api/dental.md` و `docs/api/patient.md` | `docs/api/` | مسیرها با کد یکی است |
---
## ۸. تست‌ها
- ثبت خدمت روی دندان شیری با خدمت `permanent_only`: خطای ۴۲۲.
- ثبت خدمت `tooth_surface` بدون سطح: خطای ۴۲۲.
- ثبت خدمت `arch` با شمارهٔ دندان: خطای ۴۲۲.
- ثبت کشیدن دندان: وضعیت `extracted` و یک ردیف لاگ.
- ویرایش دستی وضعیت: منبع `manual` ثبت می‌شود.
- خواندن چارت بیمار محیط دیگر: خطای ۴۰۴، نه ۴۰۳. دلیل: نباید وجود پرونده در محیط دیگر لو برود.
- منشی چارت را می‌بیند ولی نمی‌تواند ویرایش کند.
- چارت بیماری که هیچ درمانی نگرفته: ساخته می‌شود و همهٔ دندان‌ها `healthy` برمی‌گردند بدون اینکه ۳۲ ردیف در دیتابیس ساخته شود.
---
## ۹. ریسک‌ها
**واگرایی چارت از فاکتور.**
اگر کاربر خدمت را ثبت کند ولی هدف را خالی بگذارد، چارت به‌روز نمی‌شود و کسی نمی‌فهمد.
مهار: برای خدمتی که پروفایل دندانی دارد، هدف اجباری است و ردیف بدون هدف اصلاً ذخیره نمی‌شود.
**تعداد ردیف وضعیت.**
اگر برای هر بیمار ۳۲ ردیف ساخته شود، جدول سریع بزرگ می‌شود.
مهار: فقط دندان‌هایی که وضعیتشان از `healthy` فاصله گرفته ردیف می‌گیرند. بقیه در پاسخ API از پیش‌فرض ساخته می‌شوند.
**سمت چپ و راست جابه‌جا.**
خطای رایج در چارت دندان و در سند پزشکی خطرناک است.
مهار: قرارداد جهت در کامنت بالای کامپوننت، و یک تست که دندان ۱۱ را در جای درست ادعا می‌کند.
@@ -0,0 +1,200 @@
# فاز ۳ — داشبورد دندانپزشکی
> پیش‌نیاز: فاز ۱ و ۲.
> خروجی قابل تست: مدیر و پزشک و پذیرش، شاخص‌های دندانپزشکی را در همان داشبورد فعلی خودشان می‌بینند.
---
## ۱. هدف
شاخص‌های مخصوص حوزهٔ فعالیت به داشبورد نقشی اضافه شوند، بدون دست‌زدن به منطق داشبورد عمومی و بدون کند کردن داشبورد حوزه‌های دیگر.
---
## ۲. الگو
همان الگوی `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.
**دو عدد متفاوت برای یک شاخص.**
مهار: هیچ فرمولی در فرانت نوشته نمی‌شود.
@@ -0,0 +1,218 @@
# فاز ۴ — برآورد درمان و نرخ پذیرش
> پیش‌نیاز: فاز ۱ تا ۳.
> خروجی قابل تست: دندانپزشک برآورد چندخدمتی می‌سازد، بیمار تصمیم می‌گیرد، و نرخ پذیرش در داشبورد دیده می‌شود.
---
## ۱. چرا این فاز جدا افتاد
در جلسهٔ تصمیم‌گیری قرار شد فاز ۱ بدون برآورد جلو برود تا زودتر به خروجی برسیم.
هزینه‌اش این است که تا این فاز، چهار شاخص وجود ندارند:
نرخ پذیرش، درمان زمان‌بندی‌نشده، درمان مجدد، و ارزش معوق طرح.
---
## ۲. نام‌گذاری
واژهٔ انگلیسی `Treatment Plan` در `CONTEXT.md` ممنوع است، چون بین `TreatmentProtocol` و `TreatmentCase` ابهام می‌ساخت.
واژهٔ این مفهوم:
**Treatment Estimate** — فهرست پیشنهادی خدمات روی دندان‌های مشخص، با قیمت، که به بیمار ارائه می‌شود و بیمار کل یا بخشی از آن را می‌پذیرد.
در فارسی همان «طرح درمان» است، چون زبان روزمرهٔ دندانپزشک همین است.
جدایی نام انگلیسی و فارسی عمدی است: کد باید بدون ابهام باشد، رابط کاربری باید آشنا باشد.
---
## ۳. مدل داده
### `TreatmentEstimate` — جدول `dental_treatment_estimates`
| ستون | نوع | توضیح |
|---|---|---|
| `id`, `uuid` | | |
| `entity_type`, `entity_id` | | جفت محیط |
| `patient_record_id` | int | |
| `doctor_id` | int, nullable | پزشک ارائه‌دهنده |
| `title` | string 150 | |
| `status` | string 20 | |
| `total_rials` | int | جمع همهٔ ردیف‌ها |
| `accepted_rials` | int | جمع ردیف‌های پذیرفته‌شده |
| `presented_at` | int, nullable | لحظهٔ ارائه به بیمار |
| `decided_at` | int, nullable | لحظهٔ تصمیم بیمار |
| `expires_at` | int, nullable | |
| `created_at`, `updated_at` | int | |
### `TreatmentEstimateItem` — جدول `dental_treatment_estimate_items`
| ستون | نوع | توضیح |
|---|---|---|
| `id`, `uuid` | | |
| `estimate_id` | int | `ON DELETE CASCADE` |
| `service_item_id` | int | `ON DELETE RESTRICT` |
| `name_snapshot` | string 200 | نام خدمت در لحظهٔ ارائه |
| `tooth_number` | smallint, nullable | |
| `surfaces` | json, nullable | |
| `target_code` | string 10, nullable | |
| `quantity` | smallint | |
| `unit_price_rials` | int | |
| `amount_rials` | int | |
| `status` | string 20 | |
| `phase` | smallint | فاز درمان، برای اولویت‌بندی |
| `sort_order` | smallint | |
| `payer_type` | string 20 | پیش‌فرض `self_pay` |
| `insurance_ref_id` | int, nullable | فقط رزرو شده، بدون منطق |
| `session_service_id` | int, nullable | وقتی انجام شد به ردیف فاکتور وصل می‌شود |
`name_snapshot` و `unit_price_rials` عمدی‌اند.
همان دلیلی که `TreatmentCaseArea` اسنپ‌شات می‌گیرد و در `docs/adr/0002` ثبت شده:
برآوردی که به بیمار داده شده، سند است و با تغییر تعرفهٔ فردا نباید بازنویسی شود.
دو ستون `payer_type` و `insurance_ref_id` تنها نقطهٔ اتصال بیمه‌اند.
در این فاز هیچ محاسبه‌ای رویشان نوشته نمی‌شود.
---
## ۴. ماشین حالت
### برآورد
```
draft ──▶ presented ──▶ accepted ──▶ in_progress ──▶ completed
│ │ │
├──▶ partially_accepted ─────┤
├──▶ rejected └──▶ cancelled
└──▶ expired
```
قواعد گذار:
- `draft → presented`: حداقل یک ردیف و جمع بزرگتر از صفر. `presented_at` ثبت می‌شود.
- `presented → accepted | partially_accepted | rejected`: با تصمیم بیمار. `decided_at` ثبت می‌شود و `accepted_rials` از جمع ردیف‌های پذیرفته‌شده حساب می‌شود.
- `presented → expired`: با یک job زمان‌بندی‌شده بعد از N روز. پیش‌فرض پیشنهادی ۹۰ روز، قابل تنظیم در `Config`.
- `accepted → in_progress`: با اولین ردیفی که انجام می‌شود.
- `→ completed`: وقتی همهٔ ردیف‌های پذیرفته‌شده انجام یا لغو شده‌اند. توسط پروجکتور، نه دستی.
### ردیف
```
proposed ──▶ accepted ──▶ scheduled ──▶ done
│ │ │ │
└▶ rejected └▶ cancelled └▶ cancelled └▶ redo ──▶ scheduled
```
`redo` حالت مستقل است، نه حذف رکورد.
دلیل: ورودی شاخص کیفیت است.
اگر درمان مجدد با ویرایش رکورد قبلی جایگزین شود، آن شاخص برای همیشه از بین می‌رود.
---
## ۵. چرا `expired` لازم است
نرخ پذیرش باید مخرجش برآوردهایی باشد که در آن بازه **ارائه** شده‌اند، نه برآوردهایی که در آن بازه **تصمیم‌گیری** شده‌اند.
اگر مخرج بر اساس تصمیم باشد، برآوردهایی که هنوز جواب نگرفته‌اند از مخرج بیرون می‌مانند و نرخ به‌صورت مصنوعی بالا می‌رود.
`expired` همان چیزی است که برآورد بی‌جواب قدیمی را از حالت معلق در می‌آورد.
---
## ۶. اتصال به موتور موجود
ردیف پذیرفته‌شده وقتی زمان‌بندی می‌شود:
- اگر خدمتش پروتکل فعال دارد، همان مسیر موجود `TreatmentCaseStarter` یک `TreatmentCase` باز می‌کند.
- اگر ندارد، فقط یک نوبت ساخته می‌شود.
هیچ مسیر رزرو تازه‌ای نوشته نمی‌شود.
وقتی ردیف انجام شد و در ویزیت فاکتور شد، `session_service_id` پر می‌شود و وضعیت ردیف `done` می‌گیرد.
از همان‌جا پروجکتور فاز ۲ چارت را به‌روز می‌کند.
هیچ ستون پولی از برآورد به `TreatmentSession` نمی‌رود.
قاعدهٔ `docs/adr/0006` سر جایش می‌ماند.
---
## ۷. API
```
GET /api/v1/dental/estimates?patientRecordUuid=&status=
POST /api/v1/dental/estimate
GET /api/v1/dental/estimate/{uuid}
PATCH /api/v1/dental/estimate/{uuid}
POST /api/v1/dental/estimate/{uuid}/present
POST /api/v1/dental/estimate/{uuid}/decision
DELETE /api/v1/dental/estimate/{uuid}
POST /api/v1/dental/estimate/{uuid}/items
PATCH /api/v1/dental/estimate-item/{uuid}
DELETE /api/v1/dental/estimate-item/{uuid}
POST /api/v1/dental/estimate-item/{uuid}/schedule
```
دسترسی:
| عملیات | clinic | doctor | secretary |
|---|---|---|---|
| ساخت و ویرایش برآورد | نه | بله | نه |
| ارائه به بیمار | نه | بله | بله |
| ثبت تصمیم بیمار | بله | بله | بله |
| زمان‌بندی ردیف پذیرفته‌شده | بله | بله | بله |
دلیل اینکه ثبت تصمیم را منشی هم دارد: تصمیم بیمار معمولاً پشت میز پذیرش گفته می‌شود.
`docs/api/dental.md` گسترش پیدا می‌کند.
---
## ۸. شاخص‌های تازه در داشبورد
| کلید | تعریف |
|---|---|
| `case_acceptance_rate` | جمع پذیرفته‌شده تقسیم بر جمع ارائه‌شده، در بازهٔ ارائه |
| `unscheduled_treatment` | جمع مبلغ ردیف‌های پذیرفته‌شده بدون نوبت |
| `redo_rate` | ردیف‌های درمان مجدد تقسیم بر ردیف‌های انجام‌شده |
| `estimate_backlog` | جمع مبلغ برآوردهای ارائه‌شدهٔ بی‌جواب |
اضافه‌شدنشان به `DentalMetricProvider` است، بدون تغییر اینترفیس.
---
## ۹. پنل ادمین
- تب تازه در پروندهٔ بیمار: «طرح درمان».
- ساخت ردیف با انتخاب خدمت و انتخاب دندان از روی همان `ToothChart` فاز ۲.
- نمای چاپی برای دادن به بیمار.
- ثبت تصمیم به‌صورت ردیف‌به‌ردیف با `Switch`، نه یک دکمهٔ کلی. دلیل: پذیرش جزئی حالت رایج است.
- کارت‌های تازه در بخش دندانی داشبورد.
---
## ۱۰. تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM4-01 | موجودیت‌های برآورد و ردیف | `TenantSchemaCoverageTest` سبز |
| DM4-02 | ماشین حالت برآورد در یک کلاس جدا | گذار غیرمجاز `AppException` می‌دهد |
| DM4-03 | ماشین حالت ردیف | همان |
| DM4-04 | محاسبهٔ جمع و جمع پذیرفته‌شده در پروجکتور | ویرایش ردیف، جمع را همگام نگه می‌دارد |
| DM4-05 | job انقضا با مهلت قابل تنظیم | برآورد قدیمی `expired` می‌شود، برآورد پذیرفته‌شده نه |
| DM4-06 | اندپوینت‌های برآورد | همهٔ حالت‌های دسترسی تست می‌شوند |
| DM4-07 | زمان‌بندی ردیف و اتصال به `TreatmentCaseStarter` | خدمت پروتکل‌دار دوره باز می‌کند، بقیه فقط نوبت |
| DM4-08 | اتصال ردیف به `SessionService` هنگام انجام | وضعیت `done` و به‌روزرسانی چارت |
| DM4-09 | چهار شاخص تازه | مخرج صفر، `null` می‌دهد |
| DM4-10 | تب طرح درمان در پنل | پذیرش جزئی درست ثبت می‌شود |
| DM4-11 | نمای چاپی | در تم تیره هم درست چاپ می‌شود |
| DM4-12 | مستندات API | مسیرها با کد یکی است |
---
## ۱۱. تصمیم‌های باز
این‌ها قبل از شروع فاز ۴ باید جواب بگیرند:
۱. مهلت انقضای برآورد چند روز باشد؟ پیشنهاد ۹۰ روز.
۲. درمان مجدد هزینه‌دار است یا صفر؟ روی شاخص تولید اثر مستقیم دارد.
۳. قیمت ردیف برآورد از تعرفهٔ لحظهٔ ارائه می‌آید یا قابل ویرایش دستی است؟ پیشنهاد: پیش‌فرض از تعرفه، قابل ویرایش با ثبت لاگ.
۴. آیا یک بیمار می‌تواند همزمان دو برآورد ارائه‌شده داشته باشد؟ پیشنهاد: بله، ولی داشبورد باید هشدار بدهد.
@@ -0,0 +1,232 @@
# فاز ۵ — پریو، لابراتوار، استریلیزاسیون، مواد مصرفی و تصاویر
> پیش‌نیاز: فاز ۱ تا ۴.
> خروجی قابل تست: شاخص‌های هزینه و کیفیت، و ثبت بالینی کامل‌تر.
این فاز چهار موضوع مستقل دارد.
هرکدام جداگانه قابل اجراست و ترتیبشان اجباری نیست.
---
## ۱. مواد مصرفی — عمدتاً موجود است
`Inventory` و `SessionConsumable` از قبل هستند.
| موجودیت | مسیر |
|---|---|
| `InventoryItem` | `src/Inventory/Entity/InventoryItem.php` |
| `InventoryPackage` و `InventoryPackageItem` | همان پوشه |
| `SessionConsumable` | `src/Patient/Entity/SessionConsumable.php` |
| `ServiceItemConsumable` | `src/ClinicService/Entity/ServiceItemConsumable.php` |
قیمت در `SessionConsumable` اسنپ‌شات می‌شود، مثل `SessionService`.
`ServiceItem` هم می‌تواند به یک بستهٔ مصرفی وصل شود.
**پس کار این فاز فقط این است:**
- بستهٔ پیش‌فرض دندانپزشکی به `DentalPreset` اضافه شود: کامپوزیت، ماده بی‌حسی، فایل روتاری، سوزن، ماسک، دستکش.
- شاخص `consumable_cost_ratio` به `DentalMetricProvider` اضافه شود.
هیچ موجودیت تازه‌ای لازم نیست.
اگر کسی جدول مصرف مواد دندانپزشکی جدا ساخت، منبع حقیقت دوم ساخته است.
### تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM5-01 | بستهٔ مصرفی دندانپزشکی در قالب پیش‌فرض | نصب دوم چیزی تکرار نمی‌کند |
| DM5-02 | شاخص `consumable_cost_ratio` | مخرج صفر، `null` می‌دهد |
---
## ۲. لابراتوار — تازه است
### مدل داده
`Lab` — جدول `dental_labs`
| ستون | نوع |
|---|---|
| `id`, `uuid` | |
| `entity_type`, `entity_id` | جفت محیط |
| `title` | string 150 |
| `phone` | string 20, nullable |
| `active` | bool |
| `created_at`, `updated_at` | int |
`LabOrder` — جدول `dental_lab_orders`
| ستون | نوع | توضیح |
|---|---|---|
| `id`, `uuid` | | |
| `entity_type`, `entity_id` | | |
| `lab_id` | int | |
| `patient_record_id` | int | |
| `estimate_item_id` | int, nullable | ردیف برآوردی که این سفارش برایش است |
| `tooth_numbers` | json | دندان‌های درگیر |
| `description` | string 500 | |
| `status` | string 20 | |
| `cost_rials` | int | |
| `sent_at`, `due_at`, `received_at` | int, nullable | |
| `created_at`, `updated_at` | int | |
### ماشین حالت
```
draft ─▶ sent ─▶ in_lab ─▶ ready ─▶ received ─▶ delivered
└────▶ returned_for_fix ─▶ in_lab
```
`due_at` مبنای هشدار تأخیر است.
یک job روزانه سفارش‌های گذشته از موعد و در حالت غیرنهایی را برای داشبورد علامت می‌زند.
اتصال به `estimate_item_id` اختیاری است ولی توصیه‌شده.
بدون آن، بهای تمام‌شدهٔ آن ردیف قابل محاسبه نیست و شاخص حاشیهٔ سود بی‌معنا می‌شود.
### API
```
GET /api/v1/dental/labs
POST /api/v1/dental/lab
PATCH /api/v1/dental/lab/{uuid}
GET /api/v1/dental/lab-orders?status=&overdue=
POST /api/v1/dental/lab-order
PATCH /api/v1/dental/lab-order/{uuid}
POST /api/v1/dental/lab-order/{uuid}/transition
```
دسترسی: هر چهار نقش می‌بینند و ثبت می‌کنند. لابراتوار کار مشترک درمانگاه است.
### شاخص‌ها
| کلید | تعریف |
|---|---|
| `lab_cost_ratio` | جمع هزینهٔ لابراتوار تقسیم بر تولید |
| `lab_overdue_count` | تعداد سفارش گذشته از موعد |
| `lab_turnaround_days` | میانگین فاصلهٔ ارسال تا دریافت |
### تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM5-03 | موجودیت `Lab` و مخزن | یکتایی نام در محیط |
| DM5-04 | موجودیت `LabOrder` و ماشین حالت | گذار غیرمجاز `AppException` |
| DM5-05 | اندپوینت‌های لابراتوار | همهٔ حالت‌های دسترسی |
| DM5-06 | job هشدار تأخیر | سفارش نهایی‌شده علامت نمی‌خورد |
| DM5-07 | سه شاخص لابراتوار | مخرج صفر |
| DM5-08 | صفحهٔ لابراتوار در پنل | فیلتر وضعیت و تأخیر |
---
## ۳. چارت پریودنتال — تازه است
### مدل داده
`PeriodontalExam` — جدول `dental_periodontal_exams`
| ستون | نوع |
|---|---|
| `id`, `uuid` | |
| `chart_id` | int |
| `examined_at` | int |
| `examined_by_user_id` | int, nullable |
| `note` | string 500, nullable |
`PeriodontalMeasurement` — جدول `dental_periodontal_measurements`
| ستون | نوع | توضیح |
|---|---|---|
| `exam_id` | int | `ON DELETE CASCADE` |
| `tooth_number` | smallint | |
| `site` | smallint | ۱ تا ۶ |
| `pocket_depth` | smallint | میلی‌متر |
| `recession` | smallint | |
| `bleeding_on_probing` | bool | |
| `mobility` | smallint | ۰ تا ۳ |
**چرا معاینه جدا از اندازه‌گیری:**
پریو دنباله‌ای است. مقایسهٔ معاینهٔ امروز با شش ماه پیش تمام ارزش این چارت است.
اگر اندازه‌ها روی خود دندان بازنویسی شوند، آن مقایسه از بین می‌رود.
این دقیقاً قرینهٔ `ToothStatus` است که عمداً فقط وضعیت جاری را نگه می‌دارد.
ثبت کامل یک معاینه ۱۹۲ عدد است.
پس فرم باید صفحه‌کلیدمحور باشد و با `Tab` پیش برود، وگرنه کسی استفاده‌اش نمی‌کند.
### تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM5-09 | دو موجودیت پریو | یکتایی دندان و سایت در معاینه |
| DM5-10 | اندپوینت ثبت و خواندن معاینه | ثبت دسته‌ای در یک درخواست |
| DM5-11 | فرم پریو صفحه‌کلیدمحور | حرکت با `Tab` بین سایت‌ها |
| DM5-12 | نمای مقایسهٔ دو معاینه | اختلاف با رنگ نشان داده می‌شود |
---
## ۴. استریلیزاسیون — تازه است
### مدل داده
`SterilizationCycle` — جدول `dental_sterilization_cycles`
| ستون | نوع |
|---|---|
| `id`, `uuid` | |
| `entity_type`, `entity_id` | |
| `device_resource_id` | int, nullable |
| `program` | string 50 |
| `started_at`, `finished_at` | int |
| `chemical_indicator_ok` | bool |
| `biological_test_at` | int, nullable |
| `result` | string 20 |
| `operator_user_id` | int, nullable |
| `note` | string 500, nullable |
اتوکلاو به‌عنوان `ClinicResource` تعریف می‌شود، نه یک جدول دستگاه تازه.
دلیل: نوع منبع از قبل قابل تعریف است و تقویم و دسترسی‌اش هم همان‌جاست.
در این فاز، سیکل استریل به گردش کار درمان گره نمی‌خورد.
فقط ثبت و گزارش است.
گره‌زدن ست ابزار به جلسهٔ درمان کار بزرگی است و باید جدا تصمیم‌گیری شود.
### تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM5-13 | موجودیت سیکل استریل | ثبت بدون دستگاه هم ممکن است |
| DM5-14 | اندپوینت ثبت و فهرست | فیلتر بازه و نتیجه |
| DM5-15 | یادآور تست بیولوژیک هفتگی | نبود تست در هفته، هشدار داشبورد |
| DM5-16 | صفحهٔ استریلیزاسیون در پنل | گزارش قابل چاپ |
---
## ۵. تصاویر بالینی — روی سیستم موجود
`PatientAttachment` از قبل هست و به پرونده وصل است.
کار این بخش فقط افزودن دو ستون اختیاری است:
```
tooth_numbers json nullable
image_type string 20 nullable periapical | bitewing | opg | cbct | photo
```
**چرا ستون روی همان جدول و نه جدول دندانی جدا:**
برخلاف ویژگی خدمت که تنظیمات مشترک همهٔ حوزه‌هاست، ضمیمه سند خود پرونده است و هر حوزه‌ای می‌تواند تصویر داشته باشد.
جدول جدا یعنی یک ضمیمه در دو جا و دو مسیر آپلود.
### تسک‌ها
| کد | تسک | معیار پذیرش |
|---|---|---|
| DM5-17 | دو ستون روی `PatientAttachment` | ضمیمهٔ بدون دندان مثل قبل کار می‌کند |
| DM5-18 | فیلتر ضمیمه بر اساس دندان در تب چارت | کلیک روی دندان، تصاویرش را نشان می‌دهد |
---
## ۶. رضایت آگاهانه — خارج از این سند
فرم رضایت آگاهانه در همهٔ حوزه‌ها لازم است، نه فقط دندانپزشکی.
ساختنش داخل ماژول دندانپزشکی یعنی حوزهٔ بعدی باید دوباره بسازدش.
پیشنهاد: سند جدا، در سطح پرونده بیمار.
@@ -0,0 +1,245 @@
# محتوای بستهٔ پیش‌فرض دندانپزشکی
> **وضعیت: پیش‌نویس.**
> این فهرست توسط دندانپزشک تأیید نشده است.
> تسک `DM1-15` مسدودکننده است و بدون آن فاز ۱ بسته نمی‌شود.
دلیل سختگیری: این فهرست در همهٔ کلینیک‌های نصب‌کننده کپی می‌شود.
اصلاح یک نام غلط بعد از نصب در پنجاه کلینیک، ممکن نیست.
قیمت همهٔ خدمات صفر است و مدیر باید تعرفهٔ خودش را وارد کند.
---
## ۱. گروه‌های خدمات
سیزده گروه، همه در سطح ریشه.
| کلید قالب | نام | ترتیب |
|---|---|---|
| `dx` | تشخیص و معاینه | ۱ |
| `radiology` | رادیولوژی | ۲ |
| `preventive` | پیشگیری | ۳ |
| `restorative` | ترمیمی | ۴ |
| `endodontics` | درمان ریشه | ۵ |
| `periodontics` | جراحی لثه و پریو | ۶ |
| `oral_surgery` | جراحی دهان و فک | ۷ |
| `fixed_prostho` | پروتز ثابت | ۸ |
| `removable_prostho` | پروتز متحرک | ۹ |
| `implant` | ایمپلنت | ۱۰ |
| `orthodontics` | ارتودنسی | ۱۱ |
| `pediatric` | دندانپزشکی کودکان | ۱۲ |
| `cosmetic` | زیبایی | ۱۳ |
درخت تک‌سطحی است.
دلیل: عمق بیشتر بدون نیاز واقعی، فقط پیمایش را سخت می‌کند و `CatalogCategory` تا عمق ۴ را همیشه اجازه می‌دهد اگر بعداً لازم شد.
---
## ۲. خدمات
ستون‌ها:
- **هدف** مقدار `target_scope`
- **مبنا** مقدار `pricing_basis`
- **دقیقه** مدت پیش‌فرض نوبت
- **جلسه** تعداد جلسهٔ پیش‌فرض
- **اثر چارت** مقدار `chart_effect` که پروجکتور فاز ۲ استفاده می‌کند
### تشخیص و معاینه
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `dx_exam` | معاینه و مشاوره | `none` | `flat` | ۱۵ | ۱ | — |
| `dx_emergency` | ویزیت اورژانس | `none` | `flat` | ۲۰ | ۱ | — |
| `dx_full_chart` | معاینهٔ کامل و چارت‌نگاری | `mouth` | `flat` | ۳۰ | ۱ | — |
| `dx_perio_chart` | چارت پریودنتال | `mouth` | `flat` | ۳۰ | ۱ | — |
### رادیولوژی
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `rad_pa` | رادیوگرافی پری‌اپیکال | `tooth` | `per_tooth` | ۱۰ | ۱ | — |
| `rad_bw` | رادیوگرافی بایت‌وینگ | `quadrant` | `per_quadrant` | ۱۰ | ۱ | — |
| `rad_opg` | رادیوگرافی پانورامیک | `mouth` | `flat` | ۱۵ | ۱ | — |
| `rad_cbct` | سی‌بی‌سی‌تی | `mouth` | `flat` | ۲۰ | ۱ | — |
### پیشگیری
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `prev_scaling` | جرم‌گیری | `mouth` | `flat` | ۳۰ | ۱ | — |
| `prev_scaling_arch` | جرم‌گیری یک فک | `arch` | `per_arch` | ۲۰ | ۱ | — |
| `prev_polish` | پالیش و بروساژ | `mouth` | `flat` | ۲۰ | ۱ | — |
| `prev_fluoride` | فلوراید تراپی | `mouth` | `flat` | ۱۵ | ۱ | — |
| `prev_fissure_sealant` | فیشورسیلانت | `tooth` | `per_tooth` | ۱۵ | ۱ | `filled` |
### ترمیمی
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `rest_composite_1` | ترمیم کامپوزیت یک سطحی | `tooth_surface` | `per_surface` | ۳۰ | ۱ | `filled` |
| `rest_composite_2` | ترمیم کامپوزیت دو سطحی | `tooth_surface` | `per_surface` | ۴۵ | ۱ | `filled` |
| `rest_composite_3` | ترمیم کامپوزیت سه سطحی | `tooth_surface` | `per_surface` | ۶۰ | ۱ | `filled` |
| `rest_amalgam` | ترمیم آمالگام | `tooth_surface` | `per_surface` | ۳۰ | ۱ | `filled` |
| `rest_buildup` | بازسازی تاج | `tooth` | `per_tooth` | ۴۵ | ۱ | `filled` |
| `rest_post_core` | پست و کور | `tooth` | `per_tooth` | ۶۰ | ۱ | `filled` |
### درمان ریشه
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `endo_single` | درمان ریشه تک‌کاناله | `tooth` | `per_canal` | ۶۰ | ۱ | `root_canal` |
| `endo_multi` | درمان ریشه چندکاناله | `tooth` | `per_canal` | ۹۰ | ۲ | `root_canal` |
| `endo_retreat` | درمان مجدد ریشه | `tooth` | `per_canal` | ۹۰ | ۲ | `root_canal` |
| `endo_pulpotomy` | پالپوتومی | `tooth` | `per_tooth` | ۴۵ | ۱ | `root_canal` |
| `endo_apicoectomy` | آپیکواکتومی | `tooth` | `per_tooth` | ۹۰ | ۱ | `root_canal` |
### جراحی لثه و پریو
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `perio_srp` | جرم‌گیری عمقی و تسطیح ریشه | `quadrant` | `per_quadrant` | ۴۵ | ۱ | — |
| `perio_flap` | جراحی فلپ | `quadrant` | `per_quadrant` | ۹۰ | ۱ | — |
| `perio_gingivectomy` | ژنژیوکتومی | `quadrant` | `per_quadrant` | ۶۰ | ۱ | — |
| `perio_crown_lengthening` | افزایش طول تاج | `tooth` | `per_tooth` | ۶۰ | ۱ | — |
| `perio_graft` | پیوند لثه | `quadrant` | `per_quadrant` | ۹۰ | ۱ | — |
### جراحی دهان و فک
| کلید | نام | هدف | مبنا | دقیقه | جلسه | اثر چارت |
|---|---|---|---|---|---|---|
| `surg_extraction` | کشیدن دندان ساده | `tooth` | `per_tooth` | ۳۰ | ۱ | `extracted` |
| `surg_extraction_surgical` | کشیدن دندان جراحی | `tooth` | `per_tooth` | ۶۰ | ۱ | `extracted` |
| `surg_wisdom` | جراحی دندان عقل نهفته | `tooth` | `per_tooth` | ۹۰ | ۱ | `extracted` |
| `surg_root_remnant` | خارج کردن ریشهٔ باقی‌مانده | `tooth` | `per_tooth` | ۴۵ | ۱ | `extracted` |
| `surg_biopsy` | نمونه‌برداری | `mouth` | `flat` | ۴۵ | ۱ | — |
### پروتز ثابت
| کلید | نام | هدف | مبنا | دقیقه | جلسه | لابراتوار | اثر چارت |
|---|---|---|---|---|---|---|---|
| `fixed_pfm_crown` | روکش پرسلن روی فلز | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `crown` |
| `fixed_zirconia_crown` | روکش زیرکونیا | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `crown` |
| `fixed_bridge_unit` | هر واحد بریج | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `crown` |
| `fixed_inlay_onlay` | اینله و آنله | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `filled` |
| `fixed_temp_crown` | روکش موقت | `tooth` | `per_unit` | ۳۰ | ۱ | خیر | `crown` |
### پروتز متحرک
| کلید | نام | هدف | مبنا | دقیقه | جلسه | لابراتوار |
|---|---|---|---|---|---|---|
| `remov_complete_denture` | دست دندان کامل | `arch` | `per_arch` | ۶۰ | ۵ | بله |
| `remov_partial_acrylic` | پارسیل آکریلی | `arch` | `per_arch` | ۶۰ | ۴ | بله |
| `remov_partial_frame` | پارسیل فریم فلزی | `arch` | `per_arch` | ۶۰ | ۵ | بله |
| `remov_reline` | ریلاین | `arch` | `per_arch` | ۳۰ | ۱ | بله |
| `remov_repair` | تعمیر پروتز | `arch` | `per_arch` | ۳۰ | ۱ | بله |
### ایمپلنت
| کلید | نام | هدف | مبنا | دقیقه | جلسه | لابراتوار | اثر چارت |
|---|---|---|---|---|---|---|---|
| `impl_fixture` | کاشت فیکسچر | `tooth` | `per_unit` | ۹۰ | ۱ | خیر | `implant` |
| `impl_abutment` | اباتمنت | `tooth` | `per_unit` | ۴۵ | ۱ | بله | `implant` |
| `impl_crown` | روکش روی ایمپلنت | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `implant` |
| `impl_bone_graft` | پیوند استخوان | `tooth` | `per_unit` | ۹۰ | ۱ | خیر | — |
| `impl_sinus_lift` | سینوس لیفت | `quadrant` | `per_quadrant` | ۱۲۰ | ۱ | خیر | — |
### ارتودنسی
| کلید | نام | هدف | مبنا | دقیقه | جلسه |
|---|---|---|---|---|---|
| `ortho_consult` | مشاورهٔ ارتودنسی | `none` | `flat` | ۳۰ | ۱ |
| `ortho_fixed` | ارتودنسی ثابت دو فک | `mouth` | `flat` | ۶۰ | ۱۸ |
| `ortho_fixed_single_arch` | ارتودنسی ثابت یک فک | `arch` | `per_arch` | ۶۰ | ۱۲ |
| `ortho_adjust` | ویزیت تنظیم | `mouth` | `per_session` | ۲۰ | ۱ |
| `ortho_retainer` | پلاک نگهدارنده | `arch` | `per_arch` | ۳۰ | ۱ |
### دندانپزشکی کودکان
| کلید | نام | هدف | مبنا | دقیقه | جلسه | نوع دندان | اثر چارت |
|---|---|---|---|---|---|---|---|
| `ped_exam` | معاینهٔ کودک | `none` | `flat` | ۲۰ | ۱ | `any` | — |
| `ped_filling` | ترمیم دندان شیری | `tooth_surface` | `per_surface` | ۳۰ | ۱ | `primary_only` | `filled` |
| `ped_pulpotomy` | پالپوتومی شیری | `tooth` | `per_tooth` | ۴۵ | ۱ | `primary_only` | `root_canal` |
| `ped_ssc` | روکش استیل زنگ‌نزن | `tooth` | `per_unit` | ۴۵ | ۱ | `primary_only` | `crown` |
| `ped_extraction` | کشیدن دندان شیری | `tooth` | `per_tooth` | ۲۰ | ۱ | `primary_only` | `extracted` |
| `ped_space_maintainer` | فضانگهدار | `quadrant` | `per_quadrant` | ۳۰ | ۱ | `primary_only` | — |
### زیبایی
| کلید | نام | هدف | مبنا | دقیقه | جلسه | لابراتوار | اثر چارت |
|---|---|---|---|---|---|---|---|
| `cosm_bleaching_office` | بلیچینگ مطبی | `mouth` | `flat` | ۶۰ | ۱ | خیر | — |
| `cosm_bleaching_home` | بلیچینگ خانگی | `mouth` | `flat` | ۳۰ | ۱ | بله | — |
| `cosm_veneer_composite` | ونیر کامپوزیت | `tooth` | `per_unit` | ۶۰ | ۱ | خیر | `filled` |
| `cosm_veneer_porcelain` | لمینت سرامیکی | `tooth` | `per_unit` | ۶۰ | ۲ | بله | `crown` |
| `cosm_gum_contouring` | اصلاح طرح لبخند لثه | `arch` | `per_arch` | ۶۰ | ۱ | خیر | — |
جمع: هفتاد و یک خدمت.
---
## ۳. نوع منبع
| کلید | کد | نام |
|---|---|---|
| `unit` | `dental_unit` | یونیت دندانپزشکی |
| `sterilizer` | `autoclave` | اتوکلاو |
نوع منبع ساخته می‌شود، ولی هیچ منبعی ساخته نمی‌شود.
تعداد یونیت را فقط خود مدیر می‌داند.
نوع `autoclave` فقط در فاز ۵ استفاده می‌شود، ولی چون تعریف نوع منبع ارزان است، همان اول ساخته می‌شود تا مدیر بتواند دستگاهش را ثبت کند.
---
## ۴. پروتکل‌ها
فقط برای خدماتی که واقعاً چندجلسه‌ای‌اند.
| کلید | خدمت | جلسه | فاصله روز |
|---|---|---|---|
| `proto_endo_multi` | `endo_multi` | ۲ | ۷ |
| `proto_endo_retreat` | `endo_retreat` | ۲ | ۷ |
| `proto_fixed_crown` | `fixed_pfm_crown` | ۲ | ۱۰ |
| `proto_zirconia` | `fixed_zirconia_crown` | ۲ | ۱۰ |
| `proto_denture` | `remov_complete_denture` | ۵ | ۷ |
| `proto_partial_frame` | `remov_partial_frame` | ۵ | ۷ |
| `proto_impl_crown` | `impl_crown` | ۲ | ۱۴ |
| `proto_ortho_fixed` | `ortho_fixed` | ۱۸ | ۲۸ |
پزشک سرپرست پروتکل هنگام نصب مشخص نمی‌شود.
اگر محیط فقط یک پزشک دارد همان انتخاب می‌شود، وگرنه خالی می‌ماند و مدیر باید تکمیل کند.
دلیل: انتخاب خودکار پزشک اشتباه، مسئولیت بالینی را به کسی نسبت می‌دهد که قبول نکرده.
---
## ۵. مواد مصرفی — فاز ۵
| کلید | نام | واحد |
|---|---|---|
| `cons_composite` | کامپوزیت | سرنگ |
| `cons_bond` | باندینگ | میلی‌لیتر |
| `cons_anesthetic` | کارپول بی‌حسی | عدد |
| `cons_needle` | سوزن تزریق | عدد |
| `cons_rotary_file` | فایل روتاری | عدد |
| `cons_gutta` | گوتاپرکا | عدد |
| `cons_glove` | دستکش | جفت |
| `cons_mask` | ماسک | عدد |
| `cons_suction_tip` | ساکشن یک‌بار مصرف | عدد |
| `cons_impression` | ماده قالب‌گیری | گرم |
---
## ۶. چک‌لیست بازبینی تخصصی
دندانپزشک بازبین باید این‌ها را جواب بدهد:
۱. نام هر خدمت با زبان رایج مطب می‌خواند یا اصطلاح کتابی است؟
۲. مدت پیش‌فرض هر خدمت واقع‌بینانه است؟
۳. مبنای قیمت هر خدمت درست است؟ مثلاً درمان ریشه به‌ازای کانال قیمت می‌خورد یا به‌ازای دندان؟
۴. کدام خدمت جا افتاده که در هر مطب هست؟
۵. کدام خدمت اضافه است و در مطب عمومی استفاده نمی‌شود؟
۶. اثر چارت هر خدمت درست است؟
۷. تعداد جلسه و فاصلهٔ پروتکل‌ها منطقی است؟