Files
server-26/drb-frontend/components/CallRow.tsx
T
2026-04-13 00:01:19 -04:00

140 lines
5.2 KiB
TypeScript

"use client";
import { useState } from "react";
import type { CallRecord } from "@/lib/types";
interface Props {
call: CallRecord;
systemName?: string;
}
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<string, string> = {
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 }: Props) {
const [expanded, setExpanded] = useState(false);
const [showOriginal, setShowOriginal] = useState(false);
const isActive = call.status === "active";
const hasDetails = call.transcript || call.transcript_corrected || (call.tags && call.tags.length > 0) || call.incident_id;
const displayTranscript = (!showOriginal && call.transcript_corrected) ? call.transcript_corrected : call.transcript;
const hasBoth = !!(call.transcript && call.transcript_corrected);
return (
<>
<tr
className={`border-b border-gray-800 font-mono text-sm ${hasDetails ? "cursor-pointer hover:bg-gray-900/50" : "hover:bg-gray-900/30"}`}
onClick={() => hasDetails && setExpanded((v) => !v)}
>
<td className="px-4 py-2 text-gray-400 text-xs">
{new Date(call.started_at).toLocaleTimeString()}
</td>
<td className="px-4 py-2 text-gray-300">
<span>{call.talkgroup_name || call.talkgroup_id || "—"}</span>
{call.tags && call.tags.length > 0 && (
<span className={`ml-2 text-xs px-1.5 py-0.5 rounded-full capitalize ${TAG_COLORS[call.tags[0]] ?? "bg-gray-800 text-gray-400"}`}>
{call.tags[0]}
</span>
)}
</td>
<td className="px-4 py-2 text-gray-400">{systemName ?? call.system_id ?? "—"}</td>
<td className="px-4 py-2 text-gray-400">{call.node_id}</td>
<td className="px-4 py-2">
{isActive ? (
<span className="text-orange-400 animate-pulse"> live</span>
) : (
<span className="text-gray-500">{duration(call.started_at, call.ended_at)}</span>
)}
</td>
<td className="px-4 py-2">
{call.audio_url ? (
<a
href={call.audio_url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-blue-400 hover:text-blue-300 text-xs"
>
audio
</a>
) : (
<span className="text-gray-700 text-xs"></span>
)}
</td>
<td className="px-4 py-2 text-gray-600 text-xs">
{hasDetails && (expanded ? "▲" : "▼")}
</td>
</tr>
{expanded && hasDetails && (
<tr className="bg-gray-900/60 border-b border-gray-800">
<td colSpan={7} className="px-6 py-3 space-y-2">
{/* Tags */}
{call.tags && call.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{call.tags.map((tag) => (
<span
key={tag}
className={`text-xs px-2 py-0.5 rounded-full capitalize ${TAG_COLORS[tag] ?? "bg-gray-800 text-gray-400"}`}
>
{tag}
</span>
))}
</div>
)}
{/* Incident link */}
{call.incident_id && (
<p className="text-xs font-mono text-indigo-400">
Incident:{" "}
<a href="/incidents" className="underline hover:text-indigo-300">
{call.incident_id.slice(0, 8)}
</a>
</p>
)}
{/* Transcript */}
{displayTranscript ? (
<div className="space-y-1">
<div className="flex items-center gap-2">
{hasBoth && (
<span className="text-xs text-gray-600 font-mono">
{showOriginal ? "original" : "corrected"}
</span>
)}
{!hasBoth && call.transcript_corrected && (
<span className="text-xs text-gray-600 font-mono">corrected</span>
)}
</div>
<pre className="text-xs text-gray-300 bg-gray-800 rounded-lg px-4 py-3 whitespace-pre-wrap font-mono leading-relaxed max-h-40 overflow-y-auto">
{displayTranscript}
</pre>
{hasBoth && (
<button
onClick={(e) => { e.stopPropagation(); setShowOriginal((v) => !v); }}
className="text-xs text-gray-600 hover:text-gray-400 font-mono transition-colors"
>
{showOriginal ? "show corrected ↑" : "show original ↓"}
</button>
)}
</div>
) : (
<p className="text-xs text-gray-600 font-mono italic">No transcript available.</p>
)}
</td>
</tr>
)}
</>
);
}