mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
Doublehandling
This commit is contained in:
@@ -28,6 +28,8 @@ interface FeePreviewModalProps {
|
||||
const LABELS: Record<string, { label: string; color: string }> = {
|
||||
DEMURRAGE_FEE: { label: 'Demurrage', color: 'orange' },
|
||||
STORAGE_FEE: { label: 'Storage', color: 'teal' },
|
||||
DOUBLE_HANDLING_FEE: { label: 'Double Handling', color: 'grape' },
|
||||
TRUCK_DETENTION_FEE: { label: 'Truck Detention Cost', color: 'blue' },
|
||||
};
|
||||
|
||||
function fmtDate(iso: string | null) {
|
||||
|
||||
@@ -32,7 +32,21 @@ import {
|
||||
useFeeRules,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import { api } from '@/services/api';
|
||||
import { FEE_RULE_TYPES, type FeeRuleType } from '@/types/warehouse';
|
||||
import {
|
||||
FEE_RULE_BASES,
|
||||
FEE_RULE_BASIS_LABELS,
|
||||
FEE_RULE_TYPES,
|
||||
FEE_RULE_TYPE_LABELS,
|
||||
type FeeRuleBasis,
|
||||
type FeeRuleType,
|
||||
} from '@/types/warehouse';
|
||||
|
||||
const RULE_TYPE_COLOR: Record<FeeRuleType, string> = {
|
||||
STORAGE_FEE: 'teal',
|
||||
DEMURRAGE_FEE: 'orange',
|
||||
DOUBLE_HANDLING_FEE: 'grape',
|
||||
TRUCK_DETENTION_FEE: 'blue',
|
||||
};
|
||||
import { extractErrorMessage, lettersOnly } from '@/components/warehouses/options';
|
||||
|
||||
const FREIGHT = [
|
||||
@@ -353,6 +367,7 @@ function FeeRules() {
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
ruleType: 'DEMURRAGE_FEE' as FeeRuleType,
|
||||
basis: 'PER_CONTAINER' as FeeRuleBasis,
|
||||
freightType: '',
|
||||
tradeDirection: '',
|
||||
cargoTypeCode: '',
|
||||
@@ -367,11 +382,15 @@ function FeeRules() {
|
||||
const containerTypeOptions = codeOptions(containerTypes);
|
||||
const isBulkRule = form.freightType === 'BULK';
|
||||
const isContainerRule = form.freightType === 'CONTAINER';
|
||||
// Double handling is a flat per-unit charge (basis × rate), not day-based:
|
||||
// no free days, no progressive tiers.
|
||||
const isDoubleHandling = form.ruleType === 'DOUBLE_HANDLING_FEE';
|
||||
|
||||
const resetForm = () =>
|
||||
setForm({
|
||||
name: '',
|
||||
ruleType: 'DEMURRAGE_FEE',
|
||||
basis: 'PER_CONTAINER',
|
||||
freightType: '',
|
||||
tradeDirection: '',
|
||||
cargoTypeCode: '',
|
||||
@@ -440,10 +459,12 @@ function FeeRules() {
|
||||
tradeDirection: clean(form.tradeDirection) ?? null,
|
||||
cargoTypeCode: isBulkRule ? (clean(form.cargoTypeCode) ?? null) : null,
|
||||
containerType: isContainerRule ? (clean(form.containerType) ?? null) : null,
|
||||
freeDays: form.freeDays,
|
||||
// Double handling: flat basis × rate — no free days, no tiers.
|
||||
freeDays: isDoubleHandling ? 0 : form.freeDays,
|
||||
ratePerDay: form.ratePerDay,
|
||||
currency: form.currency || 'USD',
|
||||
...(tiers.length ? { tiers } : {}),
|
||||
...(isDoubleHandling ? { basis: form.basis } : {}),
|
||||
...(!isDoubleHandling && tiers.length ? { tiers } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -514,8 +535,8 @@ function FeeRules() {
|
||||
{rules.map((rule) => (
|
||||
<Table.Tr key={rule.id}>
|
||||
<Table.Td>
|
||||
<Badge color={rule.ruleType === 'DEMURRAGE_FEE' ? 'orange' : 'teal'} variant="light">
|
||||
{rule.ruleType === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage'}
|
||||
<Badge color={RULE_TYPE_COLOR[rule.ruleType] ?? 'gray'} variant="light">
|
||||
{FEE_RULE_TYPE_LABELS[rule.ruleType] ?? rule.ruleType}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{rule.name}</Table.Td>
|
||||
@@ -577,7 +598,7 @@ function FeeRules() {
|
||||
label="Rule type"
|
||||
data={FEE_RULE_TYPES.map((type) => ({
|
||||
value: type,
|
||||
label: type === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage',
|
||||
label: FEE_RULE_TYPE_LABELS[type],
|
||||
}))}
|
||||
value={form.ruleType}
|
||||
onChange={(value) =>
|
||||
@@ -637,14 +658,26 @@ function FeeRules() {
|
||||
/>
|
||||
)}
|
||||
<Group grow>
|
||||
{isDoubleHandling ? (
|
||||
<Select
|
||||
label="Basis"
|
||||
data={FEE_RULE_BASES.map((b) => ({ value: b, label: FEE_RULE_BASIS_LABELS[b] }))}
|
||||
value={form.basis}
|
||||
onChange={(value) =>
|
||||
setForm((f) => ({ ...f, basis: selectValue(value, 'PER_CONTAINER') as FeeRuleBasis }))
|
||||
}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
) : (
|
||||
<NumberInput
|
||||
label="Free days"
|
||||
min={0}
|
||||
value={form.freeDays}
|
||||
onChange={(value) => setForm((f) => ({ ...f, freeDays: numberValue(value) }))}
|
||||
/>
|
||||
)}
|
||||
<NumberInput
|
||||
label="Free days"
|
||||
min={0}
|
||||
value={form.freeDays}
|
||||
onChange={(value) => setForm((f) => ({ ...f, freeDays: numberValue(value) }))}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Rate / day"
|
||||
label={isDoubleHandling ? 'Rate / unit' : 'Rate / day'}
|
||||
min={0}
|
||||
value={form.ratePerDay}
|
||||
onChange={(value) => setForm((f) => ({ ...f, ratePerDay: numberValue(value) }))}
|
||||
@@ -657,6 +690,13 @@ function FeeRules() {
|
||||
allowDeselect={false}
|
||||
/>
|
||||
</Group>
|
||||
{isDoubleHandling && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Flat charge — the rate is multiplied by the selected basis (
|
||||
{FEE_RULE_BASIS_LABELS[form.basis]}). No free days or progressive tiers.
|
||||
</Text>
|
||||
)}
|
||||
{!isDoubleHandling && (
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={600}>
|
||||
@@ -702,6 +742,7 @@ function FeeRules() {
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button variant="default" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
|
||||
@@ -737,13 +737,38 @@ export interface AllocationRule {
|
||||
}
|
||||
export type SaveAllocationRulePayload = Omit<AllocationRule, 'id'>;
|
||||
|
||||
export const FEE_RULE_TYPES = ['STORAGE_FEE', 'DEMURRAGE_FEE'] as const;
|
||||
export const FEE_RULE_TYPES = [
|
||||
'STORAGE_FEE',
|
||||
'DEMURRAGE_FEE',
|
||||
'DOUBLE_HANDLING_FEE',
|
||||
'TRUCK_DETENTION_FEE',
|
||||
] as const;
|
||||
export type FeeRuleType = (typeof FEE_RULE_TYPES)[number];
|
||||
|
||||
/** Human labels for each fee rule type (dropdowns, badges). */
|
||||
export const FEE_RULE_TYPE_LABELS: Record<FeeRuleType, string> = {
|
||||
STORAGE_FEE: 'Storage',
|
||||
DEMURRAGE_FEE: 'Demurrage',
|
||||
DOUBLE_HANDLING_FEE: 'Double Handling',
|
||||
TRUCK_DETENTION_FEE: 'Truck Detention Cost',
|
||||
};
|
||||
|
||||
/** Charge basis for a Double Handling rule (flat rate × the chosen quantity). */
|
||||
export const FEE_RULE_BASES = ['PER_CONTAINER', 'PER_TON', 'PER_ITEM'] as const;
|
||||
export type FeeRuleBasis = (typeof FEE_RULE_BASES)[number];
|
||||
|
||||
export const FEE_RULE_BASIS_LABELS: Record<FeeRuleBasis, string> = {
|
||||
PER_CONTAINER: 'Per Container',
|
||||
PER_TON: 'Per Ton',
|
||||
PER_ITEM: 'Per Item',
|
||||
};
|
||||
|
||||
export interface FeeRule {
|
||||
id: string;
|
||||
name: string;
|
||||
ruleType: FeeRuleType;
|
||||
/** Double-handling charge basis; null for day-based fee types. */
|
||||
basis?: FeeRuleBasis | null;
|
||||
priority: number;
|
||||
freightType?: string | null;
|
||||
tradeDirection?: string | null;
|
||||
@@ -776,6 +801,7 @@ export interface FeePreviewTier extends FeeRuleTier {
|
||||
|
||||
export interface FeePreview {
|
||||
ruleType: FeeRuleType;
|
||||
basis?: FeeRuleBasis | null;
|
||||
ruleId: string | null;
|
||||
ruleName: string | null;
|
||||
freeDays: number;
|
||||
|
||||
Reference in New Issue
Block a user