fix: issue invoice on demand so view-invoice buttons actually work

The admin SPA never called POST /billing/invoices, so every session had
invoice_uuid = null and the view-invoice buttons on the patient services
tab (and MyPatients visit modal) were permanently disabled. Add a shared
useIssueInvoice hook (idempotent create + finalize when draft) and wire
it into:
- DetailsStep: the wizard's final step now really issues the invoice
- SessionServiceCard / PatientDetailPage: clicking view-invoice on a
  session without an invoice issues it first, then opens the summary
- MyPatientsPage visit modal: same, replacing the dead disabled button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-16 15:39:02 +03:30
co-authored by Claude Opus 4.8
parent 0ba4bf383b
commit 76bfa5d36f
10 changed files with 204 additions and 18 deletions
+20 -7
View File
@@ -51,6 +51,7 @@ import {
} from "../lib/patientForm";
import type { ApiResponse, PaginatedResponse } from "../lib/api";
import { api } from "../lib/api";
import { useIssueInvoice } from "../hooks/useIssueInvoice";
import {
formatDate,
formatDateTime,
@@ -210,6 +211,16 @@ function MyPatientsPageInner() {
>("visits");
const [expandedVisit, setExpandedVisit] = useState<string | null>(null);
const [invoiceUuid, setInvoiceUuid] = useState<string | null>(null);
// «مشاهده فاکتور»: نبودِ invoice_uuid یعنی اول صادر شود (idempotent create + finalize).
const issueInvoiceMut = useIssueInvoice(selectedRecord?.uuid);
const viewInvoice = (s: PatientSession) => {
if (s.invoice_uuid) { setExpandedVisit(null); setInvoiceUuid(s.invoice_uuid); return; }
issueInvoiceMut.mutate(s.uuid, {
onSuccess: (iv) => { setExpandedVisit(null); setInvoiceUuid(iv); },
onError: () => toast.error("صدور فاکتور ناموفق بود"),
});
};
const [sessionPage, setSessionPage] = useState(1);
const [sessionModal, setSessionModal] = useState(false);
const [editSession, setEditSession] = useState<PatientSession | null>(null);
@@ -1340,7 +1351,8 @@ function MyPatientsPageInner() {
onClose={() => setExpandedVisit(null)}
settling={settleSessionMut.isPending}
onSettle={(uuid) => settleSessionMut.mutate(uuid)}
onViewInvoice={(uuid) => { setExpandedVisit(null); setInvoiceUuid(uuid); }}
onViewInvoice={viewInvoice}
issuing={issueInvoiceMut.isPending}
onEdit={(s) => { setExpandedVisit(null); setEditSession(s); }}
/>
@@ -1878,6 +1890,7 @@ function VisitDetailModal({
session,
onClose,
settling,
issuing,
onSettle,
onViewInvoice,
onEdit,
@@ -1885,8 +1898,10 @@ function VisitDetailModal({
session: PatientSession | null;
onClose: () => void;
settling: boolean;
issuing?: boolean;
onSettle: (uuid: string) => void;
onViewInvoice: (uuid: string) => void;
/** کل session پاس می‌شود؛ نبودِ invoice_uuid یعنی caller باید فاکتور را صادر کند. */
onViewInvoice: (session: PatientSession) => void;
onEdit: (s: PatientSession) => void;
}) {
if (!session) return null;
@@ -1916,11 +1931,9 @@ function VisitDetailModal({
<PencilIcon style={{ width: 15 }} /> ویرایش
</button>
{paid ? (
session.invoice_uuid ? (
<button className="cp-btn-secondary" onClick={() => onViewInvoice(session.invoice_uuid!)}>مشاهده فاکتور</button>
) : (
<button className="cp-btn-secondary" disabled>پرداخت شده</button>
)
<button className="cp-btn-secondary" disabled={issuing} onClick={() => onViewInvoice(session)}>
{issuing ? 'در حال صدور…' : 'مشاهده فاکتور'}
</button>
) : (
<button className="cp-btn-primary" disabled={settling} onClick={() => onSettle(session.uuid)}>تکمیل پرداخت</button>
)}