import { useEffect, useState } from "react"; import { useApp } from "../state/store"; import { api, type WorkItem, type RuntimeTransaction } from "../lib/api"; import { Branch, Layers, Pulse } from "../components/icons"; interface DecoratedWorkItem extends WorkItem { age_label: string; } const HUB_LABELS: Record = { procurement: "Procurement", hr: "People", it: "IT", manufacturing: "Manufacturing", finance: "Finance", }; function ageLabel(item: WorkItem): string { const days = item.age_days ?? 0; if (days === 0) return "today"; if (days === 1) return "1 day"; if (days < 14) return `${days} days`; if (days < 60) return `${Math.floor(days / 7)} wk`; return `${Math.floor(days / 30)} mo`; } export default function Approvals() { const userEmail = useApp((s) => s.userEmail); const actor = useApp((s) => s.actor); const pushToast = useApp((s) => s.pushToast); const setScene = useApp((s) => s.setScene); const [items, setItems] = useState(null); const [hubFilter, setHubFilter] = useState("all"); const [activeKey, setActiveKey] = useState(null); const [tx, setTx] = useState(null); const [busy, setBusy] = useState(null); const tenantId = useApp((s) => s.tenantId); const loadQueue = async () => { if (!tenantId) { setItems([]); return; } try { const rows = await api.workItems(); const scoped = rows.filter((it) => (it as any).tenant_id === tenantId); setItems(scoped.map((it) => ({ ...it, age_label: ageLabel(it) }))); } catch (err: any) { pushToast("err", `Queue failed: ${err.message}`); } }; useEffect(() => { void loadQueue(); }, [tenantId]); useEffect(() => { if (!activeKey) return; let cancelled = false; api.transaction(activeKey).then((r) => { if (!cancelled) setTx(r); }); return () => { cancelled = true; }; }, [activeKey]); const hubs = Array.from(new Set((items || []).map((it) => it.hub).filter((h): h is string => !!h))); const visible = (items || []).filter((it) => hubFilter === "all" || it.hub === hubFilter).slice(0, 50); const counts = { running: 0, waiting: 0, blocked: 0 }; for (const it of items || []) { if (it.status === "running") counts.running++; else if (it.status === "waiting") counts.waiting++; else if (it.status === "blocked" || it.status === "stuck") counts.blocked++; } const handleAction = async (actionId: string) => { if (!tx?.transaction_id || !actor) return; setBusy(actionId); try { const res = await api.executeAction(tx.transaction_id, actionId, actor, {}); pushToast("ok", `Action recorded · ${res.status || "ok"}`); const fresh = await api.transaction(tx.transaction_id); setTx(fresh); await loadQueue(); } catch (err: any) { const msg = err.message || ""; if (/runtime-values|action_execution_failed/i.test(msg)) { pushToast("err", "Backend can't accept this action yet — runtime-values isn't reachable. The frontend sent the right shape."); } else { pushToast("err", `Action failed: ${msg.slice(0, 100)}`); } } finally { setBusy(null); } }; return (
Approvals · {userEmail.split("@")[0]}

Work that needs you

Every running process instance for your tenant. Pick a row to see the active step and the actions available right now. Acting on a row writes the decision back to EA2.

{counts.running} running {counts.waiting} waiting {counts.blocked > 0 && {counts.blocked} blocked}
{hubs.map((h) => ( ))}
QUEUE · {visible.length}
{items === null &&
Loading…
} {items !== null && visible.length === 0 && (
Nothing waiting in this hub. Open the to publish a new flow, or check back later.
)}
    {visible.map((it) => (
  • ))}
ACTIVE STEP
{!activeKey &&
Select a case on the left to see its current step and actions.
} {activeKey && tx && (
{tx.active_step?.display_name || "Active step"}
{tx.business_subject &&
Subject · {tx.business_subject}
} {(tx.active_step as any)?.dispatch_kind && (
Dispatch · {(tx.active_step as any).dispatch_kind}
)} {(tx.active_step as any)?.form_fields && (tx.active_step as any).form_fields.length > 0 && (
Form fields
    {(tx.active_step as any).form_fields.slice(0, 6).map((f: any) => (
  • {f.label || f.name} · {f.type}
  • ))}
)}
{(tx.available_actions || []).map((a: any) => ( ))} {(!tx.available_actions || tx.available_actions.length === 0) && (
No actions available on this step.
)}
)} {activeKey && !tx &&
Loading case…
}
); }