From f15c57d73cc1a5ec5e144af2d98ac1be18be418e Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 12 Jul 2026 22:09:13 +0330 Subject: [PATCH] feat(patient): enhance session management with billing details and service tracking --- assets/admin/pages/MyPatientsPage.tsx | 99 +++++++++++++++++++- assets/admin/types/index.ts | 19 ++++ src/Patient/Controller/PatientController.php | 28 +++++- src/Patient/Entity/PatientSession.php | 5 + 4 files changed, 148 insertions(+), 3 deletions(-) diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx index 33ef7467..9f72787f 100644 --- a/assets/admin/pages/MyPatientsPage.tsx +++ b/assets/admin/pages/MyPatientsPage.tsx @@ -111,7 +111,8 @@ function MyPatientsPageInner() { const [page, setPage] = useState(1); const [search, setSearch] = useState(""); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); - const [detailTab, setDetailTab] = useState<"info" | "sessions">("info"); + const [detailTab, setDetailTab] = useState<"info" | "services" | "sessions">("info"); + const [invoiceUuid, setInvoiceUuid] = useState(null); const [sessionPage, setSessionPage] = useState(1); const [sessionModal, setSessionModal] = useState(false); const [editSession, setEditSession] = useState(null); @@ -218,6 +219,13 @@ function MyPatientsPageInner() { enabled: !!selectedRecord, }); + const { data: invoiceData } = useQuery>({ + queryKey: ["invoice", invoiceUuid], + queryFn: () => api.get(`/api/v1/billing/invoices/${invoiceUuid}`), + enabled: !!invoiceUuid, + }); + const invoice = (invoiceData?.data as any)?.data ?? (invoiceData?.data as any) ?? null; + const records = recordsData?.data ?? EMPTY_RECORDS; const sessions = sessionsData?.data ?? EMPTY_SESSIONS; const totalRec = recordsData?.meta?.totalRecords ?? 0; @@ -325,6 +333,16 @@ function MyPatientsPageInner() { }); }; + const settleSessionMut = useMutation({ + mutationFn: (uuid: string) => + api.patch(`/api/v1/session/${uuid}`, { payment_method: "cash" }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["patient-sessions", selectedRecord?.uuid] }); + toast.success("پرداخت ثبت شد"); + }, + onError: (e: any) => toast.error(e?.message || "خطا در ثبت پرداخت"), + }); + const createSessionMut = useMutation({ mutationFn: (body: object) => api.post(`/api/v1/patient/${selectedRecord!.uuid}/session`, body), @@ -968,7 +986,7 @@ function MyPatientsPageInner() {
- {([["info", "اطلاعات پرونده"], ["sessions", `تاریخچه مراجعات (${totalSes})`]] as const).map(([key, label]) => ( + {([["info", "اطلاعات پرونده"], ["services", "سرویس‌ها"], ["sessions", `تاریخچه مراجعات (${totalSes})`]] as const).map(([key, label]) => (
+ {detailTab === "services" && ( + sessionsLoading ? ( +
در حال بارگذاری…
+ ) : sessions.length === 0 ? ( +
+ هنوز سرویسی برای این بیمار ثبت نشده است. +
+ ) : ( +
+ {sessions.map((s) => { + const svcNames = (s.services ?? []).map((x) => x.service_name).join("، ") || "ویزیت"; + const debt = s.patient_debt_rials ?? 0; + return ( +
+
+
{svcNames}
+ {s.is_paid + ? تسویه شده + : تسویه نشده} +
+
انجام‌دهنده{s.doctor_name || "—"}
+
تاریخ{formatDate(s.created_at)}
+
هزینه{formatRial(s.final_price_rials)}
+
+ مانده بدهی + 0 ? "var(--danger)" : "var(--success)" }}> + {debt > 0 ? formatRial(debt) : "ندارد"} + +
+ {s.is_paid ? ( + s.invoice_uuid ? ( + + ) : ( + + ) + ) : ( + + )} +
+ ); + })} +
+ ) + )} + + setInvoiceUuid(null)}> + {!invoice ? ( +
در حال بارگذاری…
+ ) : ( +
+
وضعیت{invoice.status}
+
مبلغ کل{formatRial(invoice.total_rials ?? 0)}
+
سهم بیمه پایه{formatRial(invoice.base_insurance_rials ?? 0)}
+
سهم بیمه تکمیلی{formatRial(invoice.supplementary_rials ?? 0)}
+
سهم بیمار{formatRial(invoice.patient_rials ?? 0)}
+ {Array.isArray(invoice.items) && invoice.items.length > 0 && ( +
+
اقلام
+ {invoice.items.map((it: any, i: number) => ( +
+ {it.title} + {formatRial(it.total_rials ?? 0)} +
+ ))} +
+ )} +
+ )} +
+ {detailTab === "sessions" && (
sessionRepo->countByRecord($record); return $this->paginated( - array_map(fn($s) => $s->toArray(), $sessions), + array_map(fn($s) => $this->sessionWithBilling($s), $sessions), $total, $page, $limit ); } + /** + * خروجی session به‌همراه خلاصه‌ی صورتحساب: uuid فاکتور (در صورت وجود) و + * مانده‌ی بدهیِ سهم بیمار. اگر session تسویه شده باشد (payment_method != pending) + * بدهی صفر است؛ در غیر این صورت سهم بیمار از فاکتور یا کل مبلغ نهایی. + */ + private function sessionWithBilling(\App\Patient\Entity\PatientSession $session): array + { + $data = $session->toArray(); + $invoice = $session->getId() !== null ? $this->invoiceRepo->findBySession($session->getId()) : null; + + $data['invoice_uuid'] = $invoice?->getUuid(); + $data['invoice_status'] = $invoice?->getStatus(); + + if ($data['is_paid']) { + $data['patient_debt_rials'] = 0; + } elseif ($invoice !== null) { + $data['patient_debt_rials'] = $invoice->getPatientRials(); + } else { + $data['patient_debt_rials'] = $session->getFinalPriceRials(); + } + + return $data; + } + #[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])] public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { diff --git a/src/Patient/Entity/PatientSession.php b/src/Patient/Entity/PatientSession.php index 02b0a89f..477a5f5b 100644 --- a/src/Patient/Entity/PatientSession.php +++ b/src/Patient/Entity/PatientSession.php @@ -127,6 +127,11 @@ class PatientSession 'services_total_rials' => $this->servicesTotalRials, 'final_price_rials' => $this->finalPriceRials, 'payment_method' => $this->paymentMethod, + 'is_paid' => $this->paymentMethod !== 'pending', + 'services' => array_map( + fn(SessionService $s) => $s->toArray(), + $this->services->toArray() + ), 'notes' => $this->notes, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt,