mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Handover customer sign
This commit is contained in:
@@ -170,5 +170,6 @@ export const URL_CONSTANTS = {
|
||||
BY_ID: (id: string) => `/api/warehouse-fee-invoices/${id}`,
|
||||
DOCUMENT: (id: string) => `/api/warehouse-fee-invoices/${id}/document`,
|
||||
RECEIPT: (id: string) => `/api/warehouse-fee-invoices/${id}/receipt`,
|
||||
PAY_ONLINE: (id: string) => `/api/warehouse-fee-invoices/${id}/pay-online`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { ActionIcon, Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Download, Receipt } from "lucide-react";
|
||||
import { ActionIcon, Box, Button, Group, Stack, Text } from "@mantine/core";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { CreditCard, Download, Receipt } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { paymentsService, type PaymentMethod } from "@/services/payments.service";
|
||||
import {
|
||||
warehouseInvoicesService,
|
||||
type PortalWarehouseInvoice,
|
||||
} from "@/services/warehouse-invoices.service";
|
||||
import { saveBlob } from "@/utils/download";
|
||||
|
||||
import { PaymentMethodModal } from "./PaymentMethodModal";
|
||||
import { CardTitle, SectionCard } from "./layout";
|
||||
|
||||
/** Warehouse fee invoices the customer can still settle online. */
|
||||
const PAYABLE_STATUSES = new Set(["ISSUED", "PARTIALLY_PAID"]);
|
||||
const isPayable = (inv: PortalWarehouseInvoice) =>
|
||||
PAYABLE_STATUSES.has(inv.status) && Number(inv.balanceAmount ?? 0) > 0;
|
||||
|
||||
const money = (amount: number | string | null | undefined, currency: string) =>
|
||||
`${Number(amount ?? 0).toLocaleString()} ${currency}`;
|
||||
|
||||
@@ -44,10 +52,12 @@ function StatusPill({ status }: { status: string }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Warehouse fee invoices linked to this booking — display + PDF download only.
|
||||
* Paying them online is tracked separately (in-system demurrage/storage
|
||||
* payment). Renders nothing when the booking has no warehouse fees. Carries
|
||||
* `id="warehouse-payments"` so the invoice detail page can deep-link here.
|
||||
* Warehouse fee invoices linked to this booking. Customers can pay outstanding
|
||||
* demurrage/storage invoices online (Telebirr/Waafi) so they can then sign the
|
||||
* delivery handover; paid invoices expose the receipt PDF. The backoffice cash
|
||||
* `/pay` (record-a-payment) path is unaffected. Renders nothing when the booking
|
||||
* has no warehouse fees. Carries `id="warehouse-payments"` so the invoice detail
|
||||
* page can deep-link here.
|
||||
*/
|
||||
export function WarehousePaymentsSection({ bookingId }: { bookingId: string }) {
|
||||
const { data: invoices = [] } = useQuery({
|
||||
@@ -55,6 +65,41 @@ export function WarehousePaymentsSection({ bookingId }: { bookingId: string }) {
|
||||
queryFn: () => warehouseInvoicesService.listForBooking(bookingId),
|
||||
});
|
||||
|
||||
const [payInvoice, setPayInvoice] = useState<PortalWarehouseInvoice | null>(null);
|
||||
|
||||
const payMutation = useMutation({
|
||||
mutationFn: (method: PaymentMethod) => {
|
||||
if (!payInvoice) throw new Error("No invoice selected for payment.");
|
||||
return warehouseInvoicesService.payOnline(payInvoice.id, {
|
||||
method,
|
||||
platform: "web",
|
||||
});
|
||||
},
|
||||
onSuccess: (data, method) => {
|
||||
if (!payInvoice) return;
|
||||
// Redirect to the provider (or the fallback checkout page) — same as the
|
||||
// booking "Pay now" flow, so behaviour is identical everywhere.
|
||||
const redirectUrl =
|
||||
data?.clientAction?.type === "REDIRECT" && data.clientAction.url
|
||||
? data.clientAction.url
|
||||
: paymentsService.checkoutUrlForInvoice({ invoiceId: payInvoice.id, method });
|
||||
window.location.href = redirectUrl;
|
||||
},
|
||||
});
|
||||
|
||||
const payError = payMutation.isError
|
||||
? payMutation.error instanceof Error
|
||||
? payMutation.error.message
|
||||
: "Could not start payment. Please try again."
|
||||
: null;
|
||||
|
||||
const closePayModal = () => {
|
||||
if (!payMutation.isPending) {
|
||||
setPayInvoice(null);
|
||||
payMutation.reset();
|
||||
}
|
||||
};
|
||||
|
||||
if (invoices.length === 0) return null;
|
||||
|
||||
const download = async (inv: PortalWarehouseInvoice) => {
|
||||
@@ -128,6 +173,17 @@ export function WarehousePaymentsSection({ bookingId }: { bookingId: string }) {
|
||||
</Text>
|
||||
</Box>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
{isPayable(inv) && (
|
||||
<Button
|
||||
size="xs"
|
||||
radius={10}
|
||||
color="edr-green"
|
||||
leftSection={<CreditCard size={14} />}
|
||||
onClick={() => setPayInvoice(inv)}
|
||||
>
|
||||
Pay
|
||||
</Button>
|
||||
)}
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
@@ -151,6 +207,18 @@ export function WarehousePaymentsSection({ bookingId }: { bookingId: string }) {
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
<PaymentMethodModal
|
||||
opened={payInvoice !== null}
|
||||
onClose={closePayModal}
|
||||
amountLabel={
|
||||
payInvoice ? money(payInvoice.balanceAmount, payInvoice.currency) : undefined
|
||||
}
|
||||
currency={payInvoice?.currency}
|
||||
onConfirm={(method) => payMutation.mutate(method)}
|
||||
processing={payMutation.isPending}
|
||||
error={payError}
|
||||
/>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||
import { client } from "../utils/api";
|
||||
import type {
|
||||
InitiateResponse,
|
||||
PaymentMethod,
|
||||
PaymentPlatform,
|
||||
} from "./payments.service";
|
||||
|
||||
const W = URL_CONSTANTS.WAREHOUSE_INVOICES;
|
||||
|
||||
@@ -51,4 +56,27 @@ export const warehouseInvoicesService = {
|
||||
const { data } = await client.get(W.RECEIPT(id), { responseType: "blob" });
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiate a Telebirr/Waafi online payment for a warehouse demurrage/storage
|
||||
* invoice. Returns the payment intent + `clientAction` to redirect the browser
|
||||
* to the provider (mirrors the booking `/pay` flow). The backoffice cash
|
||||
* `/pay` (record-a-payment) path is unaffected.
|
||||
*/
|
||||
payOnline: async (
|
||||
id: string,
|
||||
payload: {
|
||||
method: PaymentMethod;
|
||||
platform?: PaymentPlatform;
|
||||
payerAccount?: string;
|
||||
returnUrl?: string;
|
||||
failureUrl?: string;
|
||||
},
|
||||
): Promise<InitiateResponse> => {
|
||||
const { data } = await client.post(W.PAY_ONLINE(id), {
|
||||
platform: "web",
|
||||
...payload,
|
||||
});
|
||||
return data.data ?? data;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user