// Screens: Access Requests, My Requests, Admin Approval Queue, Admin Users / Metadata / Audit / ROE / Routing const DRow = ({ k, v }) => ( {k} {v} ); function AccessRequests({ user, perms, onApprovalAction }) { const [isApprover, setIsApprover] = useState(false); const [isManager, setIsManager] = useState(false); const [isRoutingMember, setIsRoutingMember] = useState(false); const [tab, setTab] = useState(null); const [showForm, setShowForm] = useState(false); const [showBehalf, setShowBehalf] = useState(false); const [openReq, setOpenReq] = useState(null); const [queueReload, setQueueReload] = useState(0); const [myReload, setMyReload] = useState(0); React.useEffect(() => { apiFetch("/api/access-requests/is-approver") .then(res => { const approver = !!res.is_approver; setIsApprover(approver); setIsManager(!!res.is_manager); setIsRoutingMember(!!res.is_routing_member); setTab(approver ? "queue" : "my"); }) .catch(() => setTab("my")); }, []); const tabs = []; if (isApprover) tabs.push({ id: "queue", label: "Approval Queue" }); tabs.push({ id: "my", label: "My Requests" }); if (isManager || perms.admin) tabs.push({ id: "routing", label: "Routing Table" }); if (tab === null) return
Loading…
; return (
{isRoutingMember && ( )}
} /> {tab === "queue" && } {tab === "my" && } {tab === "routing" && } {showForm && setShowForm(false)} onSubmitted={() => { setTab("my"); setMyReload(n => n + 1); }} />} {showBehalf && setShowBehalf(false)} onSubmitted={() => { setTab("queue"); setQueueReload(n => n + 1); }} />} {openReq && setOpenReq(null)} onAction={() => { setOpenReq(null); setQueueReload(n => n + 1); onApprovalAction?.(); }} />} ); } function _slaDisplay(sla_deadline, status) { if (status !== "Pending") return ; if (!sla_deadline) return null; const days = Math.ceil((new Date(sla_deadline) - new Date()) / 86400000); if (days < 0) return ⚠ Overdue; if (days <= 1) return ⚠ {days}d; return {days}d; } function ApprovalQueue({ onOpen, reload, onApprovalAction }) { const [status, setStatus] = useState("Pending"); const [rows, setRows] = useState([]); const [counts, setCounts] = useState({}); const [loading, setLoading] = useState(true); const [acting, setActing] = useState(null); // req_id being actioned inline const load = () => { setLoading(true); Promise.all([ apiFetch("/api/admin/access-requests"), apiFetch("/api/admin/access-requests?status=Pending"), ]).then(([all, pending]) => { const c = {}; ["Pending","Approved","Rejected"].forEach(s => { c[s] = (Array.isArray(all) ? all : []).filter(r => r.status === s).length; }); setCounts(c); setRows(Array.isArray(all) ? all.filter(r => r.status === status) : []); setLoading(false); }).catch(() => setLoading(false)); }; React.useEffect(() => { load(); }, [reload]); React.useEffect(() => { apiFetch(`/api/admin/access-requests${status ? `?status=${status}` : ""}`) .then(r => setRows(Array.isArray(r) ? r : [])) .catch(() => {}); }, [status]); const quickAction = async (e, req_id, action) => { e.stopPropagation(); setActing(req_id); try { await apiFetch(`/api/admin/access-requests/${req_id}/${action}`, { method: "PUT", body: JSON.stringify({ comment: null }) }); load(); onApprovalAction?.(); } catch (err) { alert(err.message); } setActing(null); }; const parseRegions = str => { try { return JSON.parse(str).join(", "); } catch { return str || "—"; } }; return (
{["Pending","Approved","Rejected"].map(s => ( ))}
SLA breach
Within SLA
{loading ?
Loading…
: ( {rows.map(r => ( onOpen(r)} style={{ cursor: "pointer" }}> ))} {rows.length === 0 && ( )}
Request # Requester Requested Role Region(s) Submitted SLA Routed to
AR-{String(r.id).padStart(4, "0")}
{r.requester_name}
{r.requester_email}
{r.requested_role} {parseRegions(r.requested_regions)} {r.submitted_at?.slice(5, 16)} {_slaDisplay(r.sla_deadline, r.status)} {r.routed_to_email || "—"} e.stopPropagation()}> {r.status === "Pending" && (
)}
No {status.toLowerCase()} requests
)}
); } function MyRequests({ user, reload }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); React.useEffect(() => { setLoading(true); apiFetch("/api/access-requests/my") .then(r => { setRows(Array.isArray(r) ? r : []); setLoading(false); }) .catch(() => setLoading(false)); }, [reload]); const parseRegions = str => { try { return JSON.parse(str).join(", "); } catch { return str || "—"; } }; return (
{loading ?
Loading…
: ( {rows.map(r => ( ))} {rows.length === 0 && ( )}
Request # Role Region(s) Submitted Routed to Decided Status
AR-{String(r.id).padStart(4, "0")} {r.requested_role} {parseRegions(r.requested_regions)} {r.submitted_at?.slice(0, 16)} {r.routed_to_email || "—"} {r.reviewed_at?.slice(0, 16) || "—"}
No requests submitted yet
)}
); } function RoutingModal({ row, regions, onClose, onSaved }) { const isEdit = !!(row && row.id); const empty = { region_id: "", manager_email: "", delegate_email: "", delegate_valid_until: "" }; const [form, setForm] = React.useState(isEdit ? { region_id: row.region_id ?? "", manager_email: row.manager_email ?? "", delegate_email: row.delegate_email ?? "", delegate_valid_until: row.delegate_valid_until ? row.delegate_valid_until.slice(0, 10) : "", } : empty); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const inputStyle = { padding: "6px 8px", borderRadius: 6, border: "1px solid var(--border)", fontSize: 13, width: "100%", boxSizing: "border-box" }; const submit = async () => { if (!form.manager_email) { setError("Manager email is required."); return; } setSaving(true); setError(null); const body = { region_id: form.region_id ? parseInt(form.region_id) : null, manager_email: form.manager_email, delegate_email: form.delegate_email || null, delegate_valid_until: form.delegate_valid_until || null, }; try { if (isEdit) await apiFetch(`/api/admin/routing/${row.id}`, { method: "PUT", body: JSON.stringify(body) }); else await apiFetch("/api/admin/routing", { method: "POST", body: JSON.stringify(body) }); onSaved(); } catch (e) { setError(e.message); setSaving(false); } }; return (
{ if (e.target === e.currentTarget) onClose(); }}>
{isEdit ? "Edit" : "Add"} Routing Rule
{error &&
{error}
}
); } function RoutingTable() { const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(true); const [regions, setRegions] = React.useState([]); const [modal, setModal] = React.useState(null); // null | "new" | row object const load = () => { setLoading(true); apiFetch("/api/metadata/regions") .then(r => setRegions(Array.isArray(r) ? r : [])) .catch(() => {}); apiFetch("/api/admin/routing") .then(r => { setRows(Array.isArray(r) ? r : []); setLoading(false); }) .catch(() => setLoading(false)); }; React.useEffect(() => { load(); }, []); const handleDelete = async (id, email) => { if (!confirm(`Delete routing rule for "${email}"?`)) return; try { await apiFetch(`/api/admin/routing/${id}`, { method: "DELETE" }); load(); } catch (e) { alert(e.message); } }; const today = new Date().toISOString().slice(0, 10); const regionName = id => regions.find(r => r.id === Number(id))?.name || "—"; const delegateStatus = row => { if (!row.delegate_email) return null; if (!row.delegate_valid_until) return Permanent; const expired = row.delegate_valid_until.slice(0, 10) < today; return {expired ? "Expired" : `Until ${row.delegate_valid_until.slice(0, 10)}`}; }; const onSaved = () => { setModal(null); load(); }; return (
setModal("new")}> Add rule} > {loading ? (
Loading…
) : (
{rows.length === 0 && ( )} {rows.map(r => ( ))}
Region Manager Email Delegate Email Delegate Status
No routing rules configured.
{regionName(r.region_id)} {r.manager_email} {r.delegate_email || "—"} {delegateStatus(r)}
)}
{modal && ( setModal(null)} onSaved={onSaved} /> )}
); } function AccessRequestForm({ user, onClose, onSubmitted, onBehalf = false }) { const ROLE_DESCS = { rate_calculator: "Cost estimation tool only", read: "View rates, run calculator", edit: "Create / update rates", fbp: "Analytical, AI Q&A, export", procurement: "Contract upload, AI Q&A, export", }; const [form, setForm] = useState({ role: "read", regions: [], areas: [], justification: "" }); const [behalfEmail, setBehalfEmail] = useState(""); const [regionObjs, setRegionObjs] = useState([]); // { id, name } const [allAreas, setAllAreas] = useState([]); // { id, name, region_id } const [approvers, setApprovers] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [areaOpen, setAreaOpen] = useState(false); const [areaSearch, setAreaSearch] = useState(""); const areaRef = React.useRef(null); React.useEffect(() => { Promise.all([ apiFetch("/api/metadata/regions"), apiFetch("/api/metadata/areas"), ]).then(([r, a]) => { setRegionObjs(Array.isArray(r) ? r : []); setAllAreas(Array.isArray(a) ? a : []); }).catch(() => {}); }, []); React.useEffect(() => { const h = e => { if (areaRef.current && !areaRef.current.contains(e.target)) setAreaOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); React.useEffect(() => { if (form.regions.length === 0) { setApprovers([]); return; } apiFetch(`/api/access-requests/routing-preview?regions=${encodeURIComponent(form.regions.join(","))}`) .then(r => setApprovers(Array.isArray(r.approvers) ? r.approvers : [])) .catch(() => {}); }, [form.regions]); // Areas available for current selected regions const selectedRegionIds = regionObjs.filter(r => form.regions.includes(r.name)).map(r => r.id); const availableAreas = selectedRegionIds.length > 0 ? allAreas.filter(a => selectedRegionIds.includes(a.region_id)) : allAreas; const filteredAreas = areaSearch.trim() ? availableAreas.filter(a => a.name.toLowerCase().includes(areaSearch.trim().toLowerCase())) : availableAreas; const toggleRegion = r => { const has = form.regions.includes(r); const newRegions = has ? form.regions.filter(x => x !== r) : [...form.regions, r]; // When deselecting a region, remove areas that no longer belong to any selected region if (has) { const remaining = regionObjs.filter(ro => newRegions.includes(ro.name)).map(ro => ro.id); const validAreaNames = allAreas.filter(a => remaining.includes(a.region_id)).map(a => a.name); setForm(f => ({ ...f, regions: newRegions, areas: f.areas.filter(a => validAreaNames.includes(a)) })); } else { setForm(f => ({ ...f, regions: newRegions })); } }; const toggleArea = name => { setForm(f => ({ ...f, areas: f.areas.includes(name) ? f.areas.filter(x => x !== name) : [...f.areas, name], })); }; const submit = async () => { if (onBehalf && !behalfEmail.trim()) { setError("Target user email is required."); return; } if (onBehalf && !behalfEmail.includes("@")) { setError("Please enter a valid email address."); return; } if (!form.justification.trim()) { setError("Business justification is required."); return; } if (form.regions.length === 0) { setError("Please select at least one region."); return; } setSubmitting(true); setError(null); try { await apiFetch("/api/access-requests", { method: "POST", body: JSON.stringify({ requested_role: form.role, requested_regions: JSON.stringify(form.regions), requested_areas: JSON.stringify(form.areas), justification: form.justification, ...(onBehalf ? { on_behalf_of: behalfEmail.trim().toLowerCase() } : {}), }), }); onSubmitted?.(); onClose(); } catch (e) { setError(e.message); setSubmitting(false); } }; return ( } >
{onBehalf ? ( setBehalfEmail(e.target.value)} placeholder="target.user@maersk.com" autoFocus /> ) : ( )}
{Object.keys(ROLE_DESCS).map(r => ( ))}
{regionObjs.map(r => ( ))}
{/* Trigger button */} {/* Selected tags */} {form.areas.length > 0 && (
{form.areas.map(a => ( {a} ))}
)} {/* Dropdown panel */} {areaOpen && (
setAreaSearch(e.target.value)} placeholder="Search areas…" style={{ width: "100%", padding: "5px 8px", fontSize: 12, border: "1px solid var(--border)", borderRadius: 4, outline: "none", boxSizing: "border-box" }} />
{availableAreas.length === 0 && (
{form.regions.length === 0 ? "Select a region first" : "No areas for selected region(s)"}
)} {filteredAreas.length === 0 && availableAreas.length > 0 && (
No matches
)} {filteredAreas.map(a => ( ))}
)}