import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Tooltip, } from "@heroui/react"; import { saveAs } from "file-saver"; type LogQsoItem = { id: number; qso_index?: number | null; time_on?: string | null; dx_call?: string | null; my_rst?: string | null; my_serial?: string | null; dx_rst?: string | null; dx_serial?: string | null; rx_wwl?: string | null; rx_exchange?: string | null; mode_code?: string | null; new_exchange?: boolean | null; new_wwl?: boolean | null; new_dxcc?: boolean | null; duplicate_qso?: boolean | null; points?: number | null; remarks?: string | null; }; type QsoResultItem = { log_qso_id: number; penalty_points?: number | null; error_code?: string | null; error_side?: string | null; match_confidence?: string | null; match_type?: string | null; error_flags?: string[] | null; }; type QsoOverrideInfo = { reason?: string | null; forced_status?: string | null; forced_matched_log_qso_id?: number | null; forced_points?: number | null; forced_penalty?: number | null; }; type LogQsoTableProps = { qsos: LogQsoItem[]; locale: string; formatDateTime: (value: string | null | undefined, locale: string) => string; officialQsoResults: Record; qsoOverrides: Record; emptyLabel: string; callsign?: string | null; }; export default function LogQsoTable({ qsos, locale, formatDateTime, officialQsoResults, qsoOverrides, emptyLabel, callsign, }: LogQsoTableProps) { if (!qsos || qsos.length === 0) { return
{emptyLabel}
; } const getOverrideLabel = (override?: QsoOverrideInfo) => { if (!override) return "—"; if (override.forced_status && override.forced_status !== "AUTO") { return `STATUS: ${override.forced_status}`; } if (override.forced_matched_log_qso_id) { return `MATCH: ${override.forced_matched_log_qso_id}`; } if ( override.forced_points !== null && override.forced_points !== undefined || override.forced_penalty !== null && override.forced_penalty !== undefined ) { return "BODY: override"; } return "OVERRIDE"; }; const toCsvValue = (value: string | number | null | undefined) => { if (value === null || value === undefined || value === "—") return ""; return String(value).replace(/"/g, '""'); }; const handleCsvExport = () => { const header = [ "#", "Time", "Callsign", "Mode", "My RST", "My Serial", "DX RST", "DX Serial", "WWL", "Exchange", "Points", "Penalty", "New WWL", "New DXCC", "Dupe", "Error", "Side", "Match", "Override", "Note", ]; const lines = qsos.map((qso) => { const override = qsoOverrides[qso.id] ?? qsoOverrides[String(qso.id) as unknown as number]; const matchResult = officialQsoResults[qso.id]; const note = override ? override.reason ?? "—" : qso.remarks || "—"; const row = [ qso.qso_index ?? "", formatDateTime(qso.time_on ?? null, locale), qso.dx_call || "—", qso.mode_code || "—", qso.my_rst || "—", qso.my_serial || "—", qso.dx_rst || "—", qso.dx_serial || "—", qso.rx_wwl || "—", qso.rx_exchange || "—", qso.points ?? "—", typeof matchResult?.penalty_points === "number" ? matchResult?.penalty_points : "—", qso.new_wwl ? "N" : "—", qso.new_dxcc ? "N" : "—", qso.duplicate_qso ? "D" : "—", matchResult?.error_code ?? "—", matchResult?.error_side && matchResult?.error_side !== "NONE" ? matchResult?.error_side : "—", matchResult?.match_confidence ?? "—", getOverrideLabel(override), note, ]; return row.map((value) => `"${toCsvValue(value)}"`).join(","); }); const csv = [header.join(","), ...lines].join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const safeCallsign = callsign?.trim() || "qso_table"; const fileName = safeCallsign.replace(/\\s+/g, "_"); saveAs(blob, `${fileName}.csv`); }; return (
{ event.preventDefault(); handleCsvExport(); }} > CSV
# Čas Volací znak Mode RST odeslané Číslo odeslané RST přijaté Číslo přijaté WWL Exchange Body Penalizace Nové WWL Nové DXCC Dupl. Chyba Strana Match Zásah Poznámka {(qso) => { const override = qsoOverrides[qso.id] ?? qsoOverrides[String(qso.id) as unknown as number]; const matchResult = officialQsoResults[qso.id]; const matchConfidence = matchResult?.match_confidence ?? "—"; const matchTooltipLines: string[] = []; if (matchResult?.match_type) { matchTooltipLines.push(`Type: ${matchResult.match_type}`); } if (Array.isArray(matchResult?.error_flags) && matchResult.error_flags.length > 0) { matchTooltipLines.push(`Flags: ${matchResult.error_flags.join(", ")}`); } const cellStyle = override ? { backgroundColor: "#FEF3C7" } : undefined; const note = override ? override.reason ?? "—" : qso.remarks || "—"; const overrideLabel = getOverrideLabel(override); return ( {qso.qso_index ?? "—"} {formatDateTime(qso.time_on ?? null, locale)} {qso.dx_call || "—"} {qso.mode_code || "—"} {qso.my_rst || "—"} {qso.my_serial || "—"} {qso.dx_rst || "—"} {qso.dx_serial || "—"} {qso.rx_wwl || "—"} {qso.rx_exchange || "—"} {qso.points ?? "—"} {typeof officialQsoResults[qso.id]?.penalty_points === "number" ? officialQsoResults[qso.id]?.penalty_points : "—"} {qso.new_wwl ? "N" : "—"} {qso.new_dxcc ? "N" : "—"} {qso.duplicate_qso ? "D" : "—"} {officialQsoResults[qso.id]?.error_code ?? "—"} {officialQsoResults[qso.id]?.error_side && officialQsoResults[qso.id]?.error_side !== "NONE" ? officialQsoResults[qso.id]?.error_side : "—"} {matchConfidence === "PARTIAL" && matchTooltipLines.length > 0 ? ( {matchTooltipLines.map((line, index) => (
{line}
))} } > {matchConfidence}
) : ( matchConfidence )}
{overrideLabel} {note}
); }}
); }