292 lines
9.7 KiB
TypeScript
292 lines
9.7 KiB
TypeScript
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<number, QsoResultItem>;
|
|
qsoOverrides: Record<number, QsoOverrideInfo>;
|
|
emptyLabel: string;
|
|
callsign?: string | null;
|
|
};
|
|
|
|
export default function LogQsoTable({
|
|
qsos,
|
|
locale,
|
|
formatDateTime,
|
|
officialQsoResults,
|
|
qsoOverrides,
|
|
emptyLabel,
|
|
callsign,
|
|
}: LogQsoTableProps) {
|
|
if (!qsos || qsos.length === 0) {
|
|
return <div>{emptyLabel}</div>;
|
|
}
|
|
|
|
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 (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-end">
|
|
<a
|
|
href="#"
|
|
className="text-xs text-foreground-500 hover:underline"
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
handleCsvExport();
|
|
}}
|
|
>
|
|
CSV
|
|
</a>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<Table
|
|
aria-label="QSO table"
|
|
isCompact
|
|
radius="sm"
|
|
removeWrapper
|
|
className="min-w-max"
|
|
classNames={{
|
|
th: "py-1 px-1 text-[11px] leading-tight",
|
|
td: "py-0.5 px-1 text-[11px] leading-tight",
|
|
}}
|
|
>
|
|
<TableHeader>
|
|
<TableColumn>#</TableColumn>
|
|
<TableColumn>Čas</TableColumn>
|
|
<TableColumn>Volací znak</TableColumn>
|
|
<TableColumn>Mode</TableColumn>
|
|
<TableColumn>RST odeslané</TableColumn>
|
|
<TableColumn>Číslo odeslané</TableColumn>
|
|
<TableColumn>RST přijaté</TableColumn>
|
|
<TableColumn>Číslo přijaté</TableColumn>
|
|
<TableColumn>WWL</TableColumn>
|
|
<TableColumn>Exchange</TableColumn>
|
|
<TableColumn>Body</TableColumn>
|
|
<TableColumn>Penalizace</TableColumn>
|
|
<TableColumn>Nové WWL</TableColumn>
|
|
<TableColumn>Nové DXCC</TableColumn>
|
|
<TableColumn>Dupl.</TableColumn>
|
|
<TableColumn>Chyba</TableColumn>
|
|
<TableColumn>Strana</TableColumn>
|
|
<TableColumn>Match</TableColumn>
|
|
<TableColumn>Zásah</TableColumn>
|
|
<TableColumn>Poznámka</TableColumn>
|
|
</TableHeader>
|
|
<TableBody items={qsos} className="text-[11px] leading-tight">
|
|
{(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 (
|
|
<TableRow key={qso.id}>
|
|
<TableCell style={cellStyle}>{qso.qso_index ?? "—"}</TableCell>
|
|
<TableCell style={cellStyle}>
|
|
{formatDateTime(qso.time_on ?? null, locale)}
|
|
</TableCell>
|
|
<TableCell style={cellStyle}>{qso.dx_call || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.mode_code || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.my_rst || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.my_serial || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.dx_rst || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.dx_serial || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.rx_wwl || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.rx_exchange || "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.points ?? "—"}</TableCell>
|
|
<TableCell style={cellStyle}>
|
|
{typeof officialQsoResults[qso.id]?.penalty_points === "number"
|
|
? officialQsoResults[qso.id]?.penalty_points
|
|
: "—"}
|
|
</TableCell>
|
|
<TableCell style={cellStyle}>{qso.new_wwl ? "N" : "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.new_dxcc ? "N" : "—"}</TableCell>
|
|
<TableCell style={cellStyle}>{qso.duplicate_qso ? "D" : "—"}</TableCell>
|
|
<TableCell style={cellStyle}>
|
|
{officialQsoResults[qso.id]?.error_code ?? "—"}
|
|
</TableCell>
|
|
<TableCell style={cellStyle}>
|
|
{officialQsoResults[qso.id]?.error_side &&
|
|
officialQsoResults[qso.id]?.error_side !== "NONE"
|
|
? officialQsoResults[qso.id]?.error_side
|
|
: "—"}
|
|
</TableCell>
|
|
<TableCell style={cellStyle}>
|
|
{matchConfidence === "PARTIAL" && matchTooltipLines.length > 0 ? (
|
|
<Tooltip
|
|
content={
|
|
<div className="text-xs leading-snug">
|
|
{matchTooltipLines.map((line, index) => (
|
|
<div key={index}>{line}</div>
|
|
))}
|
|
</div>
|
|
}
|
|
>
|
|
<span className="cursor-help">{matchConfidence}</span>
|
|
</Tooltip>
|
|
) : (
|
|
matchConfidence
|
|
)}
|
|
</TableCell>
|
|
<TableCell style={cellStyle}>{overrideLabel}</TableCell>
|
|
<TableCell style={cellStyle}>{note}</TableCell>
|
|
</TableRow>
|
|
);
|
|
}}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|