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>
This commit is contained in:
hamed
2026-07-14 18:24:33 +03:30
co-authored by Claude Opus 4.8
parent bfeffbc7b3
commit 4c29fa3274
15 changed files with 1101 additions and 0 deletions
+5
View File
@@ -38,6 +38,8 @@ import RepresentationFinancePage from './pages/RepresentationFinancePage';
import RepresentationProfilePage from './pages/RepresentationProfilePage';
import DoctorProfilePage from './pages/DoctorProfilePage';
import MyPatientsPage from './pages/MyPatientsPage';
import MyPaymentsPage from './pages/MyPaymentsPage';
import MyPaymentDetailPage from './pages/MyPaymentDetailPage';
import NewSessionPage from './pages/NewSessionPage';
import InsurancePricingPage from './pages/InsurancePricingPage';
import ClaimsPage from './pages/ClaimsPage';
@@ -197,6 +199,9 @@ export default function App() {
{/* دکتر / منشی / کلینیک */}
<Route path="my-patients" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><MyPatientsPage /></RoleRoute>} />
<Route path="my-payments" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><MyPaymentsPage /></RoleRoute>} />
<Route path="my-payments/:patientUuid" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><MyPaymentDetailPage /></RoleRoute>} />
<Route path="patients" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><PatientsListPage /></RoleRoute>} />
<Route path="patients/new" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><PatientRecordFormPage /></RoleRoute>} />
<Route path="patients/:uuid/edit" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']} blockClinicScope><PatientRecordFormPage /></RoleRoute>} />