# بازطراحی صفحه Claims به داشبورد مدیریتی بیمار-محور

## پروژه

`clinicpro` (Backend Symfony + پنل ادمین React)

پیش‌نیاز: `clinicpro/.claude/prompt/insurance-shared-calculation.md` باید **قبل** از این پرامپت اجرا شود — ستون‌های سهم بیمه/بیمار روی session و منطق واحد محاسبه از آنجا می‌آید.

## زمینه

صفحه `/admin/claims` امروز یک لیست کارتی از رکوردهای Claim است: هر Claim یک کارت، و داخل هر کارت یک `<table>` تودرتو از اقلام. این نه DataTable استاندارد پروژه است و نه برای پیگیری پرونده‌های بیمه‌ی یک بیمار کاربردی — کاربر نمی‌تواند ببیند «بیمار X مجموعاً چقدر ادعا دارد و چقدر وصول شده».

## هدف

تبدیل صفحه به داشبورد دو سطحی:
- **سطح ۱ — لیست بیماران:** هر ردیف یک بیمار با تجمیع مبالغ و وضعیت کلی.
- **سطح ۲ — جزئیات یک بیمار:** همه‌ی درخواست‌های بیمه‌ی آن بیمار با جزئیات کامل و لاگ تغییرات.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `assets/admin/pages/ClaimsPage.tsx` | صفحه فعلی (۳۶۶ خط) — بازنویسی می‌شود |
| `assets/admin/App.tsx:237` | ثبت route `claims`؛ نقش‌های `['doctor','clinic']` + `blockClinicScope` + `<FeatureGate feature="insurance">` |
| `src/Billing/Controller/BillingController.php:252` | `GET /api/v1/billing/claims` |
| `src/Billing/Controller/BillingController.php:286` | `POST /api/v1/billing/claims/{uuid}/{submit\|approve\|reject\|pay}` |
| `src/Billing/Controller/BillingController.php:332` | `GET /api/v1/billing/reports/insurance-debt` |
| `src/Billing/Entity/Claim.php` | `insuranceId`, `insuranceKind`, `totalClaimedRials`, `totalApprovedRials`, `totalPaidRials`, `status`, `TRANSITIONS` (`:26`), `rejectReason`, `submittedAt`, `settledAt` |
| `src/Billing/Entity/ClaimItem.php` | `invoiceItemId`, `claimedRials` |
| `src/Billing/Entity/Invoice.php` / `InvoiceItem.php` | مبالغ اصلی و سهم‌ها |
| `src/Billing/Service/ClaimService.php` | `buildClaim()` و انتقال وضعیت |
| `assets/admin/components/ui/` | `DataTable`, `Pagination`, `SearchableSelect`, `PageHeader`, `Modal`, `StatCard`, `StatusBadge`, `PersianDatePicker`, `FeatureGate` |

## وضعیت فعلی

`ClaimsPage.tsx` — کارت به‌ازای هر Claim، totalها به‌صورت یک رشته متنی درهم (`:271-276`):

```
ادعا … · تأیید … · پرداخت … · دلیل رد: …
```

و جدول داخلی (`:302-337`):

```tsx
<thead>
  <tr style={{ color: 'var(--text-3)', textAlign: 'right' }}>
    <th>شرح</th><th>تاریخ مراجعه</th>
    <th style={{ textAlign: 'center' }}>تعداد</th>
    <th style={{ textAlign: 'left' }}>مبلغ کل</th>
    <th style={{ textAlign: 'left' }}>سهم بیمه (ادعا)</th>
    <th style={{ textAlign: 'left' }}>تأییدشده</th>
  </tr>
</thead>
```

مشکلات موجود که باید در بازطراحی رفع شوند:
- `GET /api/v1/billing/reports/insurance-debt` **دو بار** با دو query key جدا fetch می‌شود (`:116`, `:122`) — یکی برای پنل بدهی، یکی برای پر کردن dropdown بیمه.
- فیلترها با URL همگام نیستند (همه `useState`)؛ رفرش صفحه همه فیلترها را پاک می‌کند.
- وضعیت‌ها در `STATUS_META` محلی تعریف شده‌اند (`:69-75`) به‌جای `StatusBadge`.
- بدون `DoctorClaimsPage.tsx` اشتباه گرفته نشود — آن صفحه (`/admin/doctor-claims`) مربوط به ادعای مالکیت پروفایل پزشک است، نه بیمه.

## وظایف

