Files
clinicpro/docs/new_feture/dental-module/phase-1-preset.md
T
hamed 2f030edef1 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.
2026-08-20 16:21:08 +03:30

274 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# فاز ۱ — حوزه در سطح محیط و نصب پیش‌فرض‌های دندانپزشکی
> پیش‌نیاز: ندارد.
> خروجی قابل تست: مطب یا کلینیک، حوزهٔ دندانپزشکی را انتخاب می‌کند و کاتالوگ خدمات دندانپزشکی‌اش ساخته می‌شود.
---
## ۱. هدف
سه چیز:
۱. مطب تک‌پزشک هم بتواند حوزهٔ فعالیت انتخاب کند، نه فقط کلینیک.
۲. با انتخاب دندانپزشکی، دسته‌بندی‌ها و خدمات و نوع منبع یونیت و پروتکل‌ها ساخته شوند.
۳. هر خدمت دندانی بداند روی دندان انجام می‌شود یا روی فک یا روی کل دهان.
---
## ۲. مدل داده
### ۲.۱ تغییر روی دامنهٔ موجود
روی `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` مسدودکننده است.
**نصب خودکار روی محیطی که کاربر نمی‌خواست.**
مهار: فقط وقتی کاتالوگ خالی است، و ردیف‌ها با قیمت صفر و قابل غیرفعال کردن.
**تعریف «کاتالوگ خالی» ناپایدار.**
اگر کلینیکی یک دستهٔ آزمایشی ساخته باشد، نصب خودکار انجام نمی‌شود و کاربر گیج می‌شود.
مهار: پنل همیشه وضعیت نصب و دکمه را نشان می‌دهد، پس مسیر دوم همیشه در دسترس است.