● Zion Boggan
repos/Sightline/frontend/src/app/watchlists/page.tsx
zionboggan.com ↗
100 lines · tsx
History for this file →
1
"use client";
2
import { useEffect, useState } from "react";
3
import { api, Watchlist, WatchlistEntry } from "@/lib/api";
4
 
5
export default function Watchlists() {
6
  const [lists, setLists] = useState<Watchlist[]>([]);
7
  const [selected, setSelected] = useState<Watchlist | null>(null);
8
  const [entries, setEntries] = useState<WatchlistEntry[]>([]);
9
  const [err, setErr] = useState<string | null>(null);
10
  const [plate, setPlate] = useState("");
11
  const [partial, setPartial] = useState("");
12
  const [reason, setReason] = useState("");
13
  const [flash, setFlash] = useState<string | null>(null);
14
 
15
  function loadLists() {
16
    api.watchlists().then(setLists)
17
      .catch((e) => setErr(e.status === 401 ? "Sign in to manage watchlists." : String(e)));
18
  }
19
  useEffect(loadLists, []);
20
 
21
  function open(wl: Watchlist) {
22
    setSelected(wl); setFlash(null);
23
    api.watchlistEntries(wl.id).then(setEntries).catch(() => setEntries([]));
24
  }
25
 
26
  async function createList() {
27
    const name = prompt("New watchlist name (e.g. 'BOLO - Robbery Suspects')");
28
    if (!name) return;
29
    try { const wl = await api.createWatchlist(name); setLists((p) => [wl, ...p]); open(wl); }
30
    catch { setErr("Could not create (needs manage_watchlist permission)."); }
31
  }
32
 
33
  async function addEntry(e: React.FormEvent) {
34
    e.preventDefault();
35
    if (!selected || (!plate && !partial)) return;
36
    try {
37
      const r = await api.addWatchlistEntry(selected.id,
38
        { plate_norm: plate || undefined, partial_plate: partial || undefined, reason: reason || undefined });
39
      setFlash(`Entry added - ${r.backfill_hits} historical hit(s) backfilled.`);
40
      setPlate(""); setPartial(""); setReason("");
41
      open(selected);
42
    } catch { setErr("Could not add entry."); }
43
  }
44
 
45
  return (
46
    <div>
47
      <h1>Watchlist Manager</h1>
48
      {err && <div className="badge danger">{err}</div>}
49
      <div style={{ display: "grid", gridTemplateColumns: "280px 1fr", gap: 16, marginTop: 12 }}>
50
        <div className="card">
51
          <div className="row" style={{ justifyContent: "space-between", marginBottom: 8 }}>
52
            <strong>Lists</strong><button onClick={createList}>+ New</button>
53
          </div>
54
          {lists.map((wl) => (
55
            <div key={wl.id} onClick={() => open(wl)}
56
                 style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer",
57
                          background: selected?.id === wl.id ? "var(--panel-2)" : "transparent" }}>
58
              {wl.name} {wl.active && <span className="badge ok" style={{ fontSize: 10 }}>active</span>}
59
            </div>
60
          ))}
61
          {!lists.length && !err && <div className="muted">No watchlists.</div>}
62
        </div>
63
 
64
        <div>
65
          {!selected && <div className="card muted">Select a watchlist to view and add entries.</div>}
66
          {selected && (
67
            <>
68
              <h2 style={{ marginTop: 0 }}>{selected.name}</h2>
69
              <form onSubmit={addEntry} className="card" style={{ marginBottom: 12 }}>
70
                <div className="row">
71
                  <input placeholder="exact plate (ABC1234)" value={plate} onChange={(e) => setPlate(e.target.value.toUpperCase())} />
72
                  <input placeholder="partial (ABC)" value={partial} onChange={(e) => setPartial(e.target.value.toUpperCase())} />
73
                  <input placeholder="reason" value={reason} onChange={(e) => setReason(e.target.value)} />
74
                  <button className="primary" type="submit">Add</button>
75
                </div>
76
                {flash && <div className="badge ok" style={{ marginTop: 8 }}>{flash}</div>}
77
              </form>
78
              <div className="card">
79
                <table>
80
                  <thead><tr><th>Plate</th><th>Partial</th><th>Reason</th><th>Expires</th></tr></thead>
81
                  <tbody>
82
                    {entries.map((en) => (
83
                      <tr key={en.id}>
84
                        <td>{en.plate_norm || "-"}</td>
85
                        <td>{en.partial_plate || "-"}</td>
86
                        <td className="muted">{en.reason || "-"}</td>
87
                        <td className="muted">{en.expires_at ? new Date(en.expires_at).toLocaleDateString() : "never"}</td>
88
                      </tr>
89
                    ))}
90
                    {!entries.length && <tr><td colSpan={4} className="muted">No entries.</td></tr>}
91
                  </tbody>
92
                </table>
93
              </div>
94
            </>
95
          )}
96
        </div>
97
      </div>
98
    </div>
99
  );
100
}