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

12 KiB
Raw Blame History

بازطراحی صفحه 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 (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). ۷. مبالغ سهم بیمه/بیمار در این صفحه با صفحه پرداخت و مودال فاکتور دقیقاً یکی است.