import { useState } from 'react'; import { Badge, Button, Card, Divider, Group, Loader, Modal, SegmentedControl, Stack, Text } from '@mantine/core'; import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; import { warehouseService } from '@/services/warehouse.service'; import { extractErrorMessage } from './options'; import type { FeePreview, WarehouseInventoryItem, WarehouseInvoiceStatus } from '@/types/warehouse'; import { openPdfBlob } from './pdf'; const INVOICE_STATUS_COLOR: Record = { DRAFT: 'gray', ISSUED: 'orange', PARTIALLY_PAID: 'yellow', PAID: 'edr-green', CANCELLED: 'gray', }; interface FeePreviewModalProps { opened: boolean; onClose: () => void; inventoryId: string | null; } const LABELS: Record = { DEMURRAGE_FEE: { label: 'Demurrage', color: 'orange' }, STORAGE_FEE: { label: 'Storage', color: 'teal' }, }; function fmtDate(iso: string | null) { if (!iso) return '—'; return new Date(iso).toLocaleDateString(); } const money = (amount: number, currency: string) => `${Number(amount).toLocaleString()} ${currency === 'ETB' ? 'Birr (ETB)' : currency}`; function FeeCard({ fee }: { fee: FeePreview }) { const meta = LABELS[fee.ruleType] ?? { label: fee.ruleType, color: 'gray' }; const configured = Boolean(fee.ruleId); return ( {meta.label} {fee.endIsOpen && ( accruing )} {money(fee.amount, fee.currency)} {!configured ? ( No active {meta.label.toLowerCase()} rule configured — amount shown as 0. ) : ( {(fee.tiers ?? []).map((tier) => ( ))} )} ); } function Row({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } /** Batch 5 fee preview + Batch 6 invoice generation / gate clearance for an inventory item. */ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) { const { toast } = useToast(); const [billingCurrency, setBillingCurrency] = useState<'ETB' | 'USD'>('USD'); const enabledId = opened ? inventoryId ?? undefined : undefined; const { data, isLoading } = useQuery( api.warehouses.feePreview.queryOptions({ input: { inventoryId: enabledId ?? '', billingCurrency }, enabled: Boolean(enabledId), }), ); const { data: invoices } = useQuery( api.warehouses.invoicesForInventory.queryOptions({ input: { inventoryId: enabledId ?? '' }, enabled: Boolean(enabledId), }), ); const generate = useMutation(api.warehouses.generateInvoice.mutationOptions()); const gateClear = useMutation(api.warehouses.gateClearance.mutationOptions()); const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED'); const totalPreviewAmount = (data ?? []).reduce((sum, fee) => sum + Number(fee.amount || 0), 0); const handleGenerate = async () => { if (!inventoryId) return; if (totalPreviewAmount <= 0) { toast({ title: 'No fee to invoice', description: 'The item is still within the configured free days, or no active fee rule matched it.', }); return; } try { const inv = await generate.mutateAsync({ inventoryId, billingCurrency }); toast({ title: 'Invoice generated', description: `${inv.invoiceNumber} - ${money(inv.totalAmount, inv.currency)}` }); } catch (error) { toast({ variant: 'destructive', title: 'Generate failed', description: extractErrorMessage(error) }); } }; const handleGateClearance = async () => { if (!inventoryId) return; const pdfWindow = window.open('', '_blank'); try { const releasedItem = await gateClear.mutateAsync(inventoryId) as WarehouseInventoryItem; try { const documentResponse = await warehouseService.downloadReleaseDocument(inventoryId); const filename = `release-${releasedItem?.booking?.reference ?? releasedItem?.bookingId ?? inventoryId}.pdf`; const opened = openPdfBlob(documentResponse.data, filename, pdfWindow); toast({ title: 'Gate clearance recorded', description: opened ? 'The release PDF opened in a browser tab.' : 'The browser blocked the preview tab, so the PDF was downloaded.', }); } catch (documentError) { pdfWindow?.close(); toast({ title: 'Gate clearance recorded', description: `Release paper could not be opened: ${extractErrorMessage(documentError)}`, }); } onClose(); } catch (error) { pdfWindow?.close(); toast({ variant: 'destructive', title: 'Release blocked', description: extractErrorMessage(error) }); } }; return ( Storage & Demurrage Preview } centered size="md" > {isLoading ? ( ) : ( {(data ?? []).map((fee) => ( ))} Billing currency setBillingCurrency(value as 'ETB' | 'USD')} data={[ { value: 'USD', label: 'USD' }, { value: 'ETB', label: 'Birr' }, ]} disabled={Boolean(activeInvoice)} /> {activeInvoice ? ( {activeInvoice.invoiceNumber} {activeInvoice.status.replace(/_/g, ' ')} {money(Number(activeInvoice.balanceAmount), activeInvoice.currency)} due ) : ( )} Charges accrue from arrival until gate clearance / release. Final release is blocked while a demurrage/storage invoice is unpaid. )} ); }