693 lines
25 KiB
TypeScript
693 lines
25 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import axios from "axios";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardBody,
|
|
Divider,
|
|
Accordion,
|
|
AccordionItem,
|
|
} from "@heroui/react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useContestStore } from "@/stores/contestStore";
|
|
import { useLanguageStore } from "@/stores/languageStore";
|
|
import { useUserStore } from "@/stores/userStore";
|
|
import LogQsoTable from "@/components/LogQsoTable";
|
|
|
|
type LogDetailData = {
|
|
id: number;
|
|
round_id: number;
|
|
pcall?: string | null;
|
|
rcall?: string | null;
|
|
tname?: string | null;
|
|
tdate?: string | null;
|
|
pwwlo?: string | null;
|
|
pexch?: string | null;
|
|
psect?: string | null;
|
|
pband?: string | null;
|
|
pclub?: string | null;
|
|
locator?: string | null;
|
|
raw_header?: string | null;
|
|
remarks?: string | null;
|
|
remarks_eval?: string | null;
|
|
claimed_qso_count?: number | null;
|
|
claimed_score?: number | null;
|
|
claimed_wwl?: string | null;
|
|
claimed_dxcc?: string | null;
|
|
round?: {
|
|
id: number;
|
|
contest_id: number;
|
|
name: string;
|
|
start_time?: string | null;
|
|
end_time?: string | null;
|
|
logs_deadline?: string | null;
|
|
} | null;
|
|
padr1?: string | null;
|
|
padr2?: string | null;
|
|
radr1?: string | null;
|
|
radr2?: string | null;
|
|
rpoco?: string | null;
|
|
rcity?: string | null;
|
|
rphon?: string | null;
|
|
rhbbs?: string | null;
|
|
rname?: string | null;
|
|
rcoun?: string | null;
|
|
mope1?: string | null;
|
|
mope2?: string | null;
|
|
stxeq?: string | null;
|
|
srxeq?: string | null;
|
|
sante?: string | null;
|
|
santh?: string | null;
|
|
power_watt?: number | 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;
|
|
qsos?: {
|
|
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;
|
|
}[];
|
|
};
|
|
|
|
function formatDateTime(value: string | null | undefined, locale: string): string {
|
|
if (!value) return "—";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
return new Intl.DateTimeFormat(locale, {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
}).format(date);
|
|
}
|
|
|
|
type LogDetailProps = {
|
|
logId: number | null;
|
|
};
|
|
|
|
type LogResultItem = {
|
|
id: number;
|
|
log_id: number;
|
|
official_score?: number | null;
|
|
penalty_score?: number | null;
|
|
base_score?: number | null;
|
|
multiplier_count?: number | null;
|
|
valid_qso_count?: number | null;
|
|
dupe_qso_count?: number | null;
|
|
busted_qso_count?: number | null;
|
|
other_error_qso_count?: number | null;
|
|
};
|
|
|
|
type LogResultsResponse = {
|
|
data: LogResultItem[];
|
|
};
|
|
|
|
type QsoResultItem = {
|
|
log_qso_id: number;
|
|
points?: number | null;
|
|
penalty_points?: number | null;
|
|
error_code?: string | null;
|
|
error_side?: string | null;
|
|
match_confidence?: string | null;
|
|
match_type?: string | null;
|
|
error_flags?: string[] | null;
|
|
is_valid?: boolean | null;
|
|
is_duplicate?: boolean | null;
|
|
is_nil?: boolean | null;
|
|
is_busted_call?: boolean | null;
|
|
is_busted_rst?: boolean | null;
|
|
is_busted_exchange?: boolean | null;
|
|
is_time_out_of_window?: boolean | null;
|
|
};
|
|
|
|
type LogOverrideItem = {
|
|
id: number;
|
|
log_id: number;
|
|
reason?: string | null;
|
|
};
|
|
|
|
type LogOverridesResponse = {
|
|
data: LogOverrideItem[];
|
|
};
|
|
|
|
type QsoOverrideItem = {
|
|
id: number;
|
|
log_qso_id: number;
|
|
forced_status?: string | null;
|
|
forced_matched_log_qso_id?: number | null;
|
|
forced_points?: number | null;
|
|
forced_penalty?: number | null;
|
|
reason?: string | null;
|
|
};
|
|
|
|
type LogQsoTableRow = {
|
|
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;
|
|
result?: QsoResultItem | null;
|
|
override?: QsoOverrideItem | null;
|
|
};
|
|
|
|
type QsoTableResponse = {
|
|
evaluation_run_id: number | null;
|
|
data: LogQsoTableRow[];
|
|
};
|
|
|
|
export default function LogDetail({ logId }: LogDetailProps) {
|
|
const { t } = useTranslation("common");
|
|
const locale = useLanguageStore((s) => s.locale) || navigator.language || "cs-CZ";
|
|
const setSelectedRound = useContestStore((s) => s.setSelectedRound);
|
|
const user = useUserStore((s) => s.user);
|
|
const isAdmin = !!user?.is_admin;
|
|
|
|
const [detail, setDetail] = useState<LogDetailData | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [officialResult, setOfficialResult] = useState<LogResultItem | null>(null);
|
|
const [officialQsoResults, setOfficialQsoResults] = useState<Record<number, QsoResultItem>>({});
|
|
const [officialLoading, setOfficialLoading] = useState(false);
|
|
const [officialError, setOfficialError] = useState<string | null>(null);
|
|
const [logOverrideReason, setLogOverrideReason] = useState<string | null>(null);
|
|
const [qsoOverrides, setQsoOverrides] = useState<Record<number, QsoOverrideItem>>({});
|
|
const [qsoTableRows, setQsoTableRows] = useState<LogQsoTableRow[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!logId) return;
|
|
let active = true;
|
|
(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const endpoint = isAdmin ? `/api/logs/${logId}` : `/api/logs/${logId}/public`;
|
|
const res = await axios.get<LogDetailData>(endpoint, {
|
|
params: isAdmin ? { include_qsos: 0 } : undefined,
|
|
headers: { Accept: "application/json" },
|
|
withCredentials: true,
|
|
});
|
|
if (!active) return;
|
|
setDetail(res.data);
|
|
if (res.data.round) {
|
|
setSelectedRound({
|
|
id: res.data.round.id,
|
|
contest_id: res.data.round.contest_id,
|
|
name: res.data.round.name,
|
|
description: null,
|
|
is_active: true,
|
|
is_test: false,
|
|
is_sixhr: false,
|
|
start_time: res.data.round.start_time ?? null,
|
|
end_time: res.data.round.end_time ?? null,
|
|
logs_deadline: res.data.round.logs_deadline ?? null,
|
|
});
|
|
}
|
|
} catch (e: any) {
|
|
if (!active) return;
|
|
const fallback = isAdmin
|
|
? t("unable_to_load_log") ?? "Nepodařilo se načíst log."
|
|
: "Detail logu bude dostupný po zveřejnění finálních výsledků.";
|
|
const message = e?.response?.data?.message || fallback;
|
|
setError(message);
|
|
} finally {
|
|
if (active) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [logId, t, setSelectedRound, isAdmin]);
|
|
|
|
useEffect(() => {
|
|
if (!detail?.id || !isAdmin) return;
|
|
let active = true;
|
|
|
|
(async () => {
|
|
try {
|
|
setOfficialLoading(true);
|
|
setOfficialError(null);
|
|
const qsoTableRes = await axios.get<QsoTableResponse>(`/api/logs/${detail.id}/qso-table`, {
|
|
headers: { Accept: "application/json" },
|
|
withCredentials: true,
|
|
});
|
|
if (!active) return;
|
|
|
|
const rows = qsoTableRes.data.data ?? [];
|
|
setQsoTableRows(rows);
|
|
|
|
const qsoMap: Record<number, QsoResultItem> = {};
|
|
const overrideMap: Record<number, QsoOverrideItem> = {};
|
|
rows.forEach((row) => {
|
|
if (row.result) {
|
|
qsoMap[row.id] = row.result;
|
|
}
|
|
if (row.override) {
|
|
overrideMap[row.id] = row.override;
|
|
}
|
|
});
|
|
setOfficialQsoResults(qsoMap);
|
|
setQsoOverrides(overrideMap);
|
|
|
|
const effectiveRunId = qsoTableRes.data.evaluation_run_id ?? null;
|
|
if (!effectiveRunId) {
|
|
setOfficialResult(null);
|
|
setLogOverrideReason(null);
|
|
return;
|
|
}
|
|
|
|
const resultRes = await axios.get<LogResultsResponse>("/api/log-results", {
|
|
params: {
|
|
evaluation_run_id: effectiveRunId,
|
|
log_id: detail.id,
|
|
per_page: 1,
|
|
},
|
|
headers: { Accept: "application/json" },
|
|
withCredentials: true,
|
|
});
|
|
if (!active) return;
|
|
setOfficialResult(resultRes.data.data?.[0] ?? null);
|
|
|
|
const overrideRes = await axios.get<LogOverridesResponse>("/api/log-overrides", {
|
|
params: {
|
|
evaluation_run_id: effectiveRunId,
|
|
log_id: detail.id,
|
|
per_page: 1,
|
|
},
|
|
headers: { Accept: "application/json" },
|
|
withCredentials: true,
|
|
});
|
|
if (!active) return;
|
|
setLogOverrideReason(overrideRes.data.data?.[0]?.reason ?? null);
|
|
} catch (e: any) {
|
|
if (!active) return;
|
|
const msg = e?.response?.data?.message || "Nepodařilo se načíst zkontrolované výsledky.";
|
|
setOfficialError(msg);
|
|
setOfficialResult(null);
|
|
setOfficialQsoResults({});
|
|
setLogOverrideReason(null);
|
|
setQsoOverrides({});
|
|
setQsoTableRows([]);
|
|
} finally {
|
|
if (active) setOfficialLoading(false);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [detail?.id, isAdmin]);
|
|
|
|
const title = (() => {
|
|
const pcall = detail?.pcall ?? "";
|
|
if (!isAdmin) {
|
|
return pcall || (t("log") ?? "Log");
|
|
}
|
|
const rcall = detail?.rcall ?? "";
|
|
if (pcall && rcall && pcall !== rcall) {
|
|
return `${pcall}-${rcall}`;
|
|
}
|
|
return pcall || rcall || (t("log") ?? "Log");
|
|
})();
|
|
|
|
const renderRemarksEval = (raw: string | null | undefined) => {
|
|
if (!raw) return "—";
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed)) {
|
|
const lines = parsed
|
|
.filter((item) => typeof item === "string" && item.trim() !== "")
|
|
.map((item, idx) => <div key={idx}>{item}</div>);
|
|
if (lines.length > 0) return lines;
|
|
}
|
|
} catch {
|
|
// fallback to raw string
|
|
}
|
|
|
|
return <div>{raw}</div>;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex flex-col">
|
|
<span className="text-lg font-semibold">{title}</span>
|
|
{detail?.tname && (
|
|
<span className="text-sm text-foreground-500">{detail.tname}</span>
|
|
)}
|
|
{detail?.tdate && (
|
|
<span className="text-sm text-foreground-500">{detail.tdate}</span>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<Divider />
|
|
<CardBody>
|
|
{error && <div className="text-sm text-red-600">{error}</div>}
|
|
{loading && <div>{t("loading") ?? "Načítám..."}</div>}
|
|
{detail && !loading && (
|
|
<div className="space-y-4 text-sm">
|
|
{isAdmin ? (
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_pcall_hint") ?? "Call used during contest"}>
|
|
PCall:
|
|
</span>
|
|
<span>
|
|
{detail.pcall || "—"}
|
|
{detail.pclub ? ` (${detail.pclub})` : ""}
|
|
</span>
|
|
</div>
|
|
{detail.pband && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_pband_hint") ?? "Band"}>PBand:</span>
|
|
<span>{detail.pband}</span>
|
|
</div>
|
|
)}
|
|
{detail.psect && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_psect_hint") ?? "Section / category"}>PSect:</span>
|
|
<span>{detail.psect}</span>
|
|
</div>
|
|
)}
|
|
{detail.padr1 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_padr1_hint") ?? "Address line 1 (QTH)"}>PAdr1:</span>
|
|
<span>{detail.padr1}</span>
|
|
</div>
|
|
)}
|
|
{detail.padr2 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_padr2_hint") ?? "Address line 2 (QTH)"}>PAdr2:</span>
|
|
<span>{detail.padr2}</span>
|
|
</div>
|
|
)}
|
|
{detail.mope1 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_mope1_hint") ?? "Multi operator line 1"}>MOpe1:</span>
|
|
<span>{detail.mope1}</span>
|
|
</div>
|
|
)}
|
|
{detail.mope2 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_mope2_hint") ?? "Multi operator line 2"}>MOpe2:</span>
|
|
<span>{detail.mope2}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_rcall_hint") ?? "Responsible operator callsign"}>RCall:</span>
|
|
<span>{detail.rcall || "—"}</span>
|
|
</div>
|
|
{detail.radr1 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_radr1_hint") ?? "Address line 1 of responsible operator"}>RAdr1:</span>
|
|
<span>{detail.radr1}</span>
|
|
</div>
|
|
)}
|
|
{detail.radr2 && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_radr2_hint") ?? "Address line 2 of responsible operator"}>RAdr2:</span>
|
|
<span>{detail.radr2}</span>
|
|
</div>
|
|
)}
|
|
{(detail.rpoco || detail.rcity) && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_rpoco_rcity_hint") ?? "Postal code / city of responsible operator"}>RPoCo/RCity:</span>
|
|
<span>
|
|
{detail.rpoco ?? ""} {detail.rcity ?? ""}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{detail.rcoun && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_rcoun_hint") ?? "Country of responsible operator"}>RCoun:</span>
|
|
<span>{detail.rcoun}</span>
|
|
</div>
|
|
)}
|
|
{detail.rphon && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_rphon_hint") ?? "Phone of responsible operator"}>RPhon:</span>
|
|
<span>{detail.rphon}</span>
|
|
</div>
|
|
)}
|
|
{detail.rhbbs && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_rhbbs_hint") ?? "Home BBS of responsible operator"}>RHBBS:</span>
|
|
<span>{detail.rhbbs}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{detail.stxeq && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_stxeq_hint") ?? "TX equipment"}>STXEq:</span>
|
|
<span>{detail.stxeq}</span>
|
|
</div>
|
|
)}
|
|
{detail.srxeq && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_srxeq_hint") ?? "RX equipment"}>SRXEq:</span>
|
|
<span>{detail.srxeq}</span>
|
|
</div>
|
|
)}
|
|
{detail.power_watt && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_spowe_hint") ?? "TX power [W]"}>SPowe:</span>
|
|
<span>{detail.power_watt}</span>
|
|
</div>
|
|
)}
|
|
{detail.sante && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_sante_hint") ?? "Antenna"}>SAntenna:</span>
|
|
<span>{detail.sante}</span>
|
|
</div>
|
|
)}
|
|
{detail.santh && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_santh_hint") ?? "Antenna height [m] / ASL [m]"}>SAntH:</span>
|
|
<span>{detail.santh}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_pcall_hint") ?? "Call used during contest"}>
|
|
PCall:
|
|
</span>
|
|
<span>{detail.pcall || "—"}</span>
|
|
</div>
|
|
{detail.pband && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_pband_hint") ?? "Band"}>PBand:</span>
|
|
<span>{detail.pband}</span>
|
|
</div>
|
|
)}
|
|
{detail.psect && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_psect_hint") ?? "Section / category"}>PSect:</span>
|
|
<span>{detail.psect}</span>
|
|
</div>
|
|
)}
|
|
{detail.power_watt && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_spowe_hint") ?? "TX power [W]"}>SPowe:</span>
|
|
<span>{detail.power_watt}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
{detail.stxeq && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_stxeq_hint") ?? "TX equipment"}>STXEq:</span>
|
|
<span>{detail.stxeq}</span>
|
|
</div>
|
|
)}
|
|
{detail.srxeq && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_srxeq_hint") ?? "RX equipment"}>SRXEq:</span>
|
|
<span>{detail.srxeq}</span>
|
|
</div>
|
|
)}
|
|
{detail.sante && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_sante_hint") ?? "Antenna"}>SAntenna:</span>
|
|
<span>{detail.sante}</span>
|
|
</div>
|
|
)}
|
|
{detail.santh && (
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold" title={t("edi_santh_hint") ?? "Antenna height [m] / ASL [m]"}>SAntH:</span>
|
|
<span>{detail.santh}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Divider />
|
|
<div className="space-y-3">
|
|
<div className={isAdmin ? "grid gap-4 md:grid-cols-2 text-sm" : "text-sm"}>
|
|
<div className="space-y-1">
|
|
<h4 className="font-semibold">Deklarované výsledky</h4>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Počet QSO:</span>
|
|
<span>{detail.claimed_qso_count ?? "—"}</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Body:</span>
|
|
<span>{detail.claimed_score ?? "—"}</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Unikátních WWL:</span>
|
|
<span>{detail.claimed_wwl ?? "—"}</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Počet DXCC:</span>
|
|
<span>{detail.claimed_dxcc ?? "—"}</span>
|
|
</div>
|
|
</div>
|
|
{isAdmin && (
|
|
<div className="space-y-1">
|
|
<h4 className="font-semibold">Zkontrolované výsledky</h4>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Počet QSO:</span>
|
|
<span>
|
|
{officialLoading ? "…" : officialResult?.valid_qso_count ?? "—"}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Body:</span>
|
|
<span>
|
|
{officialLoading ? "…" : officialResult?.official_score ?? "—"}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Unikátních WWL:</span>
|
|
<span>
|
|
{officialLoading ? "…" : officialResult?.multiplier_count ?? "—"}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="font-semibold">Penalizace:</span>
|
|
<span>
|
|
{officialLoading ? "…" : officialResult?.penalty_score ?? "—"}
|
|
</span>
|
|
</div>
|
|
{officialError && (
|
|
<div className="text-xs text-red-600">{officialError}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isAdmin && detail.remarks_eval && (
|
|
<div className="text-sm text-red-600">
|
|
{renderRemarksEval(detail.remarks_eval)}
|
|
</div>
|
|
)}
|
|
{isAdmin && detail.raw_header && (
|
|
<Accordion>
|
|
<AccordionItem
|
|
key="raw"
|
|
aria-label="RAW header"
|
|
title={<span className="cursor-pointer text-primary-600 hover:underline">RAW header</span>}
|
|
>
|
|
<pre className="bg-default-50 p-2 rounded text-xs whitespace-pre-wrap">
|
|
{detail.raw_header}
|
|
</pre>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardBody>
|
|
</Card>
|
|
|
|
{isAdmin && (logOverrideReason || Object.keys(qsoOverrides).length > 0) && (
|
|
<Card>
|
|
<CardHeader>
|
|
<span className="text-md font-semibold">Zásahy rozhodčího</span>
|
|
</CardHeader>
|
|
<Divider />
|
|
<CardBody>
|
|
<div className="text-sm text-foreground-600 space-y-2">
|
|
{logOverrideReason && (
|
|
<div>Log: {logOverrideReason}</div>
|
|
)}
|
|
{Object.keys(qsoOverrides).length > 0 && (
|
|
<div className="space-y-1">
|
|
{qsoTableRows
|
|
.filter((qso) => qsoOverrides[qso.id]?.reason)
|
|
.map((qso) => (
|
|
<div key={qso.id}>
|
|
QSO #{qso.qso_index ?? qso.id}: {qso.dx_call ?? "—"} —{" "}
|
|
{qsoOverrides[qso.id]?.reason}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
)}
|
|
|
|
{isAdmin && (
|
|
<Card>
|
|
<CardHeader>
|
|
<span className="text-md font-semibold">QSO</span>
|
|
</CardHeader>
|
|
<Divider />
|
|
<CardBody>
|
|
<LogQsoTable
|
|
key={`${detail?.id ?? "log"}-${qsoTableRows.length}-${Object.keys(qsoOverrides).length}-${Object.keys(officialQsoResults).length}`}
|
|
qsos={qsoTableRows}
|
|
locale={locale}
|
|
formatDateTime={formatDateTime}
|
|
officialQsoResults={officialQsoResults}
|
|
qsoOverrides={qsoOverrides}
|
|
emptyLabel={t("logs_empty") ?? "Žádné QSO záznamy."}
|
|
callsign={title}
|
|
/>
|
|
</CardBody>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|