// §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 (
Recalculate
Monthly TCO report
>
}
/>
{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
Rank
Terminal
Region
Monthly TEU
Gross / TEU
Rebate Yield
Net / TEU
vs APA avg
MoM
{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" }}>
#{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)}%
e.stopPropagation()}>
);
})}
);
}
// ============== 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:
setTerminal(e.target.value)}>
{TCO.map(x => {x.terminalName} )}
Export
{/* 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:
setCostType(e.target.value)}>
Total Net Cost / TEU
{COST_TYPES.map(c => {c.name} only )}
Open What-if
Export
Terminal
Gross / TEU
Rebate Yield
Net / TEU
Rank (Region)
MoM Change
Trend (90d)
{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 (
{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 (
{baseline.terminalName}
All cost types {COST_TYPES.map(c => {c.name} )}
Rate change
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%
Save scenario
setOverrides({ rate: 0, volume: 0 })}>Reset to baseline
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
Component
Baseline
Scenario
Δ
{Object.entries(baseline.breakdown).map(([k, v]) => {
const newV = v * (1 + overrides.rate / 100) * (1 + overrides.volume / 100);
return (
{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 (
What-if
Export
Close
>
}
>
0 ? "↑" : "↓"} ${Math.abs(terminal.momChange).toFixed(1)}%`} />
Cost Type
{months.map(m => {m.slice(5)} )}
{Object.entries(terminal.breakdown).map(([k, v]) => (
{k}
{months.map((m, i) => (
{fmt.usd(v / 1000 * (0.92 + i * 0.025), 0)}k
))}
))}
Gross
{months.map((m, i) => (
{fmt.usd(terminal.gross / 1000 * (0.92 + i * 0.025), 0)}k
))}
Rebate
{months.map((m, i) => (
-{fmt.usd(terminal.rebate / 1000 * (0.85 + i * 0.04), 0)}k
))}
Net
{months.map((m, i) => (
{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 });