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(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [officialResult, setOfficialResult] = useState(null); const [officialQsoResults, setOfficialQsoResults] = useState>({}); const [officialLoading, setOfficialLoading] = useState(false); const [officialError, setOfficialError] = useState(null); const [logOverrideReason, setLogOverrideReason] = useState(null); const [qsoOverrides, setQsoOverrides] = useState>({}); const [qsoTableRows, setQsoTableRows] = useState([]); 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(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(`/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 = {}; const overrideMap: Record = {}; 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("/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("/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) =>
{item}
); if (lines.length > 0) return lines; } } catch { // fallback to raw string } return
{raw}
; }; return (
{title} {detail?.tname && ( {detail.tname} )} {detail?.tdate && ( {detail.tdate} )}
{error &&
{error}
} {loading &&
{t("loading") ?? "Načítám..."}
} {detail && !loading && (
{isAdmin ? (
PCall: {detail.pcall || "—"} {detail.pclub ? ` (${detail.pclub})` : ""}
{detail.pband && (
PBand: {detail.pband}
)} {detail.psect && (
PSect: {detail.psect}
)} {detail.padr1 && (
PAdr1: {detail.padr1}
)} {detail.padr2 && (
PAdr2: {detail.padr2}
)} {detail.mope1 && (
MOpe1: {detail.mope1}
)} {detail.mope2 && (
MOpe2: {detail.mope2}
)}
RCall: {detail.rcall || "—"}
{detail.radr1 && (
RAdr1: {detail.radr1}
)} {detail.radr2 && (
RAdr2: {detail.radr2}
)} {(detail.rpoco || detail.rcity) && (
RPoCo/RCity: {detail.rpoco ?? ""} {detail.rcity ?? ""}
)} {detail.rcoun && (
RCoun: {detail.rcoun}
)} {detail.rphon && (
RPhon: {detail.rphon}
)} {detail.rhbbs && (
RHBBS: {detail.rhbbs}
)}
{detail.stxeq && (
STXEq: {detail.stxeq}
)} {detail.srxeq && (
SRXEq: {detail.srxeq}
)} {detail.power_watt && (
SPowe: {detail.power_watt}
)} {detail.sante && (
SAntenna: {detail.sante}
)} {detail.santh && (
SAntH: {detail.santh}
)}
) : (
PCall: {detail.pcall || "—"}
{detail.pband && (
PBand: {detail.pband}
)} {detail.psect && (
PSect: {detail.psect}
)} {detail.power_watt && (
SPowe: {detail.power_watt}
)}
{detail.stxeq && (
STXEq: {detail.stxeq}
)} {detail.srxeq && (
SRXEq: {detail.srxeq}
)} {detail.sante && (
SAntenna: {detail.sante}
)} {detail.santh && (
SAntH: {detail.santh}
)}
)}

Deklarované výsledky

Počet QSO: {detail.claimed_qso_count ?? "—"}
Body: {detail.claimed_score ?? "—"}
Unikátních WWL: {detail.claimed_wwl ?? "—"}
Počet DXCC: {detail.claimed_dxcc ?? "—"}
{isAdmin && (

Zkontrolované výsledky

Počet QSO: {officialLoading ? "…" : officialResult?.valid_qso_count ?? "—"}
Body: {officialLoading ? "…" : officialResult?.official_score ?? "—"}
Unikátních WWL: {officialLoading ? "…" : officialResult?.multiplier_count ?? "—"}
Penalizace: {officialLoading ? "…" : officialResult?.penalty_score ?? "—"}
{officialError && (
{officialError}
)}
)}
{isAdmin && detail.remarks_eval && (
{renderRemarksEval(detail.remarks_eval)}
)} {isAdmin && detail.raw_header && ( RAW header} >
                        {detail.raw_header}
                      
)}
)}
{isAdmin && (logOverrideReason || Object.keys(qsoOverrides).length > 0) && ( Zásahy rozhodčího
{logOverrideReason && (
Log: {logOverrideReason}
)} {Object.keys(qsoOverrides).length > 0 && (
{qsoTableRows .filter((qso) => qsoOverrides[qso.id]?.reason) .map((qso) => (
QSO #{qso.qso_index ?? qso.id}: {qso.dx_call ?? "—"} —{" "} {qsoOverrides[qso.id]?.reason}
))}
)}
)} {isAdmin && ( QSO )}
); }