// Main app — shell, nav, persona switching, page routing const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "persona": "fbp", "scopeFilter": "All in scope", "density": "comfortable", "showAuditTrail": true }/*EDITMODE-END*/; // Nav groups const NAV_GROUPS = [ { label: "Rates", items: [ { id: "rates", label: "Rate Repository", icon: "table" }, { id: "health", label: "Rate Health", icon: "shield" }, { id: "roe", label: "ROE Table", icon: "globe" }, { id: "calc", label: "Rate Calculator", icon: "calc" }, { id: "ai", label: "AI Assistant", icon: "sparkle" }, { id: "export",label: "Export", icon: "export" }, ], }, { label: "Contracts & Finance", items: [ { id: "rebate", label: "Rebate Monitor", icon: "key" }, { id: "volume", label: "Volume Tracker", icon: "table" }, { id: "tco", label: "TCO Analysis", icon: "bolt" }, ], }, { label: "Access", items: [ { id: "access", label: "Access Requests", icon: "key" }, ], }, { label: "Administration", items: [ { id: "admin", label: "Admin Console", icon: "settings", adminOnly: true }, ], }, { label: "Resources", items: [ { id: "docs", label: "Documentation", icon: "book" }, ], }, ]; function canSee(page, perms) { if (perms.accessOnly) return page === "access" || page === "docs"; switch (page) { case "rates": return perms.viewRates; case "health": return perms.rateHealth; case "roe": return perms.roeTable; case "calc": return true; case "ai": return perms.aiAssistant; case "export": return perms.export; case "rebate": return perms.rebateMonitor; case "volume": return perms.volumeTracker; case "tco": return perms.tcoAnalysis; case "access": return true; case "admin": return perms.admin; case "docs": return true; } return true; } function App() { const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); // Use real SSO user when available; fall back to persona simulator for local dev const [ssoUser, setSsoUser] = useState(window.OCMS_USER || null); const [permsMap, setPermsMap] = useState(window.OCMS_PERMS || PERMS); const [tweaksPanelOpen, setTweaksPanelOpen] = useState(false); useEffect(() => { const handler = (e) => { setSsoUser(e.detail); if (e.detail?.role && PERSONAS[e.detail.role]) { setTweak("persona", e.detail.role); } }; window.addEventListener('ocms:user-ready', handler); return () => window.removeEventListener('ocms:user-ready', handler); }, []); useEffect(() => { const handler = (e) => setPermsMap(e.detail); window.addEventListener('ocms:perms-ready', handler); return () => window.removeEventListener('ocms:perms-ready', handler); }, []); useEffect(() => { const onMsg = (e) => { if (e.data?.type === '__edit_mode_dismissed') { setTweaksPanelOpen(false); if (ssoUser?.role && PERSONAS[ssoUser.role]) { setTweak("persona", ssoUser.role); } // Refresh permissions from API in case admin changed them if (window.OCMS_PERMS) setPermsMap({ ...window.OCMS_PERMS }); } }; window.addEventListener('message', onMsg); return () => window.removeEventListener('message', onMsg); }, [ssoUser]); const _devEnv = ['localhost', '127.0.0.1', 'ocms-cdt.maersk.io'].includes(window.location.hostname); const _isAdmin = ssoUser ? (ssoUser.role === 'admin' || (window.location.hostname === 'localhost' && ssoUser.roles.length === 0)) : true; const showTweaks = _devEnv && _isAdmin; // Persona switcher only overrides when TweaksPanel is actually open const user = (showTweaks && tweaksPanelOpen) ? (PERSONAS[tweaks.persona] || PERSONAS.fbp) : (ssoUser || PERSONAS.fbp); const perms = user.role === 'unknown' ? PERMS.unknown : (permsMap[user.role] || permsMap.read || PERMS.read); const defaultPage = NAV_GROUPS.flatMap(g => g.items).map(i => i.id).find(p => canSee(p, perms)); const [page, setPage] = useState(defaultPage || "calc"); useEffect(() => { if (!canSee(page, perms)) setPage(defaultPage || "calc"); }, [tweaks.persona]); const [showWelcome, setShowWelcome] = useState(true); const dismissWelcome = () => setShowWelcome(false); const [openRate, setOpenRate] = useState(null); const [showAdd, setShowAdd] = useState(false); const [showAddPrefill, setShowAddPrefill] = useState(null); const [showImport, setShowImport] = useState(false); const [showAI, setShowAI] = useState(false); const [toast, setToast] = useState(""); const [rateRefreshKey, setRateRefreshKey] = useState(0); const [rateNavCount, setRateNavCount] = useState(null); const [accessPendingCount, setAccessPendingCount] = useState(0); const [accessCountKey, setAccessCountKey] = useState(0); useEffect(() => { apiFetch("/api/rates/count").then(r => { if (r?.total != null) setRateNavCount(r.total); }).catch(() => {}); }, [rateRefreshKey]); // Fetch pending access request count — returns 0 for non-approvers without 403 useEffect(() => { if (!ssoUser) return; apiFetch("/api/access-requests/pending-count") .then(r => setAccessPendingCount(r?.count ?? 0)) .catch(() => setAccessPendingCount(0)); }, [ssoUser, accessCountKey]); const fmtCount = n => n == null ? "" : n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, "") + "k" : String(n); const allNavItems = NAV_GROUPS.flatMap(g => g.items); const pageMeta = allNavItems.find(p => p.id === page); const groupMeta = NAV_GROUPS.find(g => g.items.some(i => i.id === page)); return (
{/* SIDEBAR */} {/* TOP BAR */}
APA Ocean Finance / {groupMeta && <>{groupMeta.label}/} {pageMeta?.label || "—"}
{ const t = e.currentTarget.querySelector(".scope-tip"); if (t) t.style.display = "block"; }} onMouseLeave={e => { const t = e.currentTarget.querySelector(".scope-tip"); if (t) t.style.display = "none"; }}> Scope: {user.regions.length > 1 ? `${user.regions.length} regions` : (user.regions[0] || "—")} {(user.regions.length > 0 && user.regions[0] !== "—") || user.areas?.length > 0 ? (
Regions
{user.regions.map(r =>
· {r}
)} {user.areas && user.areas.length > 0 && ( <>
Areas
{user.areas.map(a =>
· {a}
)} )}
) : null}
{perms.aiAssistant && ( )}
{ssoUser ? ssoUser.name : user.name}
{user.role}
{/* MAIN */}
{page === "rates" && perms.viewRates && ( setShowAdd(true)} onAddPrefill={(prefill) => { setShowAddPrefill(prefill); setShowAdd(true); }} onImport={() => setShowImport(true)} onOpenAI={() => setShowAI(true)} onToast={setToast} refreshKey={rateRefreshKey} /> )} {page === "rates" && !perms.viewRates && setPage("calc")} />} {page === "health" && perms.rateHealth && ( { setShowAddPrefill(prefill); setShowAdd(true); }} /> )} {page === "health" && !perms.rateHealth && setPage("calc")} />} {page === "roe" && perms.roeTable && } {page === "roe" && !perms.roeTable && setPage("calc")} />} {page === "calc" && } {page === "ai" && perms.aiAssistant && } {page === "ai" && !perms.aiAssistant && setPage("calc")} />} {page === "export" && perms.export && } {page === "export" && !perms.export && setPage("calc")} />} {page === "rebate" && canSee("rebate", perms) && } {page === "rebate" && !canSee("rebate", perms) && setPage("calc")} />} {page === "volume" && canSee("volume", perms) && } {page === "volume" && !canSee("volume", perms) && setPage("calc")} />} {page === "tco" && canSee("tco", perms) && } {page === "tco" && !canSee("tco", perms) && setPage("calc")} />} {page === "access" && setAccessCountKey(k => k + 1)} />} {page === "admin" && perms.admin && } {page === "docs" && }
{openRate && setOpenRate(null)} perms={perms} onEdit={rate => { setOpenRate(null); setShowAddPrefill(rate); setShowAdd(true); }} onDuplicate={rate => { setOpenRate(null); setShowAddPrefill({ ...rate, id: undefined, status: "Draft", created_at: undefined, modified_at: undefined }); setShowAdd(true); }} />} {showAdd && { setShowAdd(false); setShowAddPrefill(null); }} onSave={msg => { setShowAdd(false); setShowAddPrefill(null); setToast(msg); setRateRefreshKey(k => k + 1); }} />} {showImport && setShowImport(false)} onDone={msg => { setShowImport(false); setToast(msg); setRateRefreshKey(k => k + 1); }} />} {showAI && setShowAI(false)} />} setToast("")} /> {showWelcome && } {/* TWEAKS PANEL — local/dev env + admin role only */} {showTweaks && setTweak("persona", v)} options={[ { value: "rate_calculator", label: "Calc only" }, { value: "read", label: "Read" }, { value: "edit", label: "Rate Focal (edit)" }, { value: "fbp", label: "FBP" }, { value: "procurement", label: "Procurement" }, { value: "admin", label: "Admin" }, ]} />
rate_calculator · Calculator only · rates hidden
read · browse rates + Health Dashboard
edit · Rate Focal — create / import rates + Volume upload
fbp · adds AI, Export, Rebate, TCO
procurement · adds contract upload + Rebate settlement
admin · full system + Admin console
setTweak("showAuditTrail", v)} />
}
); } function NoAccess({ role, feature, canDo, onGoCalc }) { return (
{feature} is not available for your role
Your role {role} does not include access to {feature}. You can use the {canDo} for cost estimation, or submit a new access request.
); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();