# فاز ۲ — چارت دندان و هدف‌گیری دندان روی خدمت ویزیت

> پیش‌نیاز: فاز ۱.
> خروجی قابل تست: دندانپزشک وضعیت دندان‌های بیمار را می‌بیند، ثبت خدمت روی دندان انجام می‌دهد و چارت خودکار به‌روز می‌شود.

---

## ۱. هدف

سه چیز:

۱. هر بیمار در هر محیط یک چارت دندان داشته باشد.
۲. ثبت خدمت در ویزیت بتواند دندان و سطح را هدف بگیرد.
۳. چارت بعد از ثبت خدمت خودکار به‌روز شود، و وضعیت‌های قدیمی هم دستی قابل ثبت باشند.

---

## ۲. شماره‌گذاری دندان

استاندارد `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 از پیش‌فرض ساخته می‌شوند.

**سمت چپ و راست جابه‌جا.**
خطای رایج در چارت دندان و در سند پزشکی خطرناک است.
مهار: قرارداد جهت در کامنت بالای کامپوننت، و یک تست که دندان ۱۱ را در جای درست ادعا می‌کند.
