- 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.
211 lines
12 KiB
Markdown
211 lines
12 KiB
Markdown
# بازطراحی صفحه 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`).
|
||
۷. مبالغ سهم بیمه/بیمار در این صفحه با صفحه پرداخت و مودال فاکتور **دقیقاً** یکی است.
|