mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
- Allocation rules engine: cargo/container/trade criteria -> deterministic yard/warehouse/zone by code; wired into auto-unload (fallback to default) - Storage/demurrage fee rules: configurable freeDays + ratePerDay; most-specific match; fee preview per inventory item - Inventory demurrage timestamps: inspectionStartedAt, inspectionCompletedAt, readyForPickupAt, releaseDate, gateClearedAt - Migration 1790000000000 (allocation_rules + fee_rules tables + inventory date columns) - Frontend: Allocation & Fees config page, automatic Fee Preview modal, plumbing/hooks - No invoice/payment (Batch 6) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { Badge, Card, Group, Loader, Modal, Stack, Text } from '@mantine/core';
|
|
import { CalendarClock, Coins } from 'lucide-react';
|
|
|
|
import { useFeePreview } from '@/hooks/useWarehouses';
|
|
import type { FeePreview } from '@/types/warehouse';
|
|
|
|
interface FeePreviewModalProps {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
inventoryId: string | null;
|
|
}
|
|
|
|
const LABELS: Record<string, { label: string; color: string }> = {
|
|
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();
|
|
}
|
|
|
|
function FeeCard({ fee }: { fee: FeePreview }) {
|
|
const meta = LABELS[fee.ruleType] ?? { label: fee.ruleType, color: 'gray' };
|
|
const configured = Boolean(fee.ruleId);
|
|
return (
|
|
<Card withBorder radius="md" padding="md" style={{ borderColor: `var(--mantine-color-${meta.color}-3)` }}>
|
|
<Group justify="space-between" mb="xs">
|
|
<Group gap="xs">
|
|
<Coins size={16} />
|
|
<Text fw={700}>{meta.label}</Text>
|
|
{fee.endIsOpen && (
|
|
<Badge size="xs" color={meta.color} variant="light">
|
|
accruing
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
<Text fw={800} size="lg" c={`${meta.color}.7`}>
|
|
{fee.amount.toLocaleString()} {fee.currency}
|
|
</Text>
|
|
</Group>
|
|
|
|
{!configured ? (
|
|
<Text size="xs" c="dimmed">
|
|
No active {meta.label.toLowerCase()} rule configured — amount shown as 0.
|
|
</Text>
|
|
) : (
|
|
<Stack gap={4}>
|
|
<Row label="Rule" value={fee.ruleName ?? '—'} />
|
|
<Row label="Free days" value={String(fee.freeDays)} />
|
|
<Row label="Rate / day" value={`${fee.ratePerDay.toLocaleString()} ${fee.currency}`} />
|
|
<Row label="Period" value={`${fmtDate(fee.startDate)} → ${fmtDate(fee.endDate)}${fee.endIsOpen ? ' (today)' : ''}`} />
|
|
<Row label="Elapsed days" value={String(fee.elapsedDays)} />
|
|
<Row label="Chargeable days" value={`${fee.chargeableDays} (after ${fee.freeDays} free)`} />
|
|
</Stack>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function Row({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Text size="xs" c="dimmed">
|
|
{label}
|
|
</Text>
|
|
<Text size="sm">{value}</Text>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
/** Batch 5 — automatic storage/demurrage fee preview for an inventory item (no invoice/payment). */
|
|
export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) {
|
|
const { data, isLoading } = useFeePreview(opened ? inventoryId ?? undefined : undefined);
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={
|
|
<Group gap="xs">
|
|
<CalendarClock size={18} />
|
|
<Text fw={700}>Storage & Demurrage Preview</Text>
|
|
</Group>
|
|
}
|
|
centered
|
|
size="md"
|
|
>
|
|
{isLoading ? (
|
|
<Group justify="center" py="xl">
|
|
<Loader />
|
|
</Group>
|
|
) : (
|
|
<Stack gap="md">
|
|
{(data ?? []).map((fee) => (
|
|
<FeeCard key={fee.ruleType} fee={fee} />
|
|
))}
|
|
<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).
|
|
</Text>
|
|
</Stack>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|