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.
This commit is contained in:
hamed
2026-07-19 10:12:01 +03:30
parent 7ebc04fc3f
commit 5c8fe8ece4
11 changed files with 735 additions and 129 deletions
+39 -7
View File
@@ -168,18 +168,50 @@ class BillingController extends BaseController
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
}
$filters = [
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 20)));
$result = $this->invoiceService->tenantInvoiceList(
$entityType,
$entityId,
$this->paymentFilters($request),
$page,
$limit,
);
return $this->paginated($result['items'], $result['total'], $page, $limit);
}
/**
* خلاصه‌ی مالی همان مجموعه‌ی فیلترشده‌ی listPayments — برای کارت‌های آمار.
* پاسخ: { total_rials, paid_rials, unsettled_rials, invoices_count }.
*/
#[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);
}
return $this->success(
$this->invoiceService->tenantInvoiceSummary($entityType, $entityId, $this->paymentFilters($request)),
);
}
/**
* فیلترهای مشترک لیست پرداخت‌ها و خلاصه‌ی آن؛ یک منبع تا دو نما واگرا نشوند.
*
* @return array{national_code:?string,status:?string,from:?string,to:?string}
*/
private function paymentFilters(Request $request): array
{
return [
'national_code' => $request->query->get('national_code') ?: null,
'status' => $request->query->get('status') ?: null,
'from' => $request->query->get('from') ?: null,
'to' => $request->query->get('to') ?: null,
];
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 20)));
$result = $this->invoiceService->tenantInvoiceList($entityType, $entityId, $filters, $page, $limit);
return $this->paginated($result['items'], $result['total'], $page, $limit);
}
/**
@@ -67,6 +67,36 @@ class InvoiceRepository extends ServiceEntityRepository
->getSingleScalarResult();
}
/**
* Aggregate totals over the same filtered set as {@see tenantInvoices}, so the
* summary cards always agree with the table below them.
*
* @param array{national_code?:?string,status?:?string,from?:?int,to?:?int} $filters
* @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
*/
public function tenantInvoiceSummary(string $entityType, int $entityId, array $filters): array
{
$row = $this->tenantInvoicesQuery($entityType, $entityId, $filters)
->select(
'COALESCE(SUM(i.patientRials), 0) AS total_rials',
'COALESCE(SUM(CASE WHEN i.status = :paidStatus THEN i.patientRials ELSE 0 END), 0) AS paid_rials',
'COUNT(i.id) AS invoices_count',
)
->setParameter('paidStatus', Invoice::STATUS_PAID)
->getQuery()
->getSingleResult();
$total = (int) $row['total_rials'];
$paid = (int) $row['paid_rials'];
return [
'total_rials' => $total,
'paid_rials' => $paid,
'unsettled_rials' => $total - $paid,
'invoices_count' => (int) $row['invoices_count'],
];
}
private function tenantInvoicesQuery(string $entityType, int $entityId, array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('i')
+12
View File
@@ -109,6 +109,18 @@ class InvoiceService
];
}
/**
* Financial summary of the same filtered invoice set as {@see tenantInvoiceList},
* used by the payments page stat cards.
*
* @param array{national_code?:?string,status?:?string,from?:?int,to?:?int} $filters
* @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
*/
public function tenantInvoiceSummary(string $entityType, int $entityId, array $filters): array
{
return $this->invoiceRepo->tenantInvoiceSummary($entityType, $entityId, $filters);
}
/**
* A patient's recorded invoices, shaped for the detail table: number, issue
* time, a single service title (first item, "+ more" when several), total,