"use client"; import { useEffect, useState } from "react"; import type { CallRecord } from "@/lib/types"; import { c2api } from "@/lib/c2api"; interface Props { call: CallRecord; systemName?: string; isAdmin?: boolean; } function duration(started: string, ended: string | null): string { if (!ended) return "active"; const ms = new Date(ended).getTime() - new Date(started).getTime(); const s = Math.floor(ms / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; } const TAG_COLORS: Record = { fire: "bg-red-900 text-red-300", police: "bg-blue-900 text-blue-300", ems: "bg-yellow-900 text-yellow-300", accident: "bg-orange-900 text-orange-300", }; export function CallRow({ call, systemName, isAdmin }: Props) { const [expanded, setExpanded] = useState(false); const [showOriginal, setShowOriginal] = useState(false); const [editing, setEditing] = useState(false); const [editText, setEditText] = useState(""); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // Resolve incident links: prefer new list, fall back to legacy single field. const incidentIds: string[] = (call.incident_ids?.length ?? 0) > 0 ? call.incident_ids : call.incident_id ? [call.incident_id] : []; const isActive = call.status === "active"; // Rows come straight from Firestore (lib/useCalls.ts), and the doc only holds // the private gs:// object location — never a playable URL. Presence of audio // is known from the doc; the actual link is minted by the API on expand. // audio_url is the legacy field: older docs stored an (unplayable) gs:// URI // there, so it still signals "this call has a recording". const hasAudio = !!(call.audio_gcs_uri || call.audio_url); const hasDetails = call.transcript || call.transcript_corrected || (call.tags && call.tags.length > 0) || incidentIds.length > 0 || hasAudio; const displayTranscript = (!showOriginal && call.transcript_corrected) ? call.transcript_corrected : call.transcript; const hasBoth = !!(call.transcript && call.transcript_corrected); const hasSegments = call.segments && call.segments.length > 1; // Fetched lazily on expand: playback links are short-lived, so minting one // for every row up front would waste most of them and expire the rest. const [audioUrl, setAudioUrl] = useState(null); const [audioError, setAudioError] = useState(false); useEffect(() => { if (!expanded || !hasAudio || audioUrl || audioError) return; let cancelled = false; c2api.getCall(call.call_id) .then((full) => { if (!cancelled) setAudioUrl(full.audio_url ?? null); }) .catch(() => { if (!cancelled) setAudioError(true); }); return () => { cancelled = true; }; }, [expanded, hasAudio, audioUrl, audioError, call.call_id]); function startEdit() { setEditText(call.transcript_corrected ?? call.transcript ?? ""); setEditing(true); } async function saveEdit(e: React.MouseEvent) { e.stopPropagation(); setSaving(true); setSaveError(null); try { await c2api.patchTranscript(call.call_id, editText); setEditing(false); } catch (err) { setSaveError(err instanceof Error ? err.message : "Save failed"); } finally { setSaving(false); } } return ( <> hasDetails && setExpanded((v) => !v)} > {new Date(call.started_at).toLocaleDateString([], { month: "short", day: "numeric" })} {new Date(call.started_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} {call.talkgroup_name || call.talkgroup_id || "—"} {call.tags && call.tags.length > 0 && ( {call.tags[0]} )} {systemName ?? call.system_id ?? "—"} {call.node_id} {isActive ? ( ● live ) : ( {duration(call.started_at, call.ended_at)} )} {hasAudio ? ( ▶ ) : ( — )} {hasDetails && (expanded ? "▲" : "▼")} {expanded && hasDetails && ( {/* Audio player */} {hasAudio && ( audioError ? (

Could not load audio.

) : audioUrl ? (