From 5c8fe8ece49579082b1fd9138444af08c39c50ea Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Sun, 19 Jul 2026 10:12:01 +0330
Subject: [PATCH] feat: add payments summary endpoint and UI redesign for
MyPaymentsPage
- Implemented a new API endpoint `/api/v1/my/billing/payments/summary` to provide a financial summary of payments with filters for national code, status, and date range.
- Updated the InvoiceRepository to aggregate totals for paid and unsettled invoices.
- Created a new hook `usePaymentsSummary` to fetch summary data in the frontend.
- Redesigned the MyPaymentsPage to align with the ClaimsPage structure, incorporating a design system, summary statistics, and improved filtering options.
- Added tests for the new payments summary endpoint to ensure correct functionality and filtering behavior.
---
.claude/prompt/my-payments-ui-redesign.md | 294 +++++++++++++++++++
assets/admin/components/ui/StatusBadge.tsx | 12 +-
assets/admin/hooks/useMyPayments.ts | 21 ++
assets/admin/pages/MyPaymentsPage.test.tsx | 46 ++-
assets/admin/pages/MyPaymentsPage.tsx | 239 ++++++++-------
assets/admin/types/index.ts | 3 +
docs/api/billing.md | 21 ++
src/Billing/Controller/BillingController.php | 46 ++-
src/Billing/Repository/InvoiceRepository.php | 30 ++
src/Billing/Service/InvoiceService.php | 12 +
tests/Billing/PaymentsSummaryTest.php | 140 +++++++++
11 files changed, 735 insertions(+), 129 deletions(-)
create mode 100644 .claude/prompt/my-payments-ui-redesign.md
create mode 100644 tests/Billing/PaymentsSummaryTest.php
diff --git a/.claude/prompt/my-payments-ui-redesign.md b/.claude/prompt/my-payments-ui-redesign.md
new file mode 100644
index 00000000..6e187cd7
--- /dev/null
+++ b/.claude/prompt/my-payments-ui-redesign.md
@@ -0,0 +1,294 @@
+# بازطراحی UI/UX صفحه «لیست پرداختها» (`/admin/my-payments`)
+
+## پروژه
+
+`clinicpro` — پنل ادمین React (`assets/admin/`) + یک اندپوینت خلاصه در بکاند Symfony (`src/Billing/`).
+
+## زمینه
+
+صفحهی `/admin/my-payments` ([MyPaymentsPage.tsx](clinicpro/assets/admin/pages/MyPaymentsPage.tsx)) با inline-styleهای دستی و یک `
` خام نوشته شده و از design-system پروژه استفاده نمیکند. در همان پنل، صفحهی `/admin/claims` ([ClaimsPage.tsx](clinicpro/assets/admin/pages/ClaimsPage.tsx)) الگوی درست و پختهی یک صفحهی لیست است: `PageHeader` با breadcrumb، ردیف `StatCard`، کارت فیلترها با `field-label`، میانبرهای بازهی زمانی، `DataTable` (سورت + جستجو + skeleton + empty state) و `Pagination`. هدف: همسطحکردن `my-payments` با همان الگو.
+
+## مشکل
+
+وضعیت فعلی صفحه:
+
+1. **بدون design-system** — جدول خام با `th`/`td` inline style بهجای `DataTable`. یعنی: بدون skeleton loading، بدون سورت، بدون empty state استاندارد.
+2. **بدون هیچ آمار خلاصهای** — کاربر هیچ دید کلی از مجموع مبلغ/تعداد/تسویهنشده ندارد (بر خلاف claims که ۴ `StatCard` دارد).
+3. **ستون `status` نمایش داده نمیشود** — با اینکه `PaymentRow.status` (`paid | unsettled`) از API میآید و فیلترش هم در UI هست، در جدول هیچ ستون وضعیتی وجود ندارد. کاربر فیلتر میکند ولی نتیجهاش را نمیبیند.
+4. **فیلترها بدون label و بدون کارت** — یک ردیف شناور بالای صفحه، بدون `field-label`، بدون دکمهی «پاککردن فیلترها»، بدون میانبر «یک ماه اخیر / یک سال اخیر».
+5. **فیلترها در state محلیاند، نه در query string** — رفرش صفحه یا اشتراک لینک، فیلترها و شمارهی صفحه را از بین میبرد. `ClaimsPage` این را با `useSearchParams` حل کرده.
+6. **جستجو فقط کد ملی است** — با `input` دستساز، در حالی که `DataTable` خودش `searchValue`/`onSearchChange` دارد.
+7. **`PersianDateInput` بهجای `PersianDatePicker`** — ناهماهنگ با claims و بدون `height={38}` همتراز با `SearchableSelect`.
+8. **action هدر بیربط است** — دکمهی «اضافه کردن بیمار» در صفحهی پرداختها منطق ندارد.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `clinicpro/assets/admin/pages/MyPaymentsPage.tsx` | صفحهای که بازنویسی میشود |
+| `clinicpro/assets/admin/pages/ClaimsPage.tsx` | **الگوی مرجع** — ساختار را از این کپی کن |
+| `clinicpro/assets/admin/hooks/useMyPayments.ts` | `usePayments`، `PaymentRow`، `MY_PAYMENTS_LIMIT` — hook خلاصه اینجا اضافه میشود |
+| `clinicpro/assets/admin/components/ui/DataTable.tsx` | جدول design-system |
+| `clinicpro/assets/admin/components/ui/StatCard.tsx` | کارت آمار (`tone: amber\|violet\|green\|pink`) |
+| `clinicpro/assets/admin/components/ui/StatusBadge.tsx` | بج وضعیت — نیاز به type جدید `invoice` |
+| `clinicpro/assets/admin/components/ui/PersianDatePicker.tsx` | انتخاب تاریخ همراستا با claims |
+| `clinicpro/assets/admin/types/index.ts` | تعریف `InvoiceListStatus` |
+| `clinicpro/src/Billing/Controller/BillingController.php` | اندپوینت `listPayments` (L163) — اندپوینت خلاصه کنارش |
+| `clinicpro/src/Billing/Service/InvoiceService.php` | `tenantInvoiceList` — متد خلاصه کنارش |
+| `clinicpro/docs/api/billing.md` | مستند API (Standing Rule) |
+| `clinicpro/assets/admin/pages/MyPaymentsPage.test.tsx` | تستهای موجود — باید بهروز شوند |
+
+## وضعیت فعلی
+
+`MyPaymentsPage.tsx` (خلاصهی بخشهای مشکلدار):
+
+```tsx
+const th: React.CSSProperties = { textAlign: 'right', padding: '12px 16px', fontWeight: 600 };
+const td: React.CSSProperties = { padding: '12px 16px' };
+
+const [page, setPage] = useState(1);
+const [nationalCode, setNationalCode] = useState('');
+const [status, setStatus] = useState('');
+const [from, setFrom] = useState('');
+const [to, setTo] = useState('');
+// ...
+
+
+
+
+
ردیف
+
نام بیمار
+
کد ملی
+
تاریخ
+
مبلغ پرداختشده
+
عملیات
+
+
+ ...
+```
+
+نوع ردیف (`useMyPayments.ts`) — دقت کن `status` موجود است ولی رندر نمیشود:
+
+```ts
+export type PaymentRowStatus = 'paid' | 'unsettled';
+
+export interface PaymentRow {
+ invoice_uuid: string;
+ patient_uuid: string;
+ patient_name: string | null;
+ national_code: string | null;
+ issued_at: number;
+ amount_rials: number;
+ status: PaymentRowStatus;
+}
+```
+
+---
+
+## وظایف
+
+### ۱. اندپوینت خلاصهی پرداختها (بکاند)
+
+طبق قاعدهی «اول بگرد، بعد توسعه بده، در آخر بساز»: هیچ اندپوینتی خلاصهی مالی tenant را برنمیگرداند (`/api/v1/billing/reports/insurance-debt` فقط بدهی بیمه است، نه پرداختهای بیمار). پس یک اندپوینت جدید لازم است — اما **همان فیلترهای `listPayments` را میپذیرد** تا کارتها با جدول همخوان بمانند.
+
+در `InvoiceService`:
+
+```php
+/**
+ * خلاصهی مالی صورتحسابهای tenant با همان فیلترهای tenantInvoiceList.
+ * @return array{total_rials:int, paid_rials:int, unsettled_rials:int, invoices_count:int}
+ */
+public function tenantInvoiceSummary(string $entityType, int $entityId, array $filters): array
+```
+
+پیادهسازی با یک DQL aggregate (`SUM`/`COUNT` + `CASE WHEN status = 'paid'`), **نه** با بارگذاری همهی ردیفها در PHP. شرطهای فیلتر (`national_code`, `status`, `from`, `to`) را دقیقاً از `tenantInvoiceList` بازاستفاده کن — منطق `where` را در یک متد private مشترک بگذار تا دو نسخه از هم واگرا نشوند (SOLID/DRY).
+
+در `BillingController` کنار `listPayments`:
+
+```php
+#[Route('/api/v1/my/billing/payments/summary', methods: ['GET'])]
+public function paymentsSummary(Request $request, #[CurrentUser] User $user): JsonResponse
+{
+ [$entityType, $entityId] = $this->resolveEntity($user);
+ if ($entityId === null) {
+ return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
+ }
+ // همان استخراج $filters که در listPayments هست
+ return $this->success($this->invoiceService->tenantInvoiceSummary($entityType, $entityId, $filters));
+}
+```
+
+**دقت:** payload را مستقیم پاس بده (`$this->success($summary)`) نه `['data' => $summary]` — در غیر اینصورت فرانت باید `data?.data?.data` بخواند (pitfall نامبرده در CLAUDE.md).
+
+**نکتهی مسیریابی:** روت `/payments/summary` نباید با روتهای پارامتری موجود تداخل کند؛ بعد از افزودن، با `ddev exec php bin/console debug:router | grep billing` تأیید کن.
+
+### ۲. hook خلاصه در فرانت
+
+در `assets/admin/hooks/useMyPayments.ts`:
+
+```ts
+export interface PaymentsSummary {
+ total_rials: number;
+ paid_rials: number;
+ unsettled_rials: number;
+ invoices_count: number;
+}
+
+/** خلاصهی مالی با همان فیلترهای لیست — کارتهای آمار همیشه با جدول همخوان میمانند. */
+export function usePaymentsSummary(filters: Omit) {
+ const qs = new URLSearchParams();
+ if (filters.national_code) qs.set('national_code', filters.national_code);
+ if (filters.status) qs.set('status', filters.status);
+ if (filters.from) qs.set('from', String(filters.from));
+ if (filters.to) qs.set('to', String(filters.to));
+
+ return useQuery>({
+ queryKey: ['payments-summary', filters],
+ queryFn: () => api.get(`/api/v1/my/billing/payments/summary?${qs.toString()}`),
+ });
+}
+```
+
+خواندن در صفحه: `summaryQuery.data?.data`.
+
+### ۳. بج وضعیت صورتحساب
+
+`StatusBadge` هیچ mapی برای `paid | unsettled` ندارد (`paymentMap` مربوط به درگاه است: `pending/success/failed/...`). یک type جدید اضافه کن — map موجود را دستکاری نکن:
+
+در `types/index.ts`:
+
+```ts
+export type InvoiceListStatus = 'paid' | 'unsettled';
+```
+
+در `StatusBadge.tsx`:
+
+```ts
+const invoiceMap: Record = {
+ paid: { color: 'green', label: 'پرداخت شده' },
+ unsettled: { color: 'amber', label: 'تسویه نشده' },
+};
+```
+
+و `'invoice'` را به union پراپ `type` اضافه کن و در بدنه هندل کن.
+
+### ۴. بازنویسی `MyPaymentsPage.tsx` بر اساس الگوی `ClaimsPage`
+
+ساختار نهایی دقیقاً به این ترتیب:
+
+```tsx
+<>
+
+
+ {/* ۴ کارت آمار */}
+
+
+
+
+
+
+
+
+ {/* ردیف فیلترها: وضعیت + از تاریخ + تا تاریخ + میانبرها + پاککردن */}
+ {/* DataTable */}
+ {/* Pagination — فقط وقتی total > MY_PAYMENTS_LIMIT */}
+
+>
+```
+
+**۴-۱ — انتقال state به query string.** `useState`های `page/nationalCode/status/from/to` را با `useSearchParams` جایگزین کن، دقیقاً با همان `setParam` صفحهی claims (که با هر تغییر فیلتر، `page` را حذف میکند):
+
+```tsx
+const [params, setParams] = useSearchParams();
+const search = params.get('search') ?? ''; // کد ملی / نام
+const status = params.get('status') ?? '';
+const from = params.get('from') ?? '';
+const to = params.get('to') ?? '';
+const page = Math.max(1, Number(params.get('page') ?? 1));
+
+const setParam = (patch: Record) => {
+ const next = new URLSearchParams(params);
+ Object.entries(patch).forEach(([k, v]) => (v ? next.set(k, v) : next.delete(k)));
+ if (!('page' in patch)) next.delete('page');
+ setParams(next, { replace: true });
+};
+```
+
+**۴-۲ — جستجو داخل `DataTable`.** `input` دستساز و آیکون ذرهبین را حذف کن؛ بهجایش:
+
+```tsx
+searchValue={search}
+onSearchChange={(v) => setParam({ search: v.replace(/\D/g, '') })}
+searchPlaceholder="کد ملی بیمار"
+```
+
+مقدار بهعنوان `national_code` به `usePayments` میرود (API فقط `national_code` را میشناسد؛ جستجوی نام سمت سرور وجود ندارد — placeholder را همینطور صادقانه بگذار و ادعای جستجوی نام نکن).
+
+**۴-۳ — فیلترها با label، مثل claims.** هر کنترل داخل یک `