import { useEffect, useState } from "react"; import { useApp } from "../state/store"; import { api } from "../lib/api"; import { wizardApi } from "../lib/wizardApi"; import { curatedPublishedFlows } from "../lib/flowCuration"; import { Branch, Layers, Pulse } from "../components/icons"; export type HubKey = "procurement" | "hr" | "it"; interface HubWorkflow { label: string; subject: string; processHint: string; } interface HubSpec { title: string; workbench: string; intro: string; match: RegExp; workflows: HubWorkflow[]; } const HUBS: Record = { procurement: { title: "Procurement workbench", workbench: "Purchase requests, vendor approvals, three-way match", intro: "Everything a store manager needs to buy something — from a laptop to a forklift — and watch it move through the approval chain.", match: /procure|purchase|vendor|\bpo\b|\bp2p\b|requisition|payment|invoice/i, workflows: [ { label: "New purchase request", subject: "store-204-laptop", processHint: "procurement" }, { label: "Vendor approval", subject: "vendor-quote-Q3", processHint: "vendor" }, { label: "PO change / cancel", subject: "po-change-may", processHint: "po" }, { label: "Three-way match", subject: "match-batch-mar", processHint: "match" }, ], }, hr: { title: "HR workbench", workbench: "Hiring, onboarding, leave, payroll", intro: "All people movements run here. Start an onboarding for a new hire, file a leave request, or kick off a review cycle.", match: /onboard|offboard|leave|\bhr\b|hiring|payroll|employee|people operations/i, workflows: [ { label: "Employee onboarding", subject: "new-hire-2026Q3", processHint: "onboard" }, { label: "Off-boarding", subject: "leaver-2026Q3", processHint: "offboard" }, { label: "Sick leave", subject: "sick-leave-may", processHint: "leave" }, { label: "Payroll management", subject: "payroll-Q3-2026", processHint: "payroll" }, ], }, it: { title: "IT workbench", workbench: "Access requests, equipment, service tickets, off-boarding", intro: "When something breaks, request access, or you need a new account — file it here and route it to the right on-call.", match: /\bit\b|ticket|incident|service operations|access request|sap access|laptop request|hardware request|software request|password reset|account provisioning/i, workflows: [ { label: "Access request", subject: "access-sap-RW", processHint: "access" }, { label: "Equipment request", subject: "store-204-laptop", processHint: "equipment" }, { label: "Service ticket", subject: "incident-store-204", processHint: "service" }, { label: "User off-boarding", subject: "user-leaver-jun", processHint: "offboard" }, ], }, }; interface PublishedFlow { _key: string; display_name: string; description?: string; name?: string; } async function loadPublishedFlows(): Promise { const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, }); if (!res.ok) throw new Error(`processes ${res.status}`); const body = await res.json(); return curatedPublishedFlows((body?.items || []) as any[]).map((it) => ({ _key: it._key, display_name: it.display_name!, description: it.description, name: it.name, })); } export default function Hub({ hub }: { hub: HubKey }) { const spec = HUBS[hub]; const pushToast = useApp((s) => s.pushToast); const setScene = useApp((s) => s.setScene); const [flows, setFlows] = useState(null); const [busy, setBusy] = useState(null); useEffect(() => { let cancelled = false; loadPublishedFlows() .then((rows) => !cancelled && setFlows(rows)) .catch((err) => !cancelled && pushToast("err", `Catalogue failed: ${err.message}`)); return () => { cancelled = true; }; }, [hub]); const matching = (flows || []).filter((f) => spec.match.test(`${f.display_name} ${f.description || ""} ${f.name || ""}`)).slice(0, 8); const fallback = (flows || []).slice(0, 6); const visible = matching.length > 0 ? matching : fallback; const [selectedKey, setSelectedKey] = useState(null); useEffect(() => { if (!selectedKey && visible.length > 0) setSelectedKey(visible[0]._key); }, [visible, selectedKey]); const selected = visible.find((f) => f._key === selectedKey) || null; const startInstance = async (flowKey: string, subject: string, label: string) => { setBusy(flowKey); try { const res = await wizardApi.startInstance(flowKey, subject); void res; pushToast("ok", `Started ${label}${subject ? ` for ${subject}` : ""}.`); setScene("mission"); } catch (err: any) { pushToast("err", `Start failed: ${err.message}`); } finally { setBusy(null); } }; const tryQuickAction = async (label: string, subject: string, processHint: string) => { if (!flows) return; const match = flows.find((f) => new RegExp(processHint, "i").test(`${f.display_name} ${f.description || ""} ${f.name || ""}`) ); if (!match) { pushToast("err", `No published process matching "${processHint}". Build one in Studio.`); return; } await startInstance(match._key, subject, label); }; return (
{spec.workbench}

{spec.title}

{spec.intro}

{spec.title.split(" ")[0].toUpperCase()} WORKFLOW COMMANDS
{spec.workflows.map((qa) => ( ))}
{spec.title.split(" ")[0].toUpperCase()} QUEUE
{flows === null &&
Loading…
} {flows !== null && visible.length === 0 && (
Nothing in the queue yet. Open the to publish a process.
)}
    {visible.map((f) => (
  • ))}
SELECTED WORK
{!selected &&
Select a row on the left.
} {selected && (
{selected.display_name}
{selected.description &&
{selected.description.slice(0, 320)}
}
)}
); }