// §3.11 Total Cost of Ownership (TCO) Analysis function TCOAnalysis({ user, perms }) { const [tab, setTab] = useState("league"); const [openTerminal, setOpenTerminal] = useState(null); const tabs = [ { id: "league", label: "Cost League Table" }, { id: "waterfall", label: "Cost Waterfall" }, { id: "benchmark", label: "Benchmarking" }, { id: "trend", label: "Cost Trend" }, { id: "whatif", label: "What-if Simulation" }, ]; return (
} /> {tab === "league" && } {tab === "waterfall" && } {tab === "benchmark" && } {tab === "trend" && } {tab === "whatif" && } {openTerminal && setOpenTerminal(null)} />}
); } function TCOKpis() { const totalGross = TCO.reduce((s, t) => s + t.gross, 0); const totalRebate = TCO.reduce((s, t) => s + t.rebate, 0); const totalNet = totalGross - totalRebate; const totalTEU = TCO.reduce((s, t) => s + t.monthlyTEU, 0); const blendedNet = totalNet / totalTEU; const yield_ = (totalRebate / totalGross) * 100; return (
); } // ============== LEAGUE TABLE ============== function CostLeague({ onOpen }) { const apaAvg = TCO.reduce((s, t) => s + t.netPerTEU, 0) / TCO.length; return (
Below avg Near avg ±10% Above avg
{TCO.slice(0, 16).map((t, i) => { const delta = ((t.netPerTEU - apaAvg) / apaAvg) * 100; const status = delta < -5 ? "green" : delta < 10 ? "amber" : "red"; const color = status === "green" ? "var(--green)" : status === "amber" ? "var(--amber)" : "var(--red)"; return ( onOpen(t)} style={{ cursor: "pointer" }}> ); })}
Rank Terminal Region Monthly TEU Gross / TEU Rebate Yield Net / TEU vs APA avg MoM
#{i + 1}
{t.terminalName}
{t.terminal}
{t.region} {fmt.num(t.monthlyTEU, 0)} {fmt.usd(t.grossPerTEU)} 0 ? "var(--green)" : "var(--ink-mute)" }}> {t.rebateYield > 0 ? "-" + t.rebateYield.toFixed(2) + "%" : "—"} {fmt.usd(t.netPerTEU)}
{delta > 0 ? "+" : ""}{delta.toFixed(1)}%
{t.momChange > 0 ? "↑" : "↓"} {Math.abs(t.momChange).toFixed(1)}%
); } // ============== WATERFALL CHART ============== function CostWaterfall() { const [terminal, setTerminal] = useState("CNSHA"); const t = TCO.find(x => x.terminal === terminal); const steps = [ { label: "CY Handling", value: t.breakdown["CY Handling"], type: "in" }, { label: "Storage", value: t.breakdown["Storage"], type: "in" }, { label: "Reefer", value: t.breakdown["Reefer"], type: "in" }, { label: "Other Behavior", value: t.breakdown["Other Behavior"], type: "in" }, { label: "Gross Total", value: t.gross, type: "total" }, { label: "Rebate Offset", value: -t.rebate, type: "out" }, { label: "Net Total", value: t.net, type: "total" }, ]; const maxVal = t.gross; const W = 880, H = 320, padL = 60, padR = 40, padT = 30, padB = 50; const chartW = W - padL - padR; const chartH = H - padT - padB; let runningY = 0; return (
Terminal:
{/* gridlines */} {[0, 0.25, 0.5, 0.75, 1].map(p => ( {fmt.usd((maxVal * p) / 1000)}k ))} {steps.map((s, i) => { const x = padL + (i * chartW) / steps.length + 12; const bw = chartW / steps.length - 24; let h, y, color, runningTop; if (s.type === "total") { h = (s.value / maxVal) * chartH; y = padT + chartH - h; color = "var(--ink)"; runningTop = y; } else if (s.type === "in") { h = (s.value / maxVal) * chartH; y = padT + chartH - runningY - h; color = "var(--teal)"; runningY += h; runningTop = y; } else { // out — rebate h = (Math.abs(s.value) / maxVal) * chartH; y = padT + chartH - runningY; color = "var(--green)"; runningY -= h; runningTop = y; } return ( {/* dashed connector to next */} {i < steps.length - 1 && s.type !== "total" && ( )} {s.label} {s.value < 0 ? "−" : ""}{fmt.usd(Math.abs(s.value) / 1000)}k ); })}
Gross / TEU
{fmt.usd(t.grossPerTEU)}
Rebate Yield
{t.rebateYield.toFixed(2)}%
Net / TEU
{fmt.usd(t.netPerTEU)}
Cost per Move
{fmt.usd(t.gross / (t.monthlyTEU * 0.62))}
); } // ============== BENCHMARKING ============== function Benchmarking() { const [costType, setCostType] = useState(""); const sample = TCO.slice(0, 8); return (
Benchmark on:
{sample.map((t, i) => { const trendData = [t.netPerTEU * 1.04, t.netPerTEU * 1.02, t.netPerTEU * 1.05, t.netPerTEU * 1.01, t.netPerTEU * 1.03, t.netPerTEU * 0.99, t.netPerTEU]; return ( ); })}
Terminal Gross / TEU Rebate Yield Net / TEU Rank (Region) MoM Change Trend (90d)
{t.terminalName}
{t.region}
{fmt.usd(t.grossPerTEU)} 0 ? "var(--green)" : "var(--ink-mute)" }}>{t.rebateYield > 0 ? `-${t.rebateYield.toFixed(1)}%` : "—"} {fmt.usd(t.netPerTEU)} #{i + 1} of {sample.length} {t.momChange > 0 ? "↑" : "↓"} {Math.abs(t.momChange).toFixed(1)}%
Negotiation insight: Qingdao Qianwan operates {fmt.usd(sample[sample.length-1].netPerTEU - sample[0].netPerTEU)} / TEU above Shanghai (Yangshan). Closing the gap to APA Top-3 average would save approximately {fmt.usd((sample[sample.length-1].netPerTEU - sample[0].netPerTEU) * sample[sample.length-1].monthlyTEU * 12)} per year at current volume pace.
); } // ============== TREND ============== function CostTrend() { // 12 months of Net Cost/TEU for top terminals + benchmark const months = VOLUME.months; const terminals = TCO.slice(0, 5); const series = terminals.map((t, ti) => ({ name: t.terminalName, color: ["#0e7f95", "#14a3bd", "#8a4d2e", "#1f6f5c", "#6b3f8a"][ti], data: months.map((m, mi) => t.netPerTEU * (1 + 0.04 * Math.sin((mi / 12) * Math.PI * 2 + ti) - 0.005 * mi)), })); // CCFI overlay (normalized) const ccfi = months.map((m, i) => 95 + 8 * Math.sin((i / 12) * Math.PI * 2 - 1) - 0.3 * i); const W = 880, H = 280, padL = 50, padR = 50, padT = 20, padB = 40; const chartW = W - padL - padR; const chartH = H - padT - padB; const allValues = series.flatMap(s => s.data); const min = Math.min(...allValues) * 0.95; const max = Math.max(...allValues) * 1.05; return (
{/* gridlines */} {[0, 0.25, 0.5, 0.75, 1].map(p => ( {fmt.usd(min + (max - min) * p)} ))} {/* x-axis labels */} {months.map((m, i) => ( {m.slice(5)} ))} {/* CCFI dashed line */} { const norm = (v - 80) / (110 - 80); // 0-1 return `${padL + (i * chartW) / (ccfi.length - 1)},${padT + chartH - norm * chartH}`; }).join(" ")} fill="none" stroke="var(--ink-mute)" strokeWidth="1.5" strokeDasharray="4 3" /> CCFI {/* terminal lines */} {series.map((s, si) => ( `${padL + (i * chartW) / (s.data.length - 1)},${padT + chartH - ((v - min) / (max - min)) * chartH}`).join(" ")} fill="none" stroke={s.color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" /> {s.data.map((v, i) => ( ))} ))}
{series.map((s, i) => (
{s.name}
))}
CCFI Index
{/* Variance Decomposition */}
); } function VarianceWaterfall() { const steps = [ { l: "FY24 Baseline", v: 2840000, type: "base" }, { l: "Volume Effect", v: 152000, type: "in" }, { l: "Rate Effect", v: 84000, type: "in" }, { l: "Rebate Effect", v: -210000, type: "out" }, { l: "FY25 YTD", v: 2866000, type: "total" }, ]; const maxVal = 3100000; const W = 840, H = 200, padL = 60, padR = 30, padT = 20, padB = 40; const chartW = W - padL - padR, chartH = H - padT - padB; let running = 0; return ( {[0, 0.5, 1].map(p => ( {fmt.usd((maxVal * p) / 1000000, 1)}M ))} {steps.map((s, i) => { const x = padL + (i * chartW) / steps.length + 16; const bw = chartW / steps.length - 32; let h, y, color; if (s.type === "base" || s.type === "total") { h = (s.v / maxVal) * chartH; y = padT + chartH - h; color = "var(--ink)"; running = s.v; } else if (s.type === "in") { h = (s.v / maxVal) * chartH; y = padT + chartH - (running / maxVal) * chartH - h; color = "var(--amber)"; running += s.v; } else { h = (Math.abs(s.v) / maxVal) * chartH; y = padT + chartH - (running / maxVal) * chartH; color = "var(--green)"; running += s.v; } return ( {s.l} {s.v > 0 && s.type !== "base" && s.type !== "total" ? "+" : ""}{fmt.usd(s.v / 1000, 0)}k ); })} ); } // ============== WHAT-IF SIMULATION ============== function WhatIfSimulation() { const [overrides, setOverrides] = useState({ rate: 0, volume: 0 }); const baseline = TCO[0]; const newGross = baseline.gross * (1 + overrides.rate / 100) * (1 + overrides.volume / 100); const newNet = newGross - baseline.rebate; const newPerTeu = newNet / (baseline.monthlyTEU * (1 + overrides.volume / 100)); const pnlImpact = (newNet - baseline.net) * 12; return (
0 ? "var(--red)" : overrides.rate < 0 ? "var(--green)" : null }}> {overrides.rate > 0 ? "+" : ""}{overrides.rate}%
setOverrides({ ...overrides, rate: parseFloat(e.target.value) })} style={{ width: "100%" }} />
−30%+30%
0 ? "var(--green)" : overrides.volume < 0 ? "var(--red)" : null }}> {overrides.volume > 0 ? "+" : ""}{overrides.volume}%
setOverrides({ ...overrides, volume: parseFloat(e.target.value) })} style={{ width: "100%" }} />
Baseline Net/TEU
{fmt.usd(baseline.netPerTEU)}
Scenario Net/TEU
baseline.netPerTEU ? "var(--amber)" : "var(--green)" }}>{fmt.usd(newPerTeu)}
baseline.netPerTEU ? "var(--red)" : "var(--green)" }}> {newPerTeu > baseline.netPerTEU ? "↑" : "↓"} {fmt.usd(Math.abs(newPerTeu - baseline.netPerTEU))}
Annual P&L impact
0 ? "var(--red)" : "var(--green)" }}> {pnlImpact > 0 ? "+" : ""}{fmt.usd(pnlImpact)}
Cost composition
{Object.entries(baseline.breakdown).map(([k, v]) => { const newV = v * (1 + overrides.rate / 100) * (1 + overrides.volume / 100); return ( ); })}
Component Baseline Scenario Δ
{k} {fmt.usd(v / 1000, 0)}k {fmt.usd(newV / 1000, 0)}k v ? "var(--red)" : newV < v ? "var(--green)" : "var(--ink-mute)" }}> {newV !== v ? (newV > v ? "+" : "") + fmt.usd((newV - v) / 1000, 0) + "k" : "—"}
Gross {fmt.usd(baseline.gross / 1000, 0)}k {fmt.usd(newGross / 1000, 0)}k baseline.gross ? "var(--red)" : "var(--green)" }}> {newGross !== baseline.gross ? (newGross > baseline.gross ? "+" : "") + fmt.usd((newGross - baseline.gross) / 1000, 0) + "k" : "—"}
Rebate offset -{fmt.usd(baseline.rebate / 1000, 0)}k -{fmt.usd(baseline.rebate / 1000, 0)}k
Net {fmt.usd(baseline.net / 1000, 0)}k {fmt.usd(newNet / 1000, 0)}k baseline.net ? "var(--red)" : "var(--green)" }}> {newNet !== baseline.net ? (newNet > baseline.net ? "+" : "") + fmt.usd((newNet - baseline.net) / 1000, 0) + "k" : "—"}
); } // ============== TERMINAL COST DETAIL ============== function TerminalCostDetail({ terminal, onClose }) { const months = VOLUME.months.slice(-6); return (
} >
0 ? "↑" : "↓"} ${Math.abs(terminal.momChange).toFixed(1)}%`} />
{months.map(m => )} {Object.entries(terminal.breakdown).map(([k, v]) => ( {months.map((m, i) => ( ))} ))} {months.map((m, i) => ( ))} {months.map((m, i) => ( ))} {months.map((m, i) => ( ))}
Cost Type{m.slice(5)}
{k}{fmt.usd(v / 1000 * (0.92 + i * 0.025), 0)}k
Gross{fmt.usd(terminal.gross / 1000 * (0.92 + i * 0.025), 0)}k
Rebate-{fmt.usd(terminal.rebate / 1000 * (0.85 + i * 0.04), 0)}k
Net{fmt.usd((terminal.gross * (0.92 + i * 0.025) - terminal.rebate * (0.85 + i * 0.04)) / 1000, 0)}k
Rate change impact alert: CY Handling for {terminal.terminal} was updated 2025-05-10. At current volume pace, full-year P&L impact is estimated at {fmt.usd(terminal.gross * 0.04 * 12)}. FBP notified via in-app + email.
); } Object.assign(window, { TCOAnalysis });