Files
clinicpro/.claude/prompt/claims-dashboard-redesign.md
T
hamed b3a5cda808 Refactor insurance share calculation logic in PatientService
- Consolidated the calculation of patient and insurance shares into a single method using BillingCalculator.
- Introduced new fields in PatientSession to store breakdown of insurance shares and patient share.
- Updated the API responses to include the new fields for consistency across payment, invoice, and claims dashboard.
- Added migration to backfill existing sessions with appropriate values for the new fields.
- Implemented tests to ensure the correctness of the new logic and verify that the breakdown sums to the gross total.
- Redesigned the claims dashboard to provide a more user-friendly overview of patient claims and their statuses.
2026-07-18 22:56:46 +03:30

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# بازطراحی صفحه 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`).
۷. مبالغ سهم بیمه/بیمار در این صفحه با صفحه پرداخت و مودال فاکتور **دقیقاً** یکی است.