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 / pages / Dashboard.tsx 8476 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
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "../lib/api";
import { useAuth } from "../lib/auth";

function AiStatusPill({ ai, checking, onRecheck }: { ai: any; checking: boolean; onRecheck: () => void }) {
  const map: Record<string, { bg: string; fg: string; dot: string; label: string }> = {
    connected: { bg: "#e5f5ec", fg: "#1e6b40", dot: "#1e8449", label: "AI connected & working" },
    disabled: { bg: "#f2f3f7", fg: "#6b7280", dot: "#9aa0ad", label: "AI not configured" },
    error: { bg: "#fbe5e2", fg: "#a5281b", dot: "#c0392b", label: "AI error" },
  };
  const s = ai ? (map[ai.status] || map.error) : { bg: "#f2f3f7", fg: "#6b7280", dot: "#c9ced9", label: "Checking AI…" };
  return (
    <span
      title={ai ? `${ai.detail}${ai.model ? " (" + ai.model + ")" : ""} — click to re-check` : "Checking…"}
      onClick={onRecheck}
      style={{
        display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer",
        background: s.bg, color: s.fg, border: `1px solid ${s.dot}33`,
        borderRadius: 999, padding: "6px 14px", fontSize: 13, fontWeight: 500,
      }}
    >
      <span style={{
        width: 9, height: 9, borderRadius: "50%", background: s.dot,
        boxShadow: ai?.status === "connected" ? `0 0 0 3px ${s.dot}33` : "none",
        animation: checking ? "fabPulse 1.2s ease-in-out infinite" : "none",
      }} />
      {checking ? "Checking AI…" : s.label}
    </span>
  );
}

interface Row {
  id: number;
  ref: string;
  title: string;
  client: string;
  state: string;
  submission_date: string;
  completeness: number;
  production_cost: { planned_cost: number; actual_cost: number };
}

function daysLeft(d: string) {
  const diff = Math.ceil((new Date(d).getTime() - Date.now()) / 86400000);
  return diff;
}

