"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/components/AuthProvider"; import { useIncidents } from "@/lib/useIncidents"; import { useActiveCalls } from "@/lib/useCalls"; import { c2api } from "@/lib/c2api"; import type { IncidentRecord } from "@/lib/types"; import { PageHeader } from "@/components/ui/PageHeader"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; import { EmptyState, ErrorBanner } from "@/components/ui/EmptyState"; import { SkeletonCard } from "@/components/ui/Skeleton"; import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice"; import { DateRange, dayStart, dayEnd } from "@/components/ui/DateRange"; import { isKnownSeverity, severityRank } from "@/lib/severity"; import { SeverityMark, SeveritySpine } from "@/components/marks/SeverityMark"; import { TypeGlyph } from "@/components/marks/TypeGlyph"; type SeverityFilter = "all" | "minor" | "moderate" | "major"; const SEVERITY_FILTERS: { key: SeverityFilter; label: string }[] = [ { key: "all", label: "All" }, { key: "minor", label: "Minor+" }, { key: "moderate", label: "Moderate+" }, { key: "major", label: "Major only" }, ]; const FILTER_THRESHOLD: Record = { all: -1, minor: 1, moderate: 2, major: 3 }; type SortMode = "recent" | "severity"; type StatusFilter = "any" | "active" | "resolved"; const INCIDENT_TYPES = ["fire", "police", "ems", "accident", "other"]; // Firestore holds the paging; text/type/status filtering runs over the loaded // window, so "Load more" also widens what the search can find. const PAGE_SIZE = 100; function matchesSearch(inc: IncidentRecord, needle: string): boolean { if (!needle) return true; const hay = [ inc.title, inc.location, inc.summary, inc.type, ...(inc.units ?? []), ...(inc.vehicles ?? []), ...(inc.tags ?? []), ...(inc.location_mentions ?? []), ].filter(Boolean).join(" ").toLowerCase(); return hay.includes(needle); } // The Firestore client surfaces a missing composite index or an undeployed // ruleset as a raw multi-line string with a console URL in it — not something // to put in front of an operator. Collapse the known infra failures to a plain // line; pass anything else straight through so a real bug still shows. function friendlyIncidentsError(raw: string): string { if (/requires an index|PERMISSION_DENIED|Missing or insufficient permissions|failed-precondition/i.test(raw)) { return "Couldn't load incidents — the incidents database index isn't deployed on the server yet. This is a one-time backend deploy step (server-26 #13 / #51), not a problem with your data."; } return `Couldn't load incidents: ${raw}`; } function fmtTime(iso: string) { try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return iso; } } function dayBucket(iso: string): string { const d = new Date(iso); const now = new Date(); const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); const diffDays = Math.round((startOfDay(now) - startOfDay(d)) / 86_400_000); if (diffDays === 0) return "Today"; if (diffDays === 1) return "Yesterday"; return d.toLocaleDateString([], { weekday: "long", month: "short", day: "numeric" }); } function timeAgo(iso: string): string { const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400) return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; } // Same rail-card anatomy as Live (MapView's incident panel), at browse // density: severity spine, type glyph, severity chip, ON AIR pill, title, // location, units-on-scene chips, age + call count. UI_REDESIGN.md §5.1/§5.2 // — the point is that Live and Incidents read as the same object. function IncidentBrowseRow({ incident, isAdmin, onAir, onResolve, }: { incident: IncidentRecord; isAdmin: boolean; onAir: boolean; onResolve: (id: string) => void; }) { const router = useRouter(); const sev = isKnownSeverity(incident.severity) ? incident.severity : "routine"; const units = incident.units_active ?? incident.units ?? []; return (
router.push(`/incidents/${incident.incident_id}`)} >
{onAir && ( On air )} {incident.status}

{incident.title ?? "Incident"}

{incident.location &&

{incident.location}

}
{units.slice(0, 4).map((u) => ( {u} ))} {fmtTime(incident.started_at)} · {timeAgo(incident.started_at)} · {incident.call_ids.length} call{incident.call_ids.length !== 1 ? "s" : ""}
{isAdmin && incident.status === "active" && ( )}
); } function CreateModal({ onClose, onCreate }: { onClose: () => void; onCreate: (body: object) => Promise }) { const [title, setTitle] = useState(""); const [type, setType] = useState("other"); const [summary, setSummary] = useState(""); const [saving, setSaving] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSaving(true); try { await onCreate({ title, type, summary: summary || null, status: "active" }); onClose(); } finally { setSaving(false); } } return (

Create Incident

setTitle(e.target.value)} className="w-full bg-raised border border-line rounded-lg px-3 py-2 text-ink text-sm focus:outline-none focus:border-accent" />