Initial commit
This commit is contained in:
291
resources/js/components/LogQsoTable.tsx
Normal file
291
resources/js/components/LogQsoTable.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user