// §3.10 Volume Tracker — Upload, MVC Commitment, Analytics function VolumeTracker({ user, perms }) { const [tab, setTab] = useState("overview"); const [showUpload, setShowUpload] = useState(false); const tabs = [ { id: "overview", label: "Overview" }, { id: "commitment", label: "MVC Commitment", count: Object.keys(MVC).length }, { id: "rebatepace", label: "Rebate Pace" }, { id: "data", label: "Volume Records" }, ]; return (
{(perms.edit || perms.admin || user.role === "procurement") && ( )} } /> {tab === "overview" && } {tab === "commitment" && } {tab === "rebatepace" && } {tab === "data" && } {showUpload && setShowUpload(false)} />}
); } function VolumeKPIs() { const ytdAPA = Object.values(YTD_VOLUME).reduce((a, b) => a + b, 0); const monthlyAvg = ytdAPA / 5; const atRiskMVC = Object.entries(MVC).filter(([code, m]) => { const ytd = YTD_VOLUME[code]; const pace = (ytd / 5) * 12; return pace < m.annual * 0.95; }).length; return (
0 ? "down" : "up"} />
); } // ============== OVERVIEW ============== function VolumeOverview() { // Top 8 terminals by YTD volume const top = Object.entries(YTD_VOLUME) .map(([code, ytd]) => ({ code, ytd, name: TERMINAL_BY_CODE[code]?.name, region: TERMINAL_BY_CODE[code]?.region })) .sort((a, b) => b.ytd - a.ytd).slice(0, 10); const maxYtd = top[0].ytd; return (
{/* Monthly trend by region */}
} style={{ marginBottom: 16 }} >
{top.map((t, i) => (
0 ? "1px solid var(--line)" : 0, fontSize: 12 }}>
#{i + 1}
{t.name}
{t.region}
{fmt.num(t.ytd / 1000, 1)}k
{i % 3 === 0 ? "↓" : "↑"} {(2 + i * 0.7).toFixed(1)}%
))}
); } function MonthlyTrendChart() { // Aggregate volume by month & region const months = VOLUME.months; const regionTotals = REGIONS.map(reg => ({ region: reg, monthly: months.map(m => VOLUME.rows.filter(v => v.region === reg && v.period === m).reduce((s, v) => s + v.teu, 0)) })); const totalByMonth = months.map((m, i) => regionTotals.reduce((s, r) => s + r.monthly[i], 0)); const maxTotal = Math.max(...totalByMonth); const regionColors = { "North Asia": "#0e7f95", "Southeast Asia": "#14a3bd", "South Asia Sub-Continent": "#8a4d2e", "Oceania": "#1f6f5c", }; const W = 720, H = 200; return (
{/* gridlines */} {[0, 0.25, 0.5, 0.75, 1].map(p => ( ))} {/* y axis */} {[0, 0.25, 0.5, 0.75, 1].map(p => ( {fmt.num((maxTotal * p) / 1000, 0)}k ))} {/* stacked bars */} {months.map((m, mi) => { const x = 40 + (mi * (W - 50) / months.length); const bw = (W - 50) / months.length - 4; let yOff = 0; return ( {regionTotals.map((r, ri) => { const h = (r.monthly[mi] / maxTotal) * H; const y = H - h - yOff; yOff += h; return ; })} {m.slice(5)} {mi === 0 && {m.slice(0, 4)}} {mi === 11 && {m.slice(0, 4)}} ); })}
{Object.entries(regionColors).map(([r, c]) => (
{r}
))}
); } function SeasonalHeatmap() { const terminals = ["CNSHA", "CNNGB", "CNTAO", "SGSIN", "MYPKG", "INNSA", "AUSYD"]; const months = VOLUME.months; // Compute index (vs each terminal's 12-mo avg) return (
{months.map(m => )} {terminals.map(tc => { const tRows = VOLUME.rows.filter(v => v.terminal === tc); const avg = tRows.reduce((s, r) => s + r.teu, 0) / tRows.length; return ( {months.map(m => { const row = tRows.find(r => r.period === m); const idx = row ? row.teu / avg : 1; // Color: <1 cool, >1 warm let bg; if (idx < 0.9) bg = `oklch(70% 0.05 230 / ${0.4 + (0.9 - idx) * 4})`; else if (idx > 1.05) bg = `oklch(70% 0.13 35 / ${0.3 + (idx - 1) * 3})`; else bg = "var(--paper-2)"; return ( ); })} ); })}
Terminal{m.slice(5)}
{tc}
{idx > 0.85 && idx < 1.15 ? "" : Math.round(idx * 100)}
Index (100 = 12-mo avg)
low ← → high
); } // ============== MVC COMMITMENT TRACKER ============== function CommitmentTracker() { const rows = Object.entries(MVC).map(([code, m]) => { const ytd = YTD_VOLUME[code] || 0; const pace = (ytd / 5) * 12; const gap = pace - m.annual; const risk = pace < m.annual * 0.95 ? (pace < m.annual * 0.85 ? "Below Pace" : "At Risk") : "On Track"; const shortfall = gap < 0 ? Math.abs(gap) * m.penaltyRate : 0; return { code, name: TERMINAL_BY_CODE[code]?.name, region: TERMINAL_BY_CODE[code]?.region, mvc: m.annual, ytd, pace, gap, risk, shortfall, focal: m.focal }; }); const atRisk = rows.filter(r => r.risk !== "On Track"); return (
{atRisk.length > 0 && (
{atRisk.length} terminal(s) below 95% of commitment pace. Alert sent to FBP + Procurement on 2025-05-12 via Power Automate. Estimated shortfall exposure: {fmt.usd(atRisk.reduce((s, r) => s + r.shortfall, 0))} if pace continues.
)}
{rows.map(r => ( ))}
Terminal Contract MVC YTD Actual Pace vs MVC YTD Pace (Annualised) Gap to MVC Risk Shortfall Cost Focal
{r.name}
{r.region}
{fmt.num(r.mvc, 0)} {fmt.num(r.ytd, 0)} {fmt.num(r.pace, 0)} {r.gap > 0 ? "+" : ""}{fmt.num(r.gap, 0)} {r.risk === "On Track" && On Track} {r.risk === "At Risk" && ⚠ At Risk} {r.risk === "Below Pace" && Below Pace} 0 ? "var(--red)" : null }}> {r.shortfall > 0 ? fmt.usd(r.shortfall) : "—"} {r.focal}
); } // ============== REBATE PACE VIEW ============== function RebatePaceView() { return (
{REBATES.filter(r => r.currentTier >= 0 && r.tiers.length > 1).map((r, i) => { const ytd = YTD_VOLUME[r.terminal] || 18000; const lastTier = r.tiers[r.tiers.length - 1]; // Scale the chart so YTD always fits with headroom — never clip the marker const lastTierBound = lastTier.max || lastTier.min * 1.5; const max = Math.max(lastTierBound * 1.05, ytd * 1.08); const ytdPct = (ytd / max) * 100; return (
0 ? "1px solid var(--line)" : 0 }}>
{TERMINAL_BY_CODE[r.terminal]?.name}
{r.id}
YTD: {fmt.num(ytd, 0)} TEU
{/* Wrapper allows marker label to overflow above the bar */}
{/* Tier band bar (clipped) */}
{r.tiers.map((t, ti) => { const start = (t.min / max) * 100; const end = ((t.max || max) / max) * 100; const colors = ["#e7f3f6", "#cae8ee", "#9ad4de", "#5fbac9"]; return (
{t.rate}%
); })}
{/* YTD marker lives on the OUTER wrapper so the label can overflow */}
92 ? "translateX(-100%)" : ytdPct < 8 ? "translateX(0)" : "translateX(-50%)", background: "var(--ink)", color: "#fff", padding: "2px 6px", borderRadius: 3, fontSize: 9, fontFamily: "IBM Plex Mono", whiteSpace: "nowrap", lineHeight: 1.2, fontWeight: 600, }}> YTD · {fmt.num(ytd, 0)}
{r.tiers.map((t, ti) => (
{fmt.num(t.min, 0)}
))}
); })}
); } // ============== VOLUME RECORDS ============== function VolumeRecords() { const [page, setPage] = useState(1); const pageSize = 14; const sample = VOLUME.rows.filter(r => r.period >= "2024-12").slice((page - 1) * pageSize, page * pageSize); const total = VOLUME.rows.length; return (
{sample.map((v, i) => ( ))}
Period Terminal Equipment Cargo Move Type Actual TEU Actual Moves Source Uploaded
{v.period}
{v.terminalName}
{v.terminal}
{EQUIPMENT[i % EQUIPMENT.length]} {CARGO_TYPES[i % CARGO_TYPES.length]} {MOVE_TYPES[i % MOVE_TYPES.length]} {fmt.num(v.teu, 0)} {fmt.num(v.moves, 0)} Manual Upload 2025-05-08 · D. Park
Showing {(page - 1) * pageSize + 1}–{page * pageSize} of {total.toLocaleString()} records
); } // ============== UPLOAD WIZARD ============== function VolumeUploadWizard({ onClose }) { const [step, setStep] = useState(0); const sample = [ { terminal: "CNSHA", period: "2025-05", eq: "40HC", cargo: "Dry", move: "Export", teu: 5240, moves: 2620, status: "ok" }, { terminal: "CNSHA", period: "2025-05", eq: "20GP", cargo: "Dry", move: "Import", teu: 3120, moves: 3120, status: "ok" }, { terminal: "CNSHA", period: "2025-05", eq: "40RF", cargo: "Reefer", move: "Export", teu: 480, moves: 240, status: "ok" }, { terminal: "CNSHA", period: "2025-05", eq: "45HC", cargo: "Dry", move: "Transship", teu: 980, moves: 490, status: "ok" }, { terminal: "CNNGB", period: "2025-05", eq: "40HC", cargo: "Dry", move: "Export", teu: 4180, moves: 2090, status: "ok" }, { terminal: "CNNGB", period: "2025-05", eq: "40HC", cargo: "Dry", move: "Export", teu: 4180, moves: 2090, status: "dup" }, { terminal: "CNTAO", period: "2025-06", eq: "20GP", cargo: "Dry", move: "Import", teu: 2840, moves: 2840, status: "future" }, ]; return ( {step > 0 && }
{step < 2 && } {step === 2 && } } >
{step === 0 && (
Drop volume CSV / .xlsx file here
or browse — max 25MB
volume_2025-05_NA.xlsx
68 KB · 184 rows · 7 terminals
Template includes dropdown validation for Equipment Type, Cargo Type, Move Type. Upload scope restricted to your assigned Region/Area.
)} {step === 1 && (
{sample.filter(s => s.status === "ok").length} valid {sample.filter(s => s.status !== "ok").length} errors
Total file: 184 rows · 178 valid · 6 errors
{sample.map((s, i) => ( ))}
TerminalPeriodEquipmentCargoMoveTEUMovesResult
{s.terminal} {s.period} {s.eq} {s.cargo} {s.move} {fmt.num(s.teu, 0)} {fmt.num(s.moves, 0)} {s.status === "ok" && ✓ Valid} {s.status === "dup" && ✕ Duplicate combination} {s.status === "future" && ✕ Future period not allowed}
)} {step === 2 && (
Ready to commit 178 records
May 2025} /> 29,640} /> 18,260} />
On commit: System will automatically recalculate accruals for 4 active rebate contracts linked to these terminals. Affected contracts: RB-2025-001, RB-2025-002, RB-2025-003, RB-2025-005. MVC pace and TCO will refresh in real-time.
)} ); } Object.assign(window, { VolumeTracker });