Catalyst / admin/Apex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Apex

public

Bid Management and Orchestration Tool with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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>
  );
}