- 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.
12 KiB
بازطراحی صفحه 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):
<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()) با هر آیتم:
{
"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)، هر رکورد شامل:
{
"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 لازم است:
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(sizelg) با توضیحات کامل + لاگ کامل تغییرات بهصورت 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).
۷. مبالغ سهم بیمه/بیمار در این صفحه با صفحه پرداخت و مودال فاکتور دقیقاً یکی است.