// Screens: Rate Calculator, AI Assistant, Export // ─── Calculator helpers ─────────────────────────────────────────────────────── const UNIT_SHORT = { PER_TEU: "TEU", PER_FEU: "FEU", PER_BOX: "BOX", PER_DAY: "Day", PER_MOVE: "Move", PER_KWH: "kWh", UNIT: "Unit", }; // Build pivot: cols = container combos, rows keyed by material/shipment/charge/slab function buildPivot(rates) { const SIZE_ORD = { "20": 0, "40": 1, "45": 2, "All": 9 }; const TYPE_ORD = { "DRY": 0, "REEFER": 1, "All": 9 }; const FULL_ORD = { "Full": 0, "Empty": 1, "All": 9 }; const colMap = new Map(); for (const r of rates) { const cs = r.container_size || "All", ct = r.cargo_type || "All", cf = r.container_fullness || "All"; const k = `${cf}|${cs}|${ct}`; if (!colMap.has(k)) colMap.set(k, { key: k, container_size: cs, cargo_type: ct, container_fullness: cf }); } const cols = [...colMap.values()].sort((a, b) => { const f = (FULL_ORD[a.container_fullness] ?? 9) - (FULL_ORD[b.container_fullness] ?? 9); if (f) return f; const s = (SIZE_ORD[a.container_size] ?? 9) - (SIZE_ORD[b.container_size] ?? 9); if (s) return s; return (TYPE_ORD[a.cargo_type] ?? 9) - (TYPE_ORD[b.cargo_type] ?? 9); }); const cells = new Map(); const rowMeta = new Map(); for (const r of rates) { const cs = r.container_size || "All", ct = r.cargo_type || "All", cf = r.container_fullness || "All"; const colKey = `${cf}|${cs}|${ct}`; const rowKey = [r.cost_type_name, r.shipment_type || "All", r.charge_unit || "", r.slab_unit || "", r.payable_by || "Maersk"].join("|"); if (!rowMeta.has(rowKey)) { rowMeta.set(rowKey, { key: rowKey, cost_type_name: r.cost_type_name, cost_category: r.cost_category, shipment_type: r.shipment_type, charge_unit: r.charge_unit, slab_unit: r.slab_unit, isSlab: !!r.slab_unit, payable_by: r.payable_by || "Maersk", }); } if (!cells.has(rowKey)) cells.set(rowKey, new Map()); const rowCells = cells.get(rowKey); if (r.slab_unit) { if (!rowCells.has(colKey)) rowCells.set(colKey, { isSlab: true, tiers: [], payable_by: r.payable_by || "Maersk" }); rowCells.get(colKey).tiers.push({ id: r.id, rate_local: r.rate_local, currency: r.currency, slab_min: r.slab_min, slab_max: r.slab_max, slab_type: r.slab_type || "Step", }); } else { rowCells.set(colKey, { isSlab: false, id: r.id, rate_local: r.rate_local, currency: r.currency, payable_by: r.payable_by || "Maersk" }); } } for (const [, rowCells] of cells) for (const [, cell] of rowCells) if (cell.isSlab) cell.tiers.sort((a, b) => (a.slab_min ?? 0) - (b.slab_min ?? 0)); const groups = new Map(); const ctCategory = new Map(); for (const [, meta] of rowMeta) { if (!groups.has(meta.cost_type_name)) groups.set(meta.cost_type_name, []); groups.get(meta.cost_type_name).push(meta); if (meta.cost_category) ctCategory.set(meta.cost_type_name, meta.cost_category); } return { cols, cells, groups, rowMeta, ctCategory }; } // ─── Shared calc helpers (module-level for all tabs) ───────────────────────── const normSlabType = s => (s || "").toUpperCase().replace(/[\s_]/g, ""); const isTierRateType = s => { const n = normSlabType(s); return n === "TIERRATE" || n === "TIER"; }; function calcCellResult(cell, inp, roeMap) { const qty = parseFloat(inp.qty); const slabVal = parseFloat(inp.slabVal); if (!qty || qty <= 0) return null; if (cell.isSlab) { if (!slabVal || slabVal <= 0) return null; let total_local = 0, currency = null; const activeTierIds = new Set(); let anyHit = false; const tierRateTiers = cell.tiers.filter(t => isTierRateType(t.slab_type)); if (tierRateTiers.length > 0) { const hit = tierRateTiers.find(t => (t.slab_min == null || slabVal >= t.slab_min) && (t.slab_max == null || slabVal <= t.slab_max) ); if (hit) { const minSlabMin = Math.min(...tierRateTiers.map(t => t.slab_min ?? 0)); const chargeableDays = slabVal - minSlabMin + 1; if (chargeableDays > 0) { total_local += parseFloat(hit.rate_local) * chargeableDays * qty; currency = hit.currency; activeTierIds.add(hit.id); anyHit = true; } } } for (const t of cell.tiers) { if (isTierRateType(t.slab_type)) continue; const st = normSlabType(t.slab_type || "Step"); const inRange = (t.slab_min == null || slabVal >= t.slab_min) && (t.slab_max == null || slabVal <= t.slab_max); if (st === "LUMPSUM") { if (!inRange) continue; total_local += parseFloat(t.rate_local) * qty; currency = t.currency; activeTierIds.add(t.id); anyHit = true; } else { const rangeStart = t.slab_min ?? 1, rangeEnd = t.slab_max ?? slabVal; if (rangeStart > slabVal) continue; const unitsInBracket = Math.min(slabVal, rangeEnd) - rangeStart + 1; total_local += parseFloat(t.rate_local) * unitsInBracket * qty; currency = t.currency; activeTierIds.add(t.id); anyHit = true; } } if (!anyHit) return null; const roe = roeMap[currency] || 1; return { total_local, currency, total_usd: total_local / roe, activeTierIds }; } else { const total_local = parseFloat(cell.rate_local) * qty; const roe = roeMap[cell.currency] || 1; return { total_local, currency: cell.currency, total_usd: total_local / roe }; } } // ─── SingleRateTab ──────────────────────────────────────────────────────────── function SingleRateTab({ user, perms, today }) { const [terminals, setTerminals] = useState([]); const [costTypes, setCostTypes] = useState([]); const [brandCompanies, setBrandCompanies] = useState([]); const [roeMap, setRoeMap] = useState({}); const [terminalCode, setTerminalCode] = useState(""); const [costTypeCode, setCostTypeCode] = useState(""); const [brandCompanyId, setBrandCompanyId] = useState(""); const [rateRows, setRateRows] = useState([]); const [chargeUnitFilter, setChargeUnitFilter] = useState(""); const [shipmentFilter, setShipmentFilter] = useState(""); const [materialFilter, setMaterialFilter] = useState(""); const [sizeFilter, setSizeFilter] = useState(""); const [cargoTypeFilter, setCargoTypeFilter] = useState(""); const [fullnessFilter, setFullnessFilter] = useState(""); const [payableByFilter, setPayableByFilter] = useState(""); const [sharedQty, setSharedQty] = useState(""); const [sharedSlabVal, setSharedSlabVal] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const hideRate = !perms.viewRates; useEffect(() => { Promise.all([ apiFetch("/api/metadata/terminals/scoped"), apiFetch("/api/metadata/cost-types"), apiFetch("/api/roe/latest"), apiFetch("/api/metadata/brand-companies").catch(() => []), ]).then(([terms, cts, roe, bcs]) => { setTerminals(Array.isArray(terms) ? terms : []); setCostTypes(Array.isArray(cts) ? cts : []); const m = {}; (Array.isArray(roe) ? roe : []).forEach(r => { m[r.iso_code] = r.rate; }); setRoeMap(m); setBrandCompanies(Array.isArray(bcs) ? bcs : []); }).catch(() => {}); }, []); const loadRates = async () => { if (!terminalCode || !costTypeCode) return; setLoading(true); setError(null); setRateRows([]); setSharedQty(""); setSharedSlabVal(""); setChargeUnitFilter(""); setShipmentFilter(""); setMaterialFilter(""); setSizeFilter(""); setCargoTypeFilter(""); setFullnessFilter(""); setPayableByFilter(""); try { const params = new URLSearchParams({ terminal_code: terminalCode }); params.append("cost_type_codes", costTypeCode); if (brandCompanyId) params.set("brand_company_id", brandCompanyId); const data = await apiFetch(`/api/calculator/rates?${params}`); if (!Array.isArray(data)) throw new Error("Unexpected response"); setRateRows(data); } catch (e) { setError(e.message || "Failed to load rates"); } setLoading(false); }; // Faceted search: each filter's options = rateRows filtered by ALL OTHER active filters // matKey: use material_name; fall back to cost_type_name when material_name is null const matKey = r => r.material_name || r.cost_type_name || ""; const allFilters = useMemo(() => ({ materialFilter, shipmentFilter, sizeFilter, cargoTypeFilter, fullnessFilter, chargeUnitFilter, payableByFilter }), [materialFilter, shipmentFilter, sizeFilter, cargoTypeFilter, fullnessFilter, chargeUnitFilter, payableByFilter]); const applyFilters = (rows, skip) => rows.filter(r => (skip === 'material' || !allFilters.materialFilter || matKey(r) === allFilters.materialFilter) && (skip === 'shipment' || !allFilters.shipmentFilter || r.shipment_type === allFilters.shipmentFilter) && (skip === 'size' || !allFilters.sizeFilter || r.container_size === allFilters.sizeFilter) && (skip === 'cargoType' || !allFilters.cargoTypeFilter || r.cargo_type === allFilters.cargoTypeFilter) && (skip === 'fullness' || !allFilters.fullnessFilter || r.container_fullness === allFilters.fullnessFilter) && (skip === 'chargeUnit' || !allFilters.chargeUnitFilter || r.charge_unit === allFilters.chargeUnitFilter) && (skip === 'payableBy' || !allFilters.payableByFilter || r.payable_by === allFilters.payableByFilter) ); const availableMaterials = useMemo(() => [...new Set(applyFilters(rateRows, 'material').map(r => matKey(r)).filter(Boolean))].sort(), [rateRows, allFilters]); const availableShipments = useMemo(() => [...new Set(applyFilters(rateRows, 'shipment').map(r => r.shipment_type).filter(Boolean))], [rateRows, allFilters]); const availableSizes = useMemo(() => [...new Set(applyFilters(rateRows, 'size').map(r => r.container_size).filter(Boolean))].sort(), [rateRows, allFilters]); const availableCargoTypes = useMemo(() => [...new Set(applyFilters(rateRows, 'cargoType').map(r => r.cargo_type).filter(Boolean))].sort(), [rateRows, allFilters]); const availableFullness = useMemo(() => [...new Set(applyFilters(rateRows, 'fullness').map(r => r.container_fullness).filter(Boolean))].sort(), [rateRows, allFilters]); const availableChargeUnits = useMemo(() => [...new Set(applyFilters(rateRows, 'chargeUnit').map(r => r.charge_unit).filter(Boolean))], [rateRows, allFilters]); const availablePayableBy = useMemo(() => [...new Set(applyFilters(rateRows, 'payableBy').map(r => r.payable_by).filter(Boolean))].sort(), [rateRows, allFilters]); // Auto-clear stale selected values when they're no longer in the available set useEffect(() => { if (materialFilter && !availableMaterials.includes(materialFilter)) setMaterialFilter(""); }, [availableMaterials]); useEffect(() => { if (shipmentFilter && !availableShipments.includes(shipmentFilter)) setShipmentFilter(""); }, [availableShipments]); useEffect(() => { if (sizeFilter && !availableSizes.includes(sizeFilter)) setSizeFilter(""); }, [availableSizes]); useEffect(() => { if (cargoTypeFilter && !availableCargoTypes.includes(cargoTypeFilter)) setCargoTypeFilter(""); }, [availableCargoTypes]); useEffect(() => { if (fullnessFilter && !availableFullness.includes(fullnessFilter)) setFullnessFilter(""); }, [availableFullness]); useEffect(() => { if (chargeUnitFilter && !availableChargeUnits.includes(chargeUnitFilter)) setChargeUnitFilter(""); }, [availableChargeUnits]); useEffect(() => { if (payableByFilter && !availablePayableBy.includes(payableByFilter)) setPayableByFilter(""); }, [availablePayableBy]); const filteredRows = useMemo(() => applyFilters(rateRows, null), [rateRows, allFilters]); // Per-row results: rate × qty (each tier/row gets its own qty input) const sortedRows = useMemo(() => { const slab = filteredRows.filter(r => r.slab_unit).sort((a, b) => (a.slab_min ?? 0) - (b.slab_min ?? 0)); const nonSlab = filteredRows.filter(r => !r.slab_unit); return [...slab, ...nonSlab]; }, [filteredRows]); const hasSlab = sortedRows.some(r => r.slab_unit); const rowResults = useMemo(() => { const qty = parseFloat(sharedQty); const slabVal = parseFloat(sharedSlabVal); if (!qty || qty <= 0) return {}; const out = {}; // Group slab rows by cost_type|shipment|charge_unit|slab_unit const slabGroups = new Map(); for (const r of sortedRows) { if (!r.slab_unit) continue; const gk = `${r.cost_type_name}|${r.shipment_type || ""}|${r.charge_unit || ""}|${r.slab_unit}`; if (!slabGroups.has(gk)) slabGroups.set(gk, []); slabGroups.get(gk).push(r); } // Process each slab group using the same logic as calcCellResult for (const [, tiers] of slabGroups) { if (!slabVal || slabVal <= 0) { for (const t of tiers) out[t.id] = { total_local: 0, currency: t.currency, total_usd: 0, active: false }; continue; } const sorted = [...tiers].sort((a, b) => (a.slab_min ?? 0) - (b.slab_min ?? 0)); // TIER RATE: find matching tier, chargeableDays from global min slab_min const tierRateTiers = sorted.filter(t => isTierRateType(t.slab_type)); if (tierRateTiers.length > 0) { const minSlabMin = Math.min(...tierRateTiers.map(t => t.slab_min ?? 0)); const chargeableDays = slabVal - minSlabMin + 1; const hit = tierRateTiers.find(t => (t.slab_min == null || slabVal >= t.slab_min) && (t.slab_max == null || slabVal <= t.slab_max) ); for (const t of tierRateTiers) { const roe = roeMap[t.currency] || 1; if (t === hit && chargeableDays > 0) { const total_local = parseFloat(t.rate_local) * chargeableDays * qty; out[t.id] = { total_local, currency: t.currency, total_usd: total_local / roe, active: true }; } else { out[t.id] = { total_local: 0, currency: t.currency, total_usd: 0, active: false }; } } } // LUMPSUM and STEP tiers for (const t of sorted) { if (isTierRateType(t.slab_type)) continue; const st = normSlabType(t.slab_type || "Step"); const roe = roeMap[t.currency] || 1; const inRange = (t.slab_min == null || slabVal >= t.slab_min) && (t.slab_max == null || slabVal <= t.slab_max); if (st === "LUMPSUM") { if (inRange) { const total_local = parseFloat(t.rate_local) * qty; out[t.id] = { total_local, currency: t.currency, total_usd: total_local / roe, active: true }; } else { out[t.id] = { total_local: 0, currency: t.currency, total_usd: 0, active: false }; } } else { // STEP: accumulate units in bracket const rangeStart = t.slab_min ?? 1, rangeEnd = t.slab_max ?? slabVal; if (rangeStart > slabVal) { out[t.id] = { total_local: 0, currency: t.currency, total_usd: 0, active: false }; } else { const unitsInBracket = Math.min(slabVal, rangeEnd) - rangeStart + 1; const total_local = parseFloat(t.rate_local) * unitsInBracket * qty; out[t.id] = { total_local, currency: t.currency, total_usd: total_local / roe, active: true }; } } } } // Non-slab rows: rate × qty for (const r of sortedRows) { if (r.slab_unit) continue; const rate = parseFloat(r.rate_local); if (!rate && rate !== 0) continue; const roe = roeMap[r.currency] || 1; const total_local = rate * qty; out[r.id] = { total_local, currency: r.currency, total_usd: total_local / roe, active: true }; } return out; }, [sortedRows, sharedQty, sharedSlabVal, roeMap]); const totalUsd = Object.values(rowResults).reduce((s, r) => s + (r?.active ? r.total_usd || 0 : 0), 0); const currenciesInUse = [...new Set(filteredRows.map(r => r.currency).filter(Boolean))]; const selectedTerminal = terminals.find(t => t.code === terminalCode); const selectedCostType = costTypes.find(c => c.code === costTypeCode); // Slab cost curve: compute cost at each integer slab value across the full range const slabChartData = useMemo(() => { if (!hasSlab || !sharedQty) return null; const qty = parseFloat(sharedQty); if (!qty || qty <= 0) return null; const slabGroups = new Map(); for (const r of sortedRows) { if (!r.slab_unit) continue; const gk = `${r.cost_type_name}|${r.shipment_type || ""}|${r.charge_unit || ""}|${r.slab_unit}`; if (!slabGroups.has(gk)) slabGroups.set(gk, []); slabGroups.get(gk).push(r); } if (slabGroups.size === 0) return null; const allTiers = [...slabGroups.values()].flat(); const definedMaxes = allTiers.map(t => t.slab_max).filter(v => v != null); const definedMins = allTiers.map(t => t.slab_min ?? 0); const curSV = parseFloat(sharedSlabVal) || 0; const xMax = definedMaxes.length > 0 ? Math.max(Math.max(...definedMaxes) * 1.5, curSV) : Math.max(Math.max(...definedMins) * 1.5 + 30, curSV); const xMin = 0; const calcAt = sv => { let total = 0; for (const [, tiers] of slabGroups) { const sorted = [...tiers].sort((a, b) => (a.slab_min ?? 0) - (b.slab_min ?? 0)); const trTiers = sorted.filter(t => isTierRateType(t.slab_type)); if (trTiers.length > 0) { const minMin = Math.min(...trTiers.map(t => t.slab_min ?? 0)); const days = sv - minMin + 1; const hit = trTiers.find(t => (t.slab_min == null || sv >= t.slab_min) && (t.slab_max == null || sv <= t.slab_max)); if (hit && days > 0) total += parseFloat(hit.rate_local) * days * qty / (roeMap[hit.currency] || 1); } for (const t of sorted) { if (isTierRateType(t.slab_type)) continue; const st = normSlabType(t.slab_type || "Step"); const roe = roeMap[t.currency] || 1; const inRange = (t.slab_min == null || sv >= t.slab_min) && (t.slab_max == null || sv <= t.slab_max); if (st === "LUMPSUM") { if (inRange) total += parseFloat(t.rate_local) * qty / roe; } else { const rs = t.slab_min ?? 1, re = t.slab_max ?? sv; if (rs <= sv) total += parseFloat(t.rate_local) * (Math.min(sv, re) - rs + 1) * qty / roe; } } } return total; }; const step = Math.max(1, Math.ceil(xMax / 80)); const points = []; for (let x = xMin; x <= xMax; x += step) points.push({ x, y: calcAt(x) }); if (points[points.length - 1]?.x !== xMax) points.push({ x: xMax, y: calcAt(xMax) }); const slabRow = sortedRows.find(r => r.slab_unit); const slabUnitLabel = slabRow ? (UNIT_SHORT[slabRow.slab_unit] || slabRow.slab_unit) : ""; const tierBoundaries = [...new Set( [...slabGroups.values()].flat() .flatMap(t => [t.slab_min, t.slab_max]) .filter(v => v != null) )].sort((a, b) => a - b); return { points, xMin, xMax, slabUnitLabel, tierBoundaries }; }, [hasSlab, sharedQty, sharedSlabVal, sortedRows, roeMap]); // Label for each row in left panel qty section and right breakdown const rowLabel = r => { if (!r.slab_unit) return matKey(r) || "—"; const u = UNIT_SHORT[r.slab_unit] || r.slab_unit; if (r.slab_min != null && r.slab_max != null) return `${u} ${r.slab_min}–${r.slab_max}`; if (r.slab_min != null) return `${u} ${r.slab_min}+`; if (r.slab_max != null) return `≤${r.slab_max} ${u}`; return u; }; const rowUnit = r => r.slab_unit ? (UNIT_SHORT[r.slab_unit] || r.slab_unit) : (UNIT_SHORT[r.charge_unit] || r.charge_unit || ""); const filterBlock = (label, value, setter, options, renderOpt) => (
{label}
); // Read-only badge for dimensions with only 1 available option (implied by other filters) const filterBadge = (label, value) => (
{label}
{value} auto
); const hasFilters = availableMaterials.length >= 1 || availableShipments.length >= 1 || availableSizes.length >= 1 || availableCargoTypes.length >= 1 || availableFullness.length >= 1 || availableChargeUnits.length >= 1 || availablePayableBy.length >= 1; // Show qty as soon as any filtered rates are available const allFiltersSet = filteredRows.length > 0; return (
{/* ── Left panel: unified Inputs card ── */}
Inputs
Active rate selected by today's date
{/* Brand Company */}
Brand Company
{/* Terminal */}
Terminal / Port *
{ setTerminalCode(v); setRateRows([]); }} placeholder="Search terminal…" getValue={t => t.code} getLabel={t => `${t.name} (${t.code})`} />
{/* Cost Type */}
Cost Type *
{/* Load Rates button */} {error &&
{error}
} {hideRate && (
Rate values hidden (rate_calculator role)
)} {/* Filters (shown after rates loaded) */} {rateRows.length > 0 && hasFilters && (
{availableMaterials.length > 1 ? filterBlock("Material", materialFilter, setMaterialFilter, availableMaterials, m => ) : availableMaterials.length === 1 ? filterBadge("Material", availableMaterials[0]) : null} {availableShipments.length > 1 ? filterBlock("Shipment Type", shipmentFilter, setShipmentFilter, availableShipments, s => ) : availableShipments.length === 1 ? filterBadge("Shipment Type", availableShipments[0]) : null} {availableSizes.length > 1 ? filterBlock("Container Size", sizeFilter, setSizeFilter, availableSizes, s => ) : availableSizes.length === 1 ? filterBadge("Container Size", availableSizes[0]) : null} {availableCargoTypes.length > 1 ? filterBlock("Container Type", cargoTypeFilter, setCargoTypeFilter, availableCargoTypes, t => ) : availableCargoTypes.length === 1 ? filterBadge("Container Type", availableCargoTypes[0]) : null} {availableFullness.length > 1 ? filterBlock("Fullness", fullnessFilter, setFullnessFilter, availableFullness, f => ) : availableFullness.length === 1 ? filterBadge("Fullness", availableFullness[0]) : null} {availableChargeUnits.length > 1 ? filterBlock("Charge Unit", chargeUnitFilter, setChargeUnitFilter, availableChargeUnits, u => ) : availableChargeUnits.length === 1 ? filterBadge("Charge Unit", UNIT_SHORT[availableChargeUnits[0]] || availableChargeUnits[0]) : null} {availablePayableBy.length > 1 ? filterBlock("Payable By", payableByFilter, setPayableByFilter, availablePayableBy, v => ) : availablePayableBy.length === 1 ? filterBadge("Payable By", availablePayableBy[0]) : null}
)} {/* Quantity inputs */} {allFiltersSet && (
{/* Quantity */}
Quantity *
setSharedQty(e.target.value)} style={{ flex: 1, height: 32, textAlign: "right", fontSize: 13 }} /> {(() => { const nonSlab = sortedRows.find(r => !r.slab_unit); const u = nonSlab ? rowUnit(nonSlab) : (sortedRows[0] ? (UNIT_SHORT[sortedRows[0].charge_unit] || sortedRows[0].charge_unit || "") : ""); return u ? {u} : null; })()}
{/* Slab value — only when slab rows exist */} {hasSlab && (
{(() => { const slabRow = sortedRows.find(r => r.slab_unit); return slabRow ? (UNIT_SHORT[slabRow.slab_unit] || slabRow.slab_unit) : "Slab Value"; })()} *
setSharedSlabVal(e.target.value)} style={{ flex: 1, height: 32, textAlign: "right", fontSize: 13 }} /> {(() => { const slabRow = sortedRows.find(r => r.slab_unit); const u = slabRow ? (UNIT_SHORT[slabRow.slab_unit] || slabRow.slab_unit) : ""; return u ? {u} : null; })()}
)}
)} {/* ROE */} {currenciesInUse.length > 0 && (
{currenciesInUse.map(iso => (
ROE in effect ({iso} / USD) {hideRate ? "****" : roeMap[iso] ? roeMap[iso].toFixed(4) : "—"}
))}
)}
{/* ── Right panel ── */}
{/* Empty state */} {rateRows.length === 0 && (
Select terminal and cost type, then load rates
)} {/* Estimated cost + optional slab chart */} {filteredRows.length > 0 && (
{/* Estimated Cost card */}
Estimated Cost USD
0) ? "var(--teal)" : "var(--ink-mute)", lineHeight: 1.1 }}> {sharedQty ? fmt.usd(totalUsd) : "—"}
{(selectedTerminal || selectedCostType) && (
{[selectedTerminal?.name, selectedCostType?.name, `as of ${today}`].filter(Boolean).join(" · ")}
)}
{/* Slab cost curve chart */} {slabChartData && (() => { const { points, xMin, xMax, slabUnitLabel, tierBoundaries } = slabChartData; const VW = 320, VH = 150, PL = 52, PR = 12, PT = 12, PB = 32; const iW = VW - PL - PR, iH = VH - PT - PB; const yMax = Math.max(...points.map(p => p.y), 0.01); const toX = x => PL + ((x - xMin) / (xMax - xMin || 1)) * iW; const toY = y => PT + (1 - y / yMax) * iH; const poly = points.map(p => `${toX(p.x).toFixed(1)},${toY(p.y).toFixed(1)}`).join(" "); const area = `${PL},${PT + iH} ${poly} ${toX(xMax).toFixed(1)},${PT + iH}`; const curSV = parseFloat(sharedSlabVal); const curPt = curSV > 0 ? points.reduce((a, b) => Math.abs(b.x - curSV) < Math.abs(a.x - curSV) ? b : a) : null; const fmtY = v => v >= 10000 ? `${(v/1000).toFixed(0)}k` : v >= 1000 ? `${(v/1000).toFixed(1)}k` : v.toFixed(0); const yTicks = [0, yMax / 2, yMax]; // X ticks: 0 + tier boundaries + current SlabVal, deduped, sorted, crowding removed const slabValTick = curSV > 0 && curSV <= xMax ? curSV : null; const xTicks = [...new Set([xMin, ...tierBoundaries.filter(v => v <= xMax), ...(slabValTick ? [slabValTick] : [])])] .sort((a, b) => a - b) .filter((v, i, arr) => i === 0 || (toX(v) - toX(arr[i - 1])) >= 20); return (
Cost vs {slabUnitLabel}
{/* Grid */} {yTicks.map((y, i) => ( ))} {/* Area fill */} {/* Line */} {/* Current slab val marker */} {curPt && ( <> {fmtY(curPt.y)} )} {/* Axes */} {/* Y labels */} {yTicks.map((y, i) => ( {fmtY(y)} ))} {/* X labels: tier boundaries in grey, SlabVal in amber */} {xTicks.map((x, i) => { const isSlabVal = slabValTick != null && x === slabValTick; return ( {x} ); })} {/* X axis unit */} {slabUnitLabel}
); })()}
)} {/* Breakdown table — only when cost is calculated */} {sharedQty && totalUsd > 0 && (
Multi-tier Breakdown
{sortedRows.length === 0 ? ( ) : sortedRows.map(r => { const res = rowResults[r.id]; const qty = parseFloat(sharedQty); const label = rowLabel(r); const unit = rowUnit(r); const isActive = res?.active; const subtitle = (!hideRate && r.rate_local && qty && isActive) ? `${parseFloat(r.rate_local).toFixed(2)} ${r.currency} × ${res.total_local / parseFloat(r.rate_local) > qty ? `${(res.total_local / parseFloat(r.rate_local) / qty).toFixed(0)} ${unit} × ${qty}` : `${qty} ${unit}`}` : null; return ( ); })} {sortedRows.length > 0 && ( )}
No rates match current filters
{label}
{subtitle &&
{subtitle}
}
{hideRate ? **** : isActive ? {res.total_local.toFixed(2)} {res.currency} : } {hideRate ? **** : isActive ? {fmt.usd(res.total_usd)} : }
Total (USD) 0) ? "var(--teal)" : "var(--ink-mute)" }}> {hideRate ? **** : sharedQty ? fmt.usd(totalUsd) : "—"}
)}
); } // ─── TerminalMatrixTab ──────────────────────────────────────────────────────── function TerminalMatrixTab({ user, perms, today }) { const [terminals, setTerminals] = useState([]); const [costTypes, setCostTypes] = useState([]); const [brandCompanies, setBrandCompanies] = useState([]); const [roeMap, setRoeMap] = useState({}); const [terminalFilter, setTerminalFilter] = useState(""); const [costTypeFilter, setCostTypeFilter] = useState([]); const [brandCompanyId, setBrandCompanyId] = useState(""); const [pivot, setPivot] = useState(null); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [cellInputs, setCellInputs] = useState({}); const [hoveredCell, setHoveredCell] = useState(null); const [boxTypeFilter, setBoxTypeFilter] = useState(new Set()); const hideRate = !perms.viewRates; useEffect(() => { Promise.all([ apiFetch("/api/metadata/terminals/scoped"), apiFetch("/api/metadata/cost-types"), apiFetch("/api/roe/latest"), apiFetch("/api/metadata/brand-companies").catch(() => []), ]).then(([terms, cts, roe, bcs]) => { setTerminals(Array.isArray(terms) ? terms : []); setCostTypes(Array.isArray(cts) ? cts : []); const m = {}; (Array.isArray(roe) ? roe : []).forEach(r => { m[r.iso_code] = r.rate; }); setRoeMap(m); setBrandCompanies(Array.isArray(bcs) ? bcs : []); }).catch(() => {}); }, []); const toggleCostType = code => { setCostTypeFilter(prev => prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]); setPivot(null); setCellInputs({}); }; const loadRates = async () => { if (!terminalFilter || costTypeFilter.length === 0) return; setLoading(true); setLoadError(null); setPivot(null); setCellInputs({}); setBoxTypeFilter(new Set()); try { const params = new URLSearchParams({ terminal_code: terminalFilter }); costTypeFilter.forEach(c => params.append("cost_type_codes", c)); if (brandCompanyId) params.set("brand_company_id", brandCompanyId); const data = await apiFetch(`/api/calculator/rates?${params}`); if (!Array.isArray(data)) throw new Error("Unexpected response"); setPivot(buildPivot(data)); } catch (e) { setLoadError(e.message || "Failed to load rates"); } setLoading(false); }; const setCellInput = (rowKey, colKey, field, val) => { const k = `${rowKey}|${colKey}`; setCellInputs(prev => ({ ...prev, [k]: { ...prev[k], [field]: val } })); }; const cellResults = useMemo(() => { if (!pivot) return {}; const results = {}; for (const [rowKey, rowCells] of pivot.cells) { for (const [colKey, cell] of rowCells) { const cellKey = `${rowKey}|${colKey}`; const res = calcCellResult(cell, cellInputs[cellKey] || {}, roeMap); if (res) results[cellKey] = res; } } return results; }, [pivot, cellInputs, roeMap]); const grandTotal = useMemo(() => { let total = 0, hasAny = false; for (const r of Object.values(cellResults)) { if (r?.total_usd != null) { total += r.total_usd; hasAny = true; } } return hasAny ? total : null; }, [cellResults]); const categoryTotals = useMemo(() => { if (!pivot) return []; // Map }> const map = new Map(); for (const [rowKey, rowCells] of pivot.cells) { const meta = pivot.rowMeta.get(rowKey); const cat = meta?.cost_category || "Other"; const ctName = meta?.cost_type_name || "—"; if (!map.has(cat)) map.set(cat, { total: 0, byType: new Map() }); const entry = map.get(cat); for (const [colKey] of rowCells) { const res = cellResults[`${rowKey}|${colKey}`]; if (res?.total_usd) { entry.total += res.total_usd; entry.byType.set(ctName, (entry.byType.get(ctName) || 0) + res.total_usd); } } } return [...map.entries()] .filter(([, e]) => e.total > 0) .sort((a, b) => b[1].total - a[1].total); }, [cellResults, pivot]); const payableByTotals = useMemo(() => { if (!pivot) return []; const map = new Map(); for (const [rowKey, rowCells] of pivot.cells) { for (const [colKey, cell] of rowCells) { const res = cellResults[`${rowKey}|${colKey}`]; if (!res?.total_usd) continue; const pb = cell.payable_by || "Maersk"; map.set(pb, (map.get(pb) || 0) + res.total_usd); } } return [...map.entries()].filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]); }, [cellResults, pivot]); const selectedTerminal = terminals.find(t => t.code === terminalFilter); const COL_W = 110; const visibleCols = pivot ? pivot.cols.filter(col => boxTypeFilter.size === 0 || boxTypeFilter.has(col.key)) : []; const toggleBoxType = colKey => { setBoxTypeFilter(prev => { const base = prev.size === 0 ? new Set(pivot.cols.map(c => c.key)) : new Set(prev); if (base.has(colKey)) base.delete(colKey); else base.add(colKey); return base.size === pivot.cols.length ? new Set() : base; }); }; return (
{/* Compact filter bar */}
{ setTerminalFilter(v); setPivot(null); setCellInputs({}); }} placeholder="Search terminal…" getValue={t => t.code} getLabel={t => `${t.name} (${t.code})`} /> Cost Types} hint="Multi-select">
{(() => { const groups = new Map(); for (const ct of costTypes) { const cat = ct.category || "Other"; if (!groups.has(cat)) groups.set(cat, []); groups.get(cat).push(ct); } return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([cat, items]) => { const codes = items.map(i => i.code); const allOn = codes.every(c => costTypeFilter.includes(c)); const toggleCat = () => { if (allOn) { setCostTypeFilter(prev => prev.filter(c => !codes.includes(c))); } else { setCostTypeFilter(prev => [...new Set([...prev, ...codes])]); } setPivot(null); setCellInputs({}); }; return (
{cat}
{items.map(ct => { const on = costTypeFilter.includes(ct.code); return ( ); })}
); }); })()}
{loadError && {loadError}}
{pivot && }
{/* Box type filter chips */} {pivot && pivot.cols.length > 0 && (
Box Types:
{pivot.cols.map(col => { const on = boxTypeFilter.size === 0 || boxTypeFilter.has(col.key); const label = [col.container_fullness !== "All" ? col.container_fullness : null, col.container_size !== "All" ? col.container_size : null, col.cargo_type !== "All" ? col.cargo_type : null].filter(Boolean).join(" ") || "All"; return ( ); })}
)} {pivot && pivot.cols.length === 0 && (
No active rates with Calculator flag found.
)} {pivot && pivot.cols.length > 0 && (
{/* Matrix table — minWidth:0 lets the grid item shrink so overflow-x:auto works */}
{visibleCols.map(col => { const fullPart = col.container_fullness !== "All" ? col.container_fullness : null; const sizePart = col.container_size !== "All" ? col.container_size : null; const typePart = col.cargo_type !== "All" ? col.cargo_type : null; const bottomLabel = [sizePart, typePart].filter(Boolean).join(" ") || "All"; return ( ); })} {[...pivot.groups.entries()] .sort(([a], [b]) => { const ca = pivot.ctCategory.get(a) || "", cb = pivot.ctCategory.get(b) || ""; return ca !== cb ? ca.localeCompare(cb) : a.localeCompare(b); }) .map(([ctName, rows]) => ( {visibleCols.map(col => ( {[...rows].sort((a, b) => { const st = (a.shipment_type || "All").localeCompare(b.shipment_type || "All"); if (st !== 0) return st; return (a.payable_by || "Maersk").localeCompare(b.payable_by || "Maersk"); }).map(meta => { const rowCells = pivot.cells.get(meta.key) || new Map(); const unitLabel = UNIT_SHORT[meta.charge_unit] || meta.charge_unit || ""; return ( {visibleCols.map(col => { const cell = rowCells.get(col.key); const cellKey = `${meta.key}|${col.key}`; const inp = cellInputs[cellKey] || {}; const res = cellResults[cellKey]; if (!cell) return ( ); return ( ); })} ); })} ))} {grandTotal != null && ( {visibleCols.map(col => { let colSum = 0; for (const [rowKey] of pivot.cells) { const r = cellResults[`${rowKey}|${col.key}`]; if (r?.total_usd) colSum += r.total_usd; } return ( ); })} )}
Shipment Type / Payable By {fullPart &&
{fullPart}
}
{bottomLabel}
{ctName} {pivot.ctCategory.get(ctName) && ( {pivot.ctCategory.get(ctName)} )} ))}
{meta.shipment_type && meta.shipment_type.toLowerCase() !== "all" ? meta.shipment_type.charAt(0).toUpperCase() + meta.shipment_type.slice(1).toLowerCase() : "All"}
{meta.payable_by && (
{meta.payable_by}
)}
!hideRate && setHoveredCell(cellKey)} onMouseLeave={() => !hideRate && setHoveredCell(null)} style={{ padding: "6px 8px", verticalAlign: "top", borderLeft: "1px solid var(--line)", position: "relative", background: res ? "var(--teal-soft)" : undefined }}> {!hideRate && hoveredCell === cellKey && (cell.isSlab || res) && (
{cell.isSlab && (
{cell.tiers.map((t, ti) => { const isActive = res?.activeTierIds instanceof Set ? res.activeTierIds.has(t.id) : false; const range = t.slab_min != null && t.slab_max != null ? `${t.slab_min}–${t.slab_max}` : t.slab_min != null ? `≥${t.slab_min}` : t.slab_max != null ? `≤${t.slab_max}` : "All"; const _st = (t.slab_type || "Step").toUpperCase().replace(/[\s_]/g, ""); const isLump = _st === "LUMPSUM", isTier = _st === "TIERRATE" || _st === "TIER"; const bg = isLump ? "#e8f4fd" : isTier ? "#fff0e6" : "rgba(0,0,0,0.06)"; const color = isLump ? "#0066cc" : isTier ? "#c45000" : "var(--ink-mute)"; const bdColor = isLump ? "#90c8f0" : isTier ? "#f4a86b" : "transparent"; const label = isLump ? "LUMPSUM" : isTier ? "TIER" : "STEP"; return (
{range} {label}
); })}
)} {res && (
{fmt.usd(res.total_usd)}
)}
)}
setCellInput(meta.key, col.key, "qty", e.target.value.replace(/[^0-9.]/g, ""))} style={{ width: "100%", height: 26, textAlign: "right", fontSize: 12 }} /> {unitLabel && {unitLabel}}
{cell.isSlab && (
setCellInput(meta.key, col.key, "slabVal", e.target.value.replace(/[^0-9.]/g, ""))} style={{ width: "100%", height: 26, textAlign: "right", fontSize: 12 }} /> {meta.slab_unit}
)}
Grand Total (USD) {colSum > 0 ? {fmt.usd(colSum)} : }
{/* end minWidth:0 wrapper */} {/* Right sidebar */}
Estimated Total
{grandTotal != null ? fmt.usd(grandTotal) : "—"}
{Object.keys(cellResults).length} cells filled
{categoryTotals.length > 0 && (
{categoryTotals.map(([cat, entry]) => { const pct = grandTotal ? (entry.total / grandTotal) * 100 : 0; const ctRows = [...entry.byType.entries()].sort((a, b) => b[1] - a[1]); return (
{/* Category row + bar */}
{cat} {fmt.usd(entry.total)}
{/* Cost type breakdown */} {ctRows.map(([ctName, ctVal]) => { const ctPct = entry.total ? (ctVal / entry.total) * 100 : 0; return (
{ctName} {fmt.usd(ctVal)}
); })}
); })}
)} {payableByTotals.length > 0 && (
{payableByTotals.map(([pb, val]) => { const pct = grandTotal ? (val / grandTotal) * 100 : 0; return (
{pb} {fmt.usd(val)}
); })}
)} {hideRate && (
Rate values hidden (rate_calculator role)
)}
)}
); } // ─── Compare helpers ────────────────────────────────────────────────────────── // Group filtered rates into logical rate units. // Slab rates sharing (slab_type, slab_unit) + same dimension fields are one group (tiers). // Non-slab rates are each their own group. function groupRates(rates, matKeyFn) { const nonSlab = rates.filter(r => !r.slab_unit); const slabRates = rates.filter(r => r.slab_unit); const dimFields = r => ({ shipment_type: r.shipment_type || null, cargo_type: r.cargo_type || null, container_size: r.container_size || null, container_fullness: r.container_fullness || null, material_name: matKeyFn(r) || null, currency: r.currency || null, charge_unit: r.charge_unit || null, effective_date: r.effective_date || null, expiry_date: r.expiry_date || null, transport_type: r.transport_type || null, payment_terms: r.payment_terms || null, sap_vendor_code: r.sap_vendor_code || null, contract_ref: r.contract_ref || null, contract_owner: r.contract_owner || null, corridor_from: r.corridor_from || null, corridor_to: r.corridor_to || null, route: r.route || null, is_invoicerate: r.is_invoicerate != null ? String(r.is_invoicerate) : null, stcy_flag: r.stcy_flag != null ? String(r.stcy_flag) : null, notes: r.notes || null, brand_company_code: r.brand_company_code || null, payable_by: r.payable_by || null, }); const groups = nonSlab.map(r => ({ isSlab: false, ...dimFields(r), slab_unit: null, slab_type: null, rates: [r], total_usd: null, })); const slabMap = new Map(); for (const r of slabRates) { const key = [ r.slab_type, r.slab_unit, r.charge_unit, r.shipment_type, r.cargo_type, r.container_size, r.container_fullness, matKeyFn(r), r.brand_company_code || "", r.payable_by || "", ].join("|||"); if (!slabMap.has(key)) { slabMap.set(key, { isSlab: true, ...dimFields(r), slab_unit: r.slab_unit, slab_type: r.slab_type, rates: [], total_usd: null, }); } slabMap.get(key).rates.push(r); } groups.push(...slabMap.values()); return groups; } // Given the logical groups for ONE terminal, return labels of fields // that have more than one distinct value across groups (the "why" of multi-rate). const DIFF_DIMS = [ { key: "shipment_type", label: "Shipment Type" }, { key: "cargo_type", label: "Cargo Type" }, { key: "container_size", label: "Container Size" }, { key: "container_fullness", label: "Fullness" }, { key: "material_name", label: "Material" }, { key: "charge_unit", label: "Charge Unit" }, { key: "currency", label: "Currency" }, { key: "slab_unit", label: "Slab Unit" }, { key: "effective_date", label: "Effective Date" }, { key: "expiry_date", label: "Expiry Date" }, { key: "transport_type", label: "Transport Type" }, { key: "payment_terms", label: "Payment Terms" }, { key: "sap_vendor_code", label: "Vendor" }, { key: "contract_ref", label: "Contract Ref" }, { key: "contract_owner", label: "Contract Owner" }, { key: "corridor_from", label: "Corridor From" }, { key: "corridor_to", label: "Corridor To" }, { key: "route", label: "Route" }, { key: "is_invoicerate", label: "Invoice Rate" }, { key: "stcy_flag", label: "STCY Flag" }, { key: "notes", label: "Notes" }, { key: "brand_company_code", label: "Brand Company" }, { key: "payable_by", label: "Payable By" }, ]; function findDifferingFields(groups) { if (groups.length <= 1) return []; return DIFF_DIMS .filter(d => new Set(groups.map(g => g[d.key] || "")).size > 1) .map(d => d.label); } // ─── MultiTerminalSelect ────────────────────────────────────────────────────── function MultiTerminalSelect({ options, selected, onChange, disabled, placeholder }) { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); const wrapRef = React.useRef(null); React.useEffect(() => { if (!open) return; const handler = e => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [open]); const norm = s => (s || "").toLowerCase(); const tokens = norm(search).split(/\s+/).filter(Boolean); const filtered = options.filter(t => { if (!tokens.length) return true; const hay = norm(`${t.name} ${t.code}`); return tokens.every(tok => hay.includes(tok)); }); const selectedCodes = new Set(selected.map(t => t.code)); const toggle = t => { if (selectedCodes.has(t.code)) onChange(selected.filter(s => s.code !== t.code)); else onChange([...selected, { code: t.code, name: t.name }]); }; const selectAll = () => { const extra = filtered.filter(t => !selectedCodes.has(t.code)); onChange([...selected, ...extra]); }; const clearAll = () => onChange([]); const triggerLabel = selected.length === 0 ? (placeholder || "Select terminals…") : `${selected.length} terminal${selected.length !== 1 ? "s" : ""} selected`; return (
{open && (
setSearch(e.target.value)} style={{ flex: 1, height: 28, fontSize: 12, padding: "0 8px" }} />
{filtered.length === 0 && (
No terminals found
)} {filtered.map(t => { const checked = selectedCodes.has(t.code); return ( ); })}
)}
); } // ─── MultiCompareTab ────────────────────────────────────────────────────────── function MultiCompareTab({ user, perms, today }) { const [terminals, setTerminals] = useState([]); const [costTypes, setCostTypes] = useState([]); const [brandCompanies, setBrandCompanies] = useState([]); const [roeMap, setRoeMap] = useState({}); const [costTypeCode, setCostTypeCode] = useState(""); const [materialFilter, setMaterialFilter] = useState(""); const [shipmentFilter, setShipmentFilter] = useState(""); const [sizeFilter, setSizeFilter] = useState(""); const [cargoTypeFilter, setCargoTypeFilter] = useState(""); const [fullnessFilter, setFullnessFilter] = useState(""); const [payableByFilter, setPayableByFilter] = useState(""); const [brandCompanyId, setBrandCompanyId] = useState(""); const [qty, setQty] = useState(""); const [slabVal, setSlabVal] = useState(""); const [materials, setMaterials] = useState([]); const [qualifiedTerminals, setQualifiedTerminals] = useState([]); const [moveTypes, setMoveTypes] = useState([]); const [equipmentTypes, setEquipmentTypes] = useState([]); const [loadingTerminals, setLoadingTerminals] = useState(false); const [selectedTerminals, setSelectedTerminals] = useState([]); const [compareResults, setCompareResults] = useState([]); const [displayOrder, setDisplayOrder] = useState([]); const [expandedTerminals, setExpandedTerminals] = useState(new Set()); const [checkedGroups, setCheckedGroups] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { Promise.all([ apiFetch("/api/metadata/terminals/scoped"), apiFetch("/api/metadata/cost-types"), apiFetch("/api/roe/latest"), apiFetch("/api/metadata/materials"), apiFetch("/api/metadata/move-types"), apiFetch("/api/metadata/equipment-types"), apiFetch("/api/metadata/brand-companies").catch(() => []), ]).then(([terms, cts, roe, mats, mvt, eqt, bcs]) => { setTerminals(Array.isArray(terms) ? terms : []); setCostTypes(Array.isArray(cts) ? cts : []); const m = {}; (Array.isArray(roe) ? roe : []).forEach(r => { m[r.iso_code] = r.rate; }); setRoeMap(m); setMaterials(Array.isArray(mats) ? mats : []); setMoveTypes(Array.isArray(mvt) ? mvt : []); setEquipmentTypes(Array.isArray(eqt) ? eqt : []); setBrandCompanies(Array.isArray(bcs) ? bcs : []); }).catch(() => {}); }, []); // Single query: SELECT DISTINCT terminals with calculator=TRUE rates matching all Step 1 filters. // Runs whenever cost type or any filter changes. useEffect(() => { if (!costTypeCode) { setQualifiedTerminals([]); setSelectedTerminals([]); setCompareResults([]); return; } let cancelled = false; setLoadingTerminals(true); setSelectedTerminals([]); setCompareResults([]); const p = new URLSearchParams({ cost_type_code: costTypeCode }); if (materialFilter) p.set("material_name", materialFilter); if (shipmentFilter) p.set("shipment_type", shipmentFilter); if (sizeFilter) p.set("container_size", sizeFilter); if (cargoTypeFilter) p.set("cargo_type", cargoTypeFilter); if (fullnessFilter) p.set("container_fullness", fullnessFilter); if (brandCompanyId) p.set("brand_company_id", brandCompanyId); apiFetch(`/api/calculator/terminals?${p}`) .then(data => { if (cancelled) return; setQualifiedTerminals(Array.isArray(data) ? data : []); setLoadingTerminals(false); }) .catch(() => { if (!cancelled) { setQualifiedTerminals([]); setLoadingTerminals(false); } }); return () => { cancelled = true; }; }, [costTypeCode, materialFilter, shipmentFilter, sizeFilter, cargoTypeFilter, fullnessFilter, brandCompanyId]); const matKey = r => r.material_name || r.cost_type_name || ""; const removeTerminal = code => { setSelectedTerminals(prev => prev.filter(t => t.code !== code)); setCompareResults([]); }; const runCompare = async () => { if (selectedTerminals.length === 0 || !costTypeCode || !qty) return; setLoading(true); setError(null); setCompareResults([]); setDisplayOrder([]); setExpandedTerminals(new Set()); setCheckedGroups({}); try { const results = await Promise.all(selectedTerminals.map(async term => { const empty = (err) => ({ terminal: term, total_usd: null, error: err, rateGroups: [], differingFields: [] }); try { const p = new URLSearchParams({ terminal_code: term.code }); p.append("cost_type_codes", costTypeCode); if (brandCompanyId) p.set("brand_company_id", brandCompanyId); const data = await apiFetch(`/api/calculator/rates?${p}`); if (!Array.isArray(data)) return empty("No data"); const filtered = data.filter(r => (!materialFilter || matKey(r) === materialFilter) && (!shipmentFilter || r.shipment_type === shipmentFilter) && (!sizeFilter || r.container_size === sizeFilter) && (!cargoTypeFilter || r.cargo_type === cargoTypeFilter) && (!fullnessFilter || r.container_fullness === fullnessFilter) && (!payableByFilter || r.payable_by === payableByFilter) ); if (filtered.length === 0) return empty("No matching rates"); const rateGroups = groupRates(filtered, matKey); const differingFields = findDifferingFields(rateGroups); let total_usd = 0; for (const grp of rateGroups) { let res; if (grp.isSlab) { const cell = { isSlab: true, tiers: grp.rates.map(r => ({ id: r.id, rate_local: r.rate_local, currency: r.currency, slab_min: r.slab_min, slab_max: r.slab_max, slab_type: r.slab_type || "Step" })) }; res = calcCellResult(cell, { qty: parseFloat(qty), slabVal: parseFloat(slabVal) }, roeMap); } else { const r = grp.rates[0]; res = calcCellResult({ isSlab: false, rate_local: r.rate_local, currency: r.currency }, { qty: parseFloat(qty) }, roeMap); } grp.total_usd = res?.total_usd ?? null; if (grp.total_usd) total_usd += grp.total_usd; } return { terminal: term, total_usd: total_usd, error: null, rateGroups, differingFields }; } catch { return empty("Request failed"); } })); const sorted = results.sort((a, b) => { if (a.total_usd == null && b.total_usd == null) return 0; if (a.total_usd == null) return 1; if (b.total_usd == null) return -1; return a.total_usd - b.total_usd; }); setCompareResults(sorted); setDisplayOrder(sorted.map(r => r.terminal.code)); } catch (e) { setError(e.message || "Comparison failed"); } setLoading(false); }; const isGroupChecked = (termCode, gi) => (checkedGroups[`${termCode}:${gi}`] ?? true); const resultByCode = useMemo(() => { const map = new Map(); for (const r of compareResults) { const checkedTotal = (r.rateGroups || []).reduce((sum, grp, gi) => isGroupChecked(r.terminal.code, gi) && grp.total_usd != null ? sum + grp.total_usd : sum, 0); map.set(r.terminal.code, { ...r, display_total: r.error ? null : checkedTotal }); } return map; }, [compareResults, checkedGroups]); const displayResults = useMemo(() => displayOrder.map(code => resultByCode.get(code)).filter(Boolean), [displayOrder, resultByCode] ); const refreshRanking = () => { setDisplayOrder([...resultByCode.values()] .sort((a, b) => { if (a.display_total == null && b.display_total == null) return 0; if (a.display_total == null) return 1; if (b.display_total == null) return -1; return a.display_total - b.display_total; }) .map(r => r.terminal.code) ); }; const validResults = displayResults.filter(r => r.display_total != null); const maxUsd = validResults.length ? Math.max(...validResults.map(r => r.display_total)) : 1; const minUsd = validResults.length ? Math.min(...validResults.map(r => r.display_total)) : 0; return (
{(() => { const selectedCt = costTypes.find(c => c.code === costTypeCode); const opts = costTypeCode ? [{ value: "", label: "All" }, ...materials.filter(m => m.cost_type_name === selectedCt?.name).map(m => m.name).filter(Boolean).map(n => ({ value: n, label: n }))] : [{ value: "", label: "All" }]; return ( { setMaterialFilter(v); setCompareResults([]); }} placeholder={costTypeCode ? "All" : "Select cost type first"} getValue={o => o.value} getLabel={o => o.label} disabled={!costTypeCode} /> ); })()} { if (!/[\d.]/.test(e.key) && !["Backspace","Delete","ArrowLeft","ArrowRight","Tab"].includes(e.key)) e.preventDefault(); }} onChange={e => { setQty(e.target.value); setCompareResults([]); }} /> { if (!/[\d.]/.test(e.key) && !["Backspace","Delete","ArrowLeft","ArrowRight","Tab"].includes(e.key)) e.preventDefault(); }} onChange={e => { setSlabVal(e.target.value); setCompareResults([]); }} />
{loadingTerminals && (
Checking terminals for matching rates…
)} {!loadingTerminals && costTypeCode && (
{qualifiedTerminals.length} terminal{qualifiedTerminals.length !== 1 ? "s" : ""} with matching rates
)} { setSelectedTerminals(v); setCompareResults([]); }} disabled={!costTypeCode || loadingTerminals} placeholder={!costTypeCode ? "Select a cost type first" : loadingTerminals ? "Loading…" : "Select terminals…"} /> {selectedTerminals.length === 0 && !loadingTerminals && (
Select at least 2 terminals to compare
)} {selectedTerminals.length > 0 && (
{selectedTerminals.map(t => (
{t.code} {t.name}
))}
)}
{error && {error}}
{compareResults.length > 0 && (() => { const allDiffFields = [...new Set(displayResults.flatMap(r => r.differingFields || []))]; const hasMultiRate = displayResults.some(r => (r.rateGroups || []).length > 1); const toggleExpand = code => setExpandedTerminals(prev => { const next = new Set(prev); next.has(code) ? next.delete(code) : next.add(code); return next; }); return ( Refresh Ranking}>
{hasMultiRate && (
Some terminals below sum multiple rates (differing: {allDiffFields.join(", ")}). Apply the relevant filters in Step 1 for a like-for-like comparison. Click a row to see the breakdown.
)}
{/* ── Left: ranked list ── */}
{displayResults.map((r, i) => { const groups = r.rateGroups || []; const multiRate = groups.length > 1; const isExpanded = expandedTerminals.has(r.terminal.code); return (
multiRate && toggleExpand(r.terminal.code)} style={{ padding: "10px 14px", display: "grid", gridTemplateColumns: "26px 1fr auto 110px", gap: 10, alignItems: "center", cursor: multiRate ? "pointer" : "default" }} >
#{i + 1}
{r.terminal.name} {multiRate && ( Σ {groups.length} {isExpanded ? "▲" : "▼"} )}
{r.terminal.code} {(r.differingFields || []).map(f => ( {f} ))}
{r.display_total > 0 &&
}
0 ? "var(--teal)" : "var(--ink)", fontSize: 13 }}> {r.display_total != null ? fmt.usd(r.display_total) : {r.error || "N/A"}}
{multiRate && isExpanded && (
{groups.map((grp, gi) => { const checked = isGroupChecked(r.terminal.code, gi); return (
e.stopPropagation()} onChange={e => { e.stopPropagation(); setCheckedGroups(prev => ({ ...prev, [`${r.terminal.code}:${gi}`]: e.target.checked })); }} style={{ flexShrink: 0, cursor: "pointer" }} /> {DIFF_DIMS.filter(d => (r.differingFields || []).includes(d.label) && d.key !== "slab_unit").map(d => { const val = grp[d.key]; const display = val != null ? val : "—"; return {display}; })} {grp.isSlab && TIER · {grp.slab_unit}}
{grp.total_usd != null ? fmt.usd(grp.total_usd) : "—"}
{grp.isSlab && (
{[...grp.rates].sort((a, b) => (a.slab_min ?? 0) - (b.slab_min ?? 0)).map(sr => (
{sr.slab_min ?? 0} – {sr.slab_max ?? "∞"} {grp.slab_unit} @ {sr.rate_local} {sr.currency}
))}
)}
); })}
)}
); })}
{/* ── Right: Range across selection ── */}
Range across selection
{validResults.length >= 2 ? ( <>
{fmt.usd(minUsd)}
to
{fmt.usd(maxUsd)}
Spread 0 ? "var(--amber)" : "var(--teal)" }}>{fmt.usd(maxUsd - minUsd)}
Savings vs highest {maxUsd > 0 ? ((maxUsd - minUsd) / maxUsd * 100).toFixed(1) : 0}%
All terminals
{displayResults.map((r, i) => (
#{i + 1} {r.terminal.code}
0 ? "var(--teal)" : "var(--ink)", flexShrink: 0 }}> {r.display_total != null ? fmt.usd(r.display_total) : N/A}
))}
) : (
Compare 2+ terminals to see range.
)}
); })()}
); } // ─── Calculator top-level ───────────────────────────────────────────────────── function Calculator({ user, perms }) { const [activeTab, setActiveTab] = useState("single"); const today = new Date().toISOString().slice(0, 10); const tabs = [ { id: "single", label: "Single Rate" }, { id: "matrix", label: "Terminal Matrix" }, { id: "compare", label: "Multi-terminal Compare" }, ]; return (
{tabs.map(t => ( ))}
); } // ============== AI ASSISTANT ============== function AIAssistant({ user, perms, drawerMode = false, onClose }) { const [thread, setThread] = useState([ { role: "ai", content: "Hello Ravi — I have read access to all APA region rate data. Ask me to compare terminals, run trend analysis, simulate cost impacts, or look up contracts." }, { role: "user", content: "Compare CY Handling costs across all North Asia terminals for Q2 2025." }, { role: "ai", type: "comparison" }, { role: "user", content: "If CY Handling at Shanghai increases by 10%, what is the total cost impact on 5,000 TEU monthly volume?" }, { role: "ai", type: "simulation" }, ]); const [input, setInput] = useState(""); const prompts = [ "Show me how Storage rates at Ningbo have changed over the past 2 years", "Which terminal has the lowest reefer plug-in rate in Southeast Asia?", "What contract covers Tianjin Storage rates?", "Compare gate move costs across SEA terminals for 2025", ]; const Body = ( <>
{thread.map((m, i) => (
{!m.type &&
{m.content}
} {m.type === "comparison" && } {m.type === "simulation" && }
{m.role === "user" ? user.name : "Rate Repository AI · DIFY"} · 10:4{i} am
))}
{prompts.slice(0, drawerMode ? 2 : 4).map((p, i) => ( ))}