export default function Dashboard() {
  const { has } = useAuth();
  const nav = useNavigate();
  const [rows, setRows] = useState<Row[]>([]);
  const [frameworks, setFrameworks] = useState<{ id: number; name: string }[]>([]);
  const [show, setShow] = useState(false);
  const [err, setErr] = useState("");
  const [form, setForm] = useState({
    title: "",
    client: "",
    start_date: "",
    submission_date: "",
    win_themes: "",
    opportunity_value: 0,
    contract_duration_months: 12,
    web_link: "",
    frameworks: [] as number[],
  });

  const [ai, setAi] = useState<{ status: string; model: string; detail: string } | null>(null);
  const [aiChecking, setAiChecking] = useState(false);

  async function load() {
    setRows(await api.get("/bids/dashboard"));
  }
  async function loadAi(force = false) {
    setAiChecking(true);
    try { setAi(await api.get(`/ai/status${force ? "?force=true" : ""}`)); }
    catch { setAi({ status: "error", model: "", detail: "Status check failed" }); }
    setAiChecking(false);
  }
  useEffect(() => {
    load();
    loadAi();
    api.get("/bids/meta/frameworks").then(setFrameworks);
  }, []);

  async function create(e: React.FormEvent) {
    e.preventDefault();
    setErr("");
    try {
      const bid = await api.post("/bids", form);
      setShow(false);
      nav(`/bids/${bid.id}`);
    } catch (e: any) {
      setErr(e.message);
    }
  }

  const active = rows.filter((r) => !["submitted", "won", "lost", "withdrawn"].includes(r.state));
  const dueSoon = rows.filter((r) => daysLeft(r.submission_date) <= 14 && daysLeft(r.submission_date) >= 0);
  const won = rows.filter((r) => r.state === "won").length;
  const decided = rows.filter((r) => ["won", "lost"].includes(r.state)).length;

  return (
    <div>
      <div className="topbar">
        <div>
          <h1>Bid dashboard</h1>
          <span className="muted">All bids across the workspace</span>
        </div>
        <div className="row">
          <AiStatusPill ai={ai} checking={aiChecking} onRecheck={() => loadAi(true)} />
          {has("bid_manager") && <button onClick={() => setShow(true)}>+ New bid</button>}
        </div>
      </div>

      <div className="row wrap" style={{ marginBottom: 16 }}>
        <div className="card" style={{ flex: 1 }}><div className="muted">Active bids</div><h2>{active.length}</h2></div>
        <div className="card" style={{ flex: 1 }}><div className="muted">Due  14 days</div><h2>{dueSoon.length}</h2></div>
        <div className="card" style={{ flex: 1 }}><div className="muted">Win rate</div><h2>{decided ? Math.round((100 * won) / decided) : 0}%</h2></div>
      </div>

      <div className="card">
        <table>
          <thead>
            <tr>
              <th>Ref</th><th>Title</th><th>Client</th><th>State</th>
              <th>Submission</th><th>Complete</th><th>Bid cost</th><th></th>
            </tr>
          </thead>
          <tbody>
            {rows.map((r) => {
              const dl = daysLeft(r.submission_date);
              return (
                <tr key={r.id}>
                  <td>{r.ref}</td>
                  <td>{r.title}</td>
                  <td>{r.client}</td>
                  <td><span className="chip">{r.state}</span></td>
                  <td>
                    {r.submission_date}{" "}
                    {dl < 0 ? <span className="chip danger">overdue</span> :
                      dl <= 14 ? <span className="chip warn">{dl}d</span> : null}
                  </td>
                  <td>{r.completeness}%</td>
                  <td className="muted">£{r.production_cost.planned_cost.toLocaleString()} / £{r.production_cost.actual_cost.toLocaleString()}</td>
                  <td>
                    <button className="iconbtn" title="Open bid workspace" onClick={() => nav(`/bids/${r.id}`)}>👁</button>
                  </td>
                </tr>
              );
            })}
            {rows.length === 0 && <tr><td colSpan={8} className="muted">No bids yet.</td></tr>}
          </tbody>
        </table>
      </div>

      {show && (
        <div style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.4)", display: "flex", alignItems: "center", justifyContent: "center" }}>
          <form className="card" style={{ width: 480 }} onSubmit={create}>
            <h2>Create bid</h2>
            <label>Title</label>
            <input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} required />
            <label>Client</label>
            <input value={form.client} onChange={(e) => setForm({ ...form, client: e.target.value })} />
            <div className="grid2">
              <div><label>Start date</label><input type="date" value={form.start_date} onChange={(e) => setForm({ ...form, start_date: e.target.value })} required /></div>
              <div><label>Submission date</label><input type="date" value={form.submission_date} onChange={(e) => setForm({ ...form, submission_date: e.target.value })} required /></div>
            </div>
            <div className="grid2">
              <div><label>Opportunity value (£)</label><input type="number" value={form.opportunity_value} onChange={(e) => setForm({ ...form, opportunity_value: Number(e.target.value) })} /></div>
              <div><label>Contract duration (months)</label><input type="number" value={form.contract_duration_months} onChange={(e) => setForm({ ...form, contract_duration_months: Number(e.target.value) })} /></div>
            </div>
            <label>Opportunity web link</label>
            <input type="url" placeholder="https://portal.example.com/tender/123" value={form.web_link} onChange={(e) => setForm({ ...form, web_link: e.target.value })} />
            <label>Compliance frameworks</label>
            <select multiple value={form.frameworks.map(String)} onChange={(e) =>
              setForm({ ...form, frameworks: Array.from(e.target.selectedOptions).map((o) => Number(o.value)) })
            }>
              {frameworks.map((f) => <option key={f.id} value={f.id}>{f.name}</option>)}
            </select>
            <label>Win themes</label>
            <textarea rows={2} value={form.win_themes} onChange={(e) => setForm({ ...form, win_themes: e.target.value })} />
            {err && <div className="err">{err}</div>}
            <div className="row" style={{ marginTop: 14 }}>
              <button type="submit">Create</button>
              <button type="button" className="ghost" onClick={() => setShow(false)}>Cancel</button>
            </div>
          </form>
        </div>
      )}
    </div>
  );
}