fee invoice frontend

This commit is contained in:
Hagernesh
2026-06-18 00:12:59 +00:00
parent 3860d17304
commit be12a9d496
8 changed files with 520 additions and 8 deletions

View File

@@ -1,8 +1,23 @@
import { Badge, Card, Group, Loader, Modal, Stack, Text } from '@mantine/core';
import { CalendarClock, Coins } from 'lucide-react';
import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core';
import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react';
import { useFeePreview } from '@/hooks/useWarehouses';
import type { FeePreview } from '@/types/warehouse';
import { useToast } from '@/hooks/use-toast';
import {
useFeePreview,
useGateClearance,
useGenerateInvoice,
useInvoicesForInventory,
} from '@/hooks/useWarehouses';
import { extractErrorMessage } from './options';
import type { FeePreview, WarehouseInvoiceStatus } from '@/types/warehouse';
const INVOICE_STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
DRAFT: 'gray',
ISSUED: 'orange',
PARTIALLY_PAID: 'yellow',
PAID: 'green',
CANCELLED: 'gray',
};
interface FeePreviewModalProps {
opened: boolean;
@@ -69,9 +84,44 @@ function Row({ label, value }: { label: string; value: string }) {
);
}
/** Batch 5 — automatic storage/demurrage fee preview for an inventory item (no invoice/payment). */
/** Batch 5 fee preview + Batch 6 invoice generation / gate clearance for an inventory item. */
export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) {
const { data, isLoading } = useFeePreview(opened ? inventoryId ?? undefined : undefined);
const { toast } = useToast();
const enabledId = opened ? inventoryId ?? undefined : undefined;
const { data, isLoading } = useFeePreview(enabledId);
const { data: invoices } = useInvoicesForInventory(enabledId);
const generate = useGenerateInvoice();
const gateClear = useGateClearance();
const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED');
const handleGenerate = async (confirmZero = false) => {
if (!inventoryId) return;
try {
const inv = await generate.mutateAsync({ inventoryId, confirmZero });
toast({ title: 'Invoice generated', description: `${inv.invoiceNumber}${inv.totalAmount} ${inv.currency}` });
} catch (error) {
const msg = extractErrorMessage(error);
if (/no payable warehouse fee/i.test(msg)) {
if (window.confirm('No payable warehouse fee found. Create a zero-amount invoice anyway?')) {
handleGenerate(true);
}
return;
}
toast({ variant: 'destructive', title: 'Generate failed', description: msg });
}
};
const handleGateClearance = async () => {
if (!inventoryId) return;
try {
await gateClear.mutateAsync(inventoryId);
toast({ title: 'Gate clearance recorded', description: 'Item released from terminal.' });
onClose();
} catch (error) {
toast({ variant: 'destructive', title: 'Release blocked', description: extractErrorMessage(error) });
}
};
return (
<Modal
@@ -95,9 +145,47 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa
{(data ?? []).map((fee) => (
<FeeCard key={fee.ruleType} fee={fee} />
))}
<Divider label="Invoice & Release" labelPosition="left" />
{activeInvoice ? (
<Group justify="space-between">
<Group gap="xs">
<FileText size={16} />
<Text size="sm" fw={600}>{activeInvoice.invoiceNumber}</Text>
<Badge variant="light" color={INVOICE_STATUS_COLOR[activeInvoice.status]}>
{activeInvoice.status.replace(/_/g, ' ')}
</Badge>
</Group>
<Text size="sm">
{Number(activeInvoice.balanceAmount).toLocaleString()} {activeInvoice.currency} due
</Text>
</Group>
) : (
<Button
variant="light"
color="orange"
leftSection={<FileText size={16} />}
loading={generate.isPending}
onClick={() => handleGenerate(false)}
>
Generate Fee Invoice
</Button>
)}
<Button
variant="light"
color="green"
leftSection={<DoorOpen size={16} />}
loading={gateClear.isPending}
onClick={handleGateClearance}
>
Gate Clearance / Release
</Button>
<Text size="xs" c="dimmed">
Preview only invoicing &amp; payment are handled in Batch 6. Charges accrue from arrival until
gate clearance / release (or today if still in terminal).
Charges accrue from arrival until gate clearance / release. Final release is blocked while a
demurrage/storage invoice is unpaid.
</Text>
</Stack>
)}