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