feat: add payment modal to the invoice detail page

This commit is contained in:
ghost2023
2026-07-01 16:11:32 +03:00
parent 174df0b7aa
commit 43d1634823

View File

@@ -15,12 +15,15 @@ import {
Text,
Title,
} from "@mantine/core";
import { ArrowLeft, CreditCard, Download, ExternalLink, Info, Receipt } from "lucide-react";
import { ArrowLeft, CreditCard, Download, ExternalLink, Receipt } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { api } from "@/services/api";
import { invoicesService } from "@/services/invoices.service";
import { paymentsService, type PaymentMethod } from "@/services/payments.service";
import { warehouseInvoicesService } from "@/services/warehouse-invoices.service";
import { PaymentMethodModal } from "@/pages/bookings/BookingDetailPage/components/PaymentMethodModal";
import { saveBlob } from "@/utils/download";
import { formatCurrency } from "@/lib/currency";
import { BORDER, INK, MUTED } from "../contracts/contract-ui";
@@ -53,14 +56,29 @@ export default function InvoiceDetailPage() {
api.invoices.get.queryOptions({ input: { id } }),
);
const payMutation = useMutation(
api.invoices.pay.mutationOptions({
onSuccess: (res) => {
const url = res.clientAction?.url;
if (url) window.location.href = url;
},
}),
);
const [payModalOpen, setPayModalOpen] = useState(false);
// Extracted for payMutation callbacks — guaranteed defined when they run
// (guarded by the early return below).
const invSource = invoice?.source;
const invSourceId = invoice?.sourceId;
const payMutation = useMutation({
mutationFn: async (method: PaymentMethod) => {
const bookingId =
invSource === "warehouse"
? (await warehouseInvoicesService.get(id)).bookingId ?? invSourceId!
: invSourceId!;
return api.payments.initiate.call({ bookingId, method });
},
onSuccess: (data, method) => {
const redirectUrl =
data?.clientAction?.type === "REDIRECT" && data.clientAction.url
? data.clientAction.url
: paymentsService.checkoutUrl({ bookingId: invSourceId!, method });
window.location.href = redirectUrl;
},
});
if (isLoading) {
return (
@@ -94,9 +112,7 @@ export default function InvoiceDetailPage() {
const lines = invoice.lines ?? [];
const handlePay = () => {
const returnUrl = `${window.location.origin}/payment/success`;
const failureUrl = `${window.location.origin}/payment/failure`;
payMutation.mutate({ id, payload: { returnUrl, failureUrl } });
setPayModalOpen(true);
};
const hasReceipt = Number(invoice.paidAmount) > 0;
@@ -221,12 +237,6 @@ export default function InvoiceDetailPage() {
</Group>
</Group>
{payMutation.isError && (
<Alert color="red" icon={<Info size={16} />} title="Payment could not be started">
Please try again, or contact support if the problem persists.
</Alert>
)}
{/* Summary */}
<Paper withBorder radius="lg" p="lg" style={{ borderColor: BORDER }}>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="lg">
@@ -330,6 +340,27 @@ export default function InvoiceDetailPage() {
</Table>
</Box>
</Paper>
<PaymentMethodModal
opened={payModalOpen}
onClose={() => {
if (!payMutation.isPending) {
setPayModalOpen(false);
payMutation.reset();
}
}}
amountLabel={formatCurrency(Number(invoice.totalAmount), invoice.currency)}
currency={invoice.currency}
processing={payMutation.isPending}
error={
payMutation.isError
? payMutation.error instanceof Error
? payMutation.error.message
: "Could not start payment. Please try again."
: null
}
onConfirm={(method) => payMutation.mutate(method)}
/>
</Stack>
</Box>
);