- 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.
274 lines
14 KiB
Markdown
274 lines
14 KiB
Markdown
# فاز ۱ — حوزه در سطح محیط و نصب پیشفرضهای دندانپزشکی
|
||
|
||
> پیشنیاز: ندارد.
|
||
> خروجی قابل تست: مطب یا کلینیک، حوزهٔ دندانپزشکی را انتخاب میکند و کاتالوگ خدمات دندانپزشکیاش ساخته میشود.
|
||
|
||
---
|
||
|
||
## ۱. هدف
|
||
|
||
سه چیز:
|
||
|
||
۱. مطب تکپزشک هم بتواند حوزهٔ فعالیت انتخاب کند، نه فقط کلینیک.
|
||
۲. با انتخاب دندانپزشکی، دستهبندیها و خدمات و نوع منبع یونیت و پروتکلها ساخته شوند.
|
||
۳. هر خدمت دندانی بداند روی دندان انجام میشود یا روی فک یا روی کل دهان.
|
||
|
||
---
|
||
|
||
## ۲. مدل داده
|
||
|
||
### ۲.۱ تغییر روی دامنهٔ موجود
|
||
|
||
روی `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` مسدودکننده است.
|
||
|
||
**نصب خودکار روی محیطی که کاربر نمیخواست.**
|
||
مهار: فقط وقتی کاتالوگ خالی است، و ردیفها با قیمت صفر و قابل غیرفعال کردن.
|
||
|
||
**تعریف «کاتالوگ خالی» ناپایدار.**
|
||
اگر کلینیکی یک دستهٔ آزمایشی ساخته باشد، نصب خودکار انجام نمیشود و کاربر گیج میشود.
|
||
مهار: پنل همیشه وضعیت نصب و دکمه را نشان میدهد، پس مسیر دوم همیشه در دسترس است.
|