Files
clinicpro/assets/admin/hooks/useMyPayments.ts
T
hamedandClaude Opus 4.8 4c29fa3274 feat(billing): patient payments list + patient invoices detail (doctor/clinic)
Port two nobat724 Figma screens into the admin SPA for the doctor/clinic
tenant panel:

- node 1 — لیست پرداخت‌ها (/admin/my-payments): per-patient payment summary
  (invoice count, paid, remaining, derived status paid/unsettled/unpaid),
  filters by national code / status / Jalali date range, pagination.
- node 2 — پرداخت‌های ثبت‌شده (/admin/my-payments/:patientUuid): a patient's
  recorded invoices with patient header, service title, total, status badge,
  and an expandable per-invoice item breakdown.

Backend (App\Billing):
- InvoiceRepository::patientPaymentSummary/countPatientPaymentSummary — DQL
  aggregation grouped by patient record (arbitrary join Invoice→PatientRecord
  →User), draft/void excluded, derived-status HAVING filters.
- InvoiceRepository::invoicesForPatient/count + InvoiceService methods that
  shape rows and derive status.
- BillingController: GET /api/v1/my/billing/patient-payments and
  GET /api/v1/my/billing/patients/{patientUuid}/invoices (thin, resolveEntity,
  tenant-scoped, 403/404). Invoice::getIssuedAt / InvoiceItem::getTitle added.
- docs/api/billing.md documents both endpoints.

Frontend: useMyPayments hooks, MyPaymentsPage, MyPaymentDetailPage, routes in
App.tsx (doctor/secretary/clinic, blockClinicScope) and a sidebar entry.
Persian strings hardcoded per existing admin convention (no i18n infra).

Tests: tests/Billing/PatientPaymentsTest.php (8), useMyPayments + both page
tests (11). Note: pre-existing LoginPage.test failures are unrelated (proven
by stashing this change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:24:33 +03:30

71 lines
2.5 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
/** A derived per-patient payment status shown on the list (node 1). */
export type PaymentRowStatus = 'paid' | 'unpaid' | 'unsettled';
export interface PatientPaymentRow {
patient_uuid: string;
patient_name: string | null;
national_code: string | null;
invoice_count: number;
paid_rials: number;
remaining_rials: number;
status: PaymentRowStatus;
}
export interface PatientPaymentFilters {
page: number;
national_code?: string;
status?: string; // paid | unsettled | unpaid
from?: number; // unix seconds
to?: number; // unix seconds
}
/** A single invoice status on the detail table (node 2) — two-state. */
export type InvoiceRowStatus = 'paid' | 'unsettled';
export interface PatientInvoiceRow {
uuid: string;
number: number;
issued_at: number;
total_rials: number;
status: InvoiceRowStatus;
service_title: string | null;
items: Array<{ uuid: string; title: string; quantity: number; total_rials: number; patient_rials: number }>;
}
export interface PatientInvoicesPayload {
patient: { uuid: string; name: string | null; national_code: string | null };
data: PatientInvoiceRow[];
meta: { totalRecords: number; totalPages: number; currentPage: number };
}
const LIMIT = 20;
/** Node 1 — paginated per-patient payment summary for the current tenant. */
export function usePatientPayments(filters: PatientPaymentFilters) {
const qs = new URLSearchParams({ page: String(filters.page), limit: String(LIMIT) });
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<PaginatedResponse<PatientPaymentRow>>({
queryKey: ['patient-payments', filters],
queryFn: () => api.get(`/api/v1/my/billing/patient-payments?${qs.toString()}`),
});
}
/** Node 2 — a single patient's recorded invoices (header + paginated list). */
export function usePatientInvoices(patientUuid: string | undefined, page: number) {
return useQuery<ApiResponse<PatientInvoicesPayload>>({
queryKey: ['patient-invoices', patientUuid, page],
queryFn: () => api.get(`/api/v1/my/billing/patients/${patientUuid}/invoices?page=${page}&limit=${LIMIT}`),
enabled: !!patientUuid,
});
}
export const MY_PAYMENTS_LIMIT = LIMIT;