# فاز ۲ — چارت دندان و هدف‌گیری دندان روی خدمت ویزیت > پیش‌نیاز: فاز ۱. > خروجی قابل تست: دندانپزشک وضعیت دندان‌های بیمار را می‌بیند، ثبت خدمت روی دندان انجام می‌دهد و چارت خودکار به‌روز می‌شود. --- ## ۱. هدف سه چیز: ۱. هر بیمار در هر محیط یک چارت دندان داشته باشد. ۲. ثبت خدمت در ویزیت بتواند دندان و سطح را هدف بگیرد. ۳. چارت بعد از ثبت خدمت خودکار به‌روز شود، و وضعیت‌های قدیمی هم دستی قابل ثبت باشند. --- ## ۲. شماره‌گذاری دندان استاندارد `FDI` دو رقمی، همان `ISO 3950`. ``` دائمی : 11–18, 21–28, 31–38, 41–48 شیری : 51–55, 61–65, 71–75, 81–85 ``` ذخیره به‌صورت `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 از پیش‌فرض ساخته می‌شوند. **سمت چپ و راست جابه‌جا.** خطای رایج در چارت دندان و در سند پزشکی خطرناک است. مهار: قرارداد جهت در کامنت بالای کامپوننت، و یک تست که دندان ۱۱ را در جای درست ادعا می‌کند.