admin / Apex
publicBid Management and Orchestration Tool with AI Capability
Apex / Synapse-Apexv2 / frontend / src / components / WorkflowBar.tsx
4015 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | import { useState } from "react"; import { api } from "../lib/api"; import { useAuth } from "../lib/auth"; // Main lifecycle chain + terminal outcomes (mirrors backend TRANSITIONS). const CHAIN = ["draft", "planning", "in_progress", "in_review", "approved", "submitted"]; const OUTCOMES = ["won", "lost", "withdrawn"]; const TRANSITIONS: Record<string, string[]> = { draft: ["planning"], planning: ["in_progress", "draft"], in_progress: ["in_review", "planning"], in_review: ["approved", "in_progress"], approved: ["submitted", "in_review"], submitted: ["won", "lost", "withdrawn"], won: [], lost: [], withdrawn: [], }; const LABEL: Record<string, string> = { draft: "Draft", planning: "Planning", in_progress: "In progress", in_review: "In review", approved: "Approved", submitted: "Submitted", won: "Won", lost: "Lost", withdrawn: "Withdrawn", }; export default function WorkflowBar({ bid, reload }: { bid: any; reload: () => void }) { const { has } = useAuth(); const [err, setErr] = useState(""); const current = bid.state; const canMove = has("bid_manager", "approver"); const allowed = TRANSITIONS[current] || []; const curIdx = CHAIN.indexOf(current); const outcome = OUTCOMES.includes(current) ? current : null; async function move(state: string) { setErr(""); try { await api.post(`/bids/${bid.id}/state`, { state }); reload(); } catch (e: any) { setErr(e.message); } } function boxStyle(state: string, idx: number): React.CSSProperties { const isCurrent = state === current; const isPast = curIdx >= 0 && idx >= 0 && idx < curIdx; const clickable = canMove && allowed.includes(state); let bg = "#eef1fb", color = "#3a4a86", border = "#d5dbf3"; if (isCurrent) { bg = "#27357e"; color = "#fff"; border = "#27357e"; } else if (isPast) { bg = "#e5f5ec"; color = "#1e6b40"; border = "#bfe6cf"; } if (clickable) { border = "#f4b942"; } return { background: bg, color, border: `2px solid ${border}`, borderRadius: 12, padding: "8px 16px", fontSize: 14, fontWeight: isCurrent ? 600 : 400, cursor: clickable ? "pointer" : "default", whiteSpace: "nowrap", boxShadow: clickable ? "0 0 0 3px rgba(244,185,66,0.25)" : "none", }; } return ( <div className="card" style={{ padding: "14px 18px" }}> <div className="row wrap" style={{ gap: 6, alignItems: "center" }}> {CHAIN.map((state, idx) => ( <span key={state} className="row" style={{ gap: 6 }}> <div style={boxStyle(state, idx)} onClick={() => canMove && allowed.includes(state) && move(state)} title={canMove && allowed.includes(state) ? `Move to ${LABEL[state]}` : LABEL[state]}> {LABEL[state]} </div> {idx < CHAIN.length - 1 && <span style={{ color: "#aab", fontSize: 16 }}>›</span>} </span> ))} </div> {(current === "submitted" || outcome) && ( <div className="row wrap" style={{ gap: 8, marginTop: 10, alignItems: "center" }}> <span className="muted" style={{ fontSize: 13 }}>Outcome:</span> {OUTCOMES.map((o) => { const isCur = o === current; const clickable = canMove && allowed.includes(o); return ( <div key={o} onClick={() => clickable && move(o)} style={{ borderRadius: 12, padding: "6px 14px", fontSize: 13, cursor: clickable ? "pointer" : "default", background: isCur ? (o === "won" ? "#1e8449" : o === "lost" ? "#c0392b" : "#7a7f8c") : "#f2f3f7", color: isCur ? "#fff" : "#555", border: "2px solid " + (clickable ? "#f4b942" : "#e2e5ee"), }}>{LABEL[o]}</div> ); })} </div> )} {err && <div className="err">{err}</div>} {!canMove && <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>Your role can view but not advance the workflow.</div>} </div> ); } |