### ۱. Backend — endpoint تجمیع بیمار-محور

endpoint جدید:

```
GET /api/v1/billing/claims/by-patient
```

پارامترها: `page`, `limit`, `search` (نام/موبایل/کد ملی), `insurance_id`, `doctor_id`, `clinic_id`, `status`, `payment_status`, `from`, `to` (Unix), `sort`, `dir`.

پاسخ paginated (`$this->paginated()`) با هر آیتم:

```json
{
  "patient_uuid": "...",
  "full_name": "...",
  "mobile": "...",
  "national_code": "...",
  "claims_count": 0,
  "total_services_rials": 0,
  "total_insurance_rials": 0,
  "total_patient_rials": 0,
  "total_approved_rials": 0,
  "total_paid_rials": 0,
  "overall_status": "pending|submitted|approved|rejected|paid|mixed",
  "last_activity_at": 0
}
```

قاعده `overall_status`: اگر همه Claimهای بیمار یک وضعیت دارند همان؛ در غیر این صورت `mixed`.

پیاده‌سازی با DQL و array hydration (`->getArrayResult()`)، تجمیع در SQL (`GROUP BY patient`) — نه در PHP روی کل رکوردها.

### ۲. Backend — endpoint جزئیات یک بیمار

```
GET /api/v1/billing/claims/by-patient/{patientUuid}
```

لیست همه Claimهای آن بیمار (paginated)، هر رکورد شامل:

```json
{
  "uuid": "...",
  "visit_date": 0,
  "doctor_name": "...",
  "clinic_name": "...",
  "service_title": "...",
  "service_base_rials": 0,
  "coverage_percent": 0,
  "coverage_rials": 0,
  "patient_share_rials": 0,
  "insurance_share_rials": 0,
  "insurance_title": "...",
  "insurance_kind": "base|supplementary",
  "status": "...",
  "submitted_at": 0,
  "settled_at": 0,
  "tracking_number": null,
  "reject_reason": null,
  "description": null,
  "allowed_transitions": ["submit"],
  "logs": [
    { "at": 0, "from": "pending", "to": "submitted", "by": "نام کاربر", "note": null }
  ]
}
```

`allowed_transitions` از `Claim::TRANSITIONS` بیاید تا فرانت دکمه‌های مجاز را خودش hardcode نکند.

### ۳. Backend — شماره پیگیری و لاگ تغییرات

دو مورد در دیتابیس وجود ندارند و باید اضافه شوند:

**الف) `trackingNumber`** روی `Claim` (string nullable) — شماره پرونده/پیگیری بیمه. هنگام `submit` قابل ورود باشد و در `POST /api/v1/billing/claims/{uuid}/submit` به‌عنوان فیلد اختیاری بدنه پذیرفته شود.

**ب) `ClaimStatusLog`** — entity جدید:

| فیلد | نوع |
|------|-----|
| `claimId` | int |
| `fromStatus` | string nullable |
| `toStatus` | string |
| `note` | text nullable |
| `createdBy` | userId |
| `createdAt` | Unix int |

در `ClaimService` هر جا انتقال وضعیت انجام می‌شود یک رکورد لاگ نوشته شود. migration لازم است:

```bash
ddev exec php bin/console make:migration
ddev exec php bin/console doctrine:migrations:migrate -n
```

**Backfill:** برای Claimهای موجود از `submittedAt` / `settledAt` رکوردهای لاگ تقریبی ساخته شود تا صفحه جزئیات برای داده قدیمی خالی نباشد.

### ۴. Frontend — سطح ۱: DataTable بیماران

`ClaimsPage.tsx` بازنویسی شود با `DataTable` استاندارد پروژه (نه کارت):

ستون‌ها: نام بیمار | موبایل | کد ملی | تعداد درخواست | مجموع خدمات | مجموع سهم بیمه | مجموع سهم بیمار | وضعیت کلی (`StatusBadge`)

بالای جدول: ردیف `StatCard` با مجموع کل خدمات / کل سهم بیمه / وصول‌شده / مانده وصول‌نشده — از `GET /api/v1/billing/reports/insurance-debt` که باید **فقط یک بار** fetch شود (یک `useQuery`، خروجی‌اش هم پنل و هم options فیلتر بیمه را بدهد).

کلیک روی ردیف → سطح ۲.

### ۵. Frontend — سطح ۲: جزئیات بیمار

مسیر جدید `/admin/claims/:patientUuid` در `App.tsx` با همان roleها و `<FeatureGate feature="insurance">`.

