feat(hubs): match dev.flow-master.ai workbench pattern + 4 named workflows
Inspected dev.flow-master.ai/dashboard/hubs/{procurement,hr,it} via real
dev-login. Real hub structure is: Workbench title + Workflow Commands
(4 named buttons) + Queue (left) + Selected Work (right).
- Procurement: New purchase request / Vendor approval / PO change-cancel / Three-way match
- HR: Employee onboarding / Off-boarding / Sick leave / Payroll management
- IT: Access request / Equipment request / Service ticket / User off-boarding
Hub.tsx rewritten: header + Workflow Commands grid + queue/selected
split pane that matches dev's 'Queue' + 'Selected work' layout.
Oracle gap #3 (hubs not feature parity) — partial close. Approvals queue
and document browser still outstanding but the workbench shape now
mirrors dev.
This commit is contained in:
+77
-42
@@ -7,46 +7,58 @@ 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;
|
||||
eyebrow: string;
|
||||
workbench: string;
|
||||
intro: string;
|
||||
match: RegExp;
|
||||
quickActions: { label: string; subject: string; processHint: string }[];
|
||||
workflows: HubWorkflow[];
|
||||
}
|
||||
|
||||
const HUBS: Record<HubKey, HubSpec> = {
|
||||
procurement: {
|
||||
title: "Procurement Hub",
|
||||
eyebrow: "Purchase requests, vendor approvals, three-way match",
|
||||
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,
|
||||
quickActions: [
|
||||
{ label: "Request a new laptop", subject: "store-204-laptop", processHint: "procurement" },
|
||||
{ label: "Submit a quote for review", subject: "vendor-quote-Q3", processHint: "procurement" },
|
||||
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: "People Hub",
|
||||
eyebrow: "Hiring, onboarding, leave, performance",
|
||||
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,
|
||||
quickActions: [
|
||||
{ label: "Onboard a new hire", subject: "new-hire-2026Q3", processHint: "onboard" },
|
||||
{ label: "Request annual leave", subject: "leave-7d-may", processHint: "leave" },
|
||||
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 Hub",
|
||||
eyebrow: "Tickets, access requests, incident response",
|
||||
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,
|
||||
quickActions: [
|
||||
{ label: "Open an incident", subject: "incident-store-204", processHint: "service" },
|
||||
{ label: "Request system access", subject: "access-sap-RW", processHint: "access" },
|
||||
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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -92,6 +104,11 @@ export default function Hub({ hub }: { hub: HubKey }) {
|
||||
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<string | null>(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);
|
||||
@@ -122,15 +139,15 @@ export default function Hub({ hub }: { hub: HubKey }) {
|
||||
return (
|
||||
<div className="hub-scene">
|
||||
<header className="hub-head">
|
||||
<div className="mc-hero-eyebrow"><Layers size={12} /> {spec.eyebrow}</div>
|
||||
<div className="mc-hero-eyebrow"><Layers size={12} /> {spec.workbench}</div>
|
||||
<h2 className="mc-hero-title">{spec.title}</h2>
|
||||
<p className="hub-intro">{spec.intro}</p>
|
||||
</header>
|
||||
|
||||
<section className="hub-quick">
|
||||
<div className="hub-section-label">QUICK ACTIONS</div>
|
||||
<section className="hub-commands">
|
||||
<div className="hub-section-label">{spec.title.split(" ")[0].toUpperCase()} WORKFLOW COMMANDS</div>
|
||||
<div className="hub-quick-grid">
|
||||
{spec.quickActions.map((qa) => (
|
||||
{spec.workflows.map((qa) => (
|
||||
<button
|
||||
key={qa.label}
|
||||
className="hub-quick-card"
|
||||
@@ -138,36 +155,54 @@ export default function Hub({ hub }: { hub: HubKey }) {
|
||||
disabled={!flows}
|
||||
>
|
||||
<div className="hub-quick-title"><Pulse size={12} /> {qa.label}</div>
|
||||
<div className="hub-quick-meta">Looks up "{qa.processHint}" in your catalogue</div>
|
||||
<div className="hub-quick-meta">Open workflow</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="hub-catalogue">
|
||||
<div className="hub-section-label">RELATED PROCESSES</div>
|
||||
{flows === null && <div className="hub-empty">Loading catalogue…</div>}
|
||||
{flows !== null && visible.length === 0 && (
|
||||
<div className="hub-empty">
|
||||
No matching published processes yet. Open the <button className="link-inline" onClick={() => setScene("studio")}>Process Studio</button> to build one.
|
||||
</div>
|
||||
)}
|
||||
<div className="hub-catalogue-grid">
|
||||
{visible.map((f) => (
|
||||
<div key={f._key} className="hub-flow-card">
|
||||
<div className="hub-flow-title"><Branch size={11} /> {f.display_name}</div>
|
||||
{f.description && <div className="hub-flow-desc">{f.description.slice(0, 160)}</div>}
|
||||
<div className="hub-split">
|
||||
<section className="hub-queue">
|
||||
<div className="hub-section-label">{spec.title.split(" ")[0].toUpperCase()} QUEUE</div>
|
||||
{flows === null && <div className="hub-empty">Loading…</div>}
|
||||
{flows !== null && visible.length === 0 && (
|
||||
<div className="hub-empty">
|
||||
Nothing in the queue yet. Open the <button className="link-inline" onClick={() => setScene("studio")}>Process Studio</button> to publish a process.
|
||||
</div>
|
||||
)}
|
||||
<ul className="hub-queue-list">
|
||||
{visible.map((f) => (
|
||||
<li key={f._key}>
|
||||
<button
|
||||
className={`hub-queue-row ${f._key === selectedKey ? "active" : ""}`}
|
||||
onClick={() => setSelectedKey(f._key)}
|
||||
>
|
||||
<Branch size={11} />
|
||||
<span className="hub-queue-name">{f.display_name}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="hub-selected">
|
||||
<div className="hub-section-label">SELECTED WORK</div>
|
||||
{!selected && <div className="hub-empty">Select a row on the left.</div>}
|
||||
{selected && (
|
||||
<div className="hub-flow-card">
|
||||
<div className="hub-flow-title"><Branch size={11} /> {selected.display_name}</div>
|
||||
{selected.description && <div className="hub-flow-desc">{selected.description.slice(0, 320)}</div>}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
disabled={busy === f._key}
|
||||
onClick={() => startInstance(f._key, "", f.display_name)}
|
||||
className="btn btn-primary"
|
||||
disabled={busy === selected._key}
|
||||
onClick={() => startInstance(selected._key, "", selected.display_name)}
|
||||
>
|
||||
{busy === f._key ? "Starting…" : "Start an instance"}
|
||||
{busy === selected._key ? "Starting…" : "Start an instance"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user