mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
fee invoice frontend
This commit is contained in:
@@ -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 & 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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user