- `PageHeader` با breadcrumb: پرونده‌های بیمه ← نام بیمار.
- `DataTable` از درخواست‌های بیمه با ستون‌های: تاریخ مراجعه | پزشک | کلینیک | سرویس | مبلغ اصلی | پوشش بیمه (درصد + مبلغ) | سهم بیمار | سهم بیمه | وضعیت | تاریخ ارسال | تاریخ پرداخت | شماره پیگیری.
- `actions?(row)` → دکمه‌های انتقال وضعیت بر اساس `allowed_transitions`.
- کلیک روی ردیف → `Modal` (size `lg`) با توضیحات کامل + **لاگ کامل تغییرات** به‌صورت timeline.
- عملیات `reject` باید `reject_reason` بگیرد؛ `submit` باید `tracking_number` اختیاری بگیرد.

### ۶. Frontend — فیلتر، جستجو، مرتب‌سازی، URL sync

فیلترها در هر دو سطح: بیمار (فقط سطح ۱) | بیمه | پزشک | کلینیک | وضعیت Claim | بازه زمانی (`PersianDatePicker` + میان‌بر یک ماه/یک سال اخیر) | وضعیت پرداخت.

همه‌ی فیلترها + `page` + `sort`/`dir` باید در **query string آدرس** ذخیره شوند (`useSearchParams`) تا رفرش و اشتراک‌گذاری لینک کار کند. این رفع مشکل فعلی است.

مرتب‌سازی سرور-ساید از طریق `sort`/`dir` روی: نام بیمار، تعداد درخواست، مجموع خدمات، مجموع سهم بیمه، آخرین فعالیت.

## نکات مهم

- **الزامی:** صفحه جدید با تم، Layout و کامپوننت‌های موجود ساخته شود — بدون طراحی جدید. از `components/ui/` استفاده شود، نه جدول دست‌ساز.
- برای dropdownها **همیشه** `SearchableSelect`، هرگز `<select>` نیتیو.
- `StatusBadge` فعلاً `type` برای `claim` ندارد — نوع جدید `claim` با نگاشت pending=خاکستری، submitted=آبی، approved=کهربایی، rejected=قرمز، paid=سبز، mixed=بنفش اضافه شود و `STATUS_META` محلی حذف شود.
- استایل: از design tokenها (`var(--text-3)`, `.card`, `.btn primary sm`, `.badge`) استفاده شود. هگز hardcode ممنوع.
- پاسخ paginated: items از `data?.data`، total از `data?.meta?.totalRecords`. single: `data?.data` (گاهی double-nested).
- تاریخ‌ها Unix timestamp صحیح؛ تبدیل بازه با `toUnix` / `endOfDayUnix` مثل کد فعلی (`ClaimsPage.tsx:21-30`)؛ نمایش شمسی با `formatDate` / `formatDateTime`.
- مبالغ با `formatRial`؛ مرز ریال/تومان رعایت شود.
- همه controllerها از `BaseController`؛ پاسخ‌ها با `$this->paginated()` / `$this->success()` / `$this->error()`.
- انتقال وضعیت غیرمجاز باید سمت سرور هم reject شود (`Claim::TRANSITIONS` منبع حقیقت است) — اتکا به مخفی‌کردن دکمه در UI کافی نیست.
- تست با کاربر `09390039833 / 09390039833` روی `https://clinic-pro.ddev.site`.
- بعد از تغییر API، `clinicpro/docs/api/` به‌روز شود.

## تست پذیرش

۱. `/admin/claims` لیست بیماران با تجمیع درست نمایش می‌دهد؛ جمع ستون سهم بیمه با `insurance-debt` هم‌خوان است.
۲. کلیک روی بیمار → صفحه جزئیات با همه‌ی رکوردهای بیمه‌ی همان بیمار.
۳. انتقال وضعیت یک Claim (submit با شماره پیگیری، سپس approve، سپس pay) → لاگ در timeline ثبت می‌شود.
۴. reject بدون دلیل → خطا؛ با دلیل → ثبت و نمایش دلیل.
۵. فیلتر بازه زمانی + بیمه اعمال شود، صفحه رفرش شود → فیلترها از URL بازیابی می‌شوند.
۶. کاربر بدون فیچر `insurance` → صفحه در دسترس نیست (`FeatureGate`).
۷. مبالغ سهم بیمه/بیمار در این صفحه با صفحه پرداخت و مودال فاکتور **دقیقاً** یکی است.
