| 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 | } |