"use client"; /** * One entry in an incident's call spine — UI_REDESIGN.md §5.2. Replaces * CallRow for this context (CallRow stays in use for the Archive table * until chunk 12). `stopNumber` is the same index the map's path stops use * (see IncidentPathLayer in MapView.tsx) — that shared index is the one * memorable thing per §2.4: "where was he when he said that" is answered * by looking, not cross-referencing timestamps. */ import { useEffect, useRef, useState } from "react"; import type { CallRecord } from "@/lib/types"; import { c2api } from "@/lib/c2api"; import { isKnownSeverity, SEVERITY_COLORS } from "@/lib/severity"; function fmtTime(iso: string): string { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function fmtClock(s: number): string { if (!Number.isFinite(s)) return "0:00"; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${r.toString().padStart(2, "0")}`; } function InlinePlayer({ callId }: { callId: string }) { const [url, setUrl] = useState(null); const [error, setError] = useState(false); const [loading, setLoading] = useState(false); const [playing, setPlaying] = useState(false); const [current, setCurrent] = useState(0); const [duration, setDuration] = useState(0); const audioRef = useRef(null); async function ensureUrl() { if (url || loading) return; setLoading(true); try { const full = await c2api.getCall(callId); setUrl(full.audio_url ?? null); if (!full.audio_url) setError(true); } catch { setError(true); } finally { setLoading(false); } } async function toggle() { if (!url) { await ensureUrl(); return; } const el = audioRef.current; if (!el) return; if (playing) el.pause(); else el.play(); } // Once the URL lands, autoplay (the click that fetched it was the play intent). useEffect(() => { if (url && audioRef.current) audioRef.current.play().catch(() => {}); }, [url]); if (error) { return Audio unavailable; } return (
e.stopPropagation()}> { const t = Number(e.target.value); if (audioRef.current) audioRef.current.currentTime = t; setCurrent(t); }} className="flex-1 h-1 accent-accent" disabled={!url} /> {fmtClock(current)} {url && (
); } function StopMarker({ stopNumber, color }: { stopNumber?: number; color: string }) { if (!stopNumber) { return ; } return ( {stopNumber} ); } export function CallSpineEntry({ call, stopNumber, isAdmin, }: { call: CallRecord; stopNumber?: number; isAdmin?: boolean; }) { const [expanded, setExpanded] = useState(false); const hasAudio = !!(call.audio_gcs_uri || call.audio_url); const transcript = call.transcript_corrected || call.transcript; const color = isKnownSeverity(call.severity) ? SEVERITY_COLORS[call.severity] : "var(--ink-muted)"; const substantive = !!transcript || (call.units && call.units.length > 0) || !!call.location_coords; const units = call.units ?? []; const clearedInCall = call.cleared_units ?? []; if (!substantive) { // Thin/routine calls collapse to a single line. return (
{fmtTime(call.started_at)} TG {call.talkgroup_name ?? call.talkgroup_id ?? "—"} · status only
); } return (
{fmtTime(call.started_at)} · TG {call.talkgroup_name ?? call.talkgroup_id ?? "—"} {call.node_id}
{hasAudio && (
)} {transcript && (

setExpanded((v) => !v)} > {transcript}

)}
{call.location && ( {call.location} )} {units.map((u) => ( {u} ))} {clearedInCall.map((u) => ( {u} ))}
{isAdmin && call.corr_path && (

corr: {call.corr_path}

)}
); }