140 lines
5.2 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|