import React, { useEffect, useMemo, useState } from "react"; import axios from "axios"; import { Button, Card, CardBody, CardHeader } from "@heroui/react"; import FileDropZone from "@/components/FileDropZone"; import HeaderFormFields from "@/components/RoundFileUpload/HeaderFormFields"; import UploadMessages from "@/components/RoundFileUpload/UploadMessages"; import { useUserStore } from "@/stores/userStore"; import { useContestStore } from "@/stores/contestStore"; import { useTranslation } from "react-i18next"; import { EMAIL_REGEX, PWWLO_REGEX, computeQsoCountWarning, extractHeaderMap, hasEdiHeader, hasPowerKeyword, normalizeSpowe, parsePSect, updateContentWithHeaders, validateCallsign, validateCallsignList, isMultiOp, isSingleOp, buildIaruPsect, } from "@/utils/ediValidation"; import { validateEdi } from "@/utils/ediFileValidation"; import { type EdiHeaderForm } from "@/types/edi"; import { useRoundMeta } from "@/hooks/useRoundMeta"; type RoundFileUploadProps = { roundId: number | null; startTime?: string | null; logsDeadline?: string | null; onUploaded?: () => void; }; // Lokátor, email a callsign regexy pro konzistentní validaci všech vstupů const isWithinWindow = (_start?: string | null, deadline?: string | null) => { if (!deadline) return false; const endDate = new Date(deadline); if (Number.isNaN(endDate.getTime())) return false; const now = new Date(); return now <= endDate; }; const FILE_NAME_RULES = [ { code: "01", operator: "SO", time: null, band: "145mhz" }, { code: "02", operator: "MO", time: null, band: "145mhz" }, { code: "03", operator: "SO", time: null, band: "435mhz" }, { code: "04", operator: "MO", time: null, band: "435mhz" }, { code: "05", operator: "SO", time: null, band: "1.3ghz" }, { code: "06", operator: "MO", time: null, band: "1.3ghz" }, { code: "07", operator: "SO", time: null, band: "2.3ghz" }, { code: "08", operator: "MO", time: null, band: "2.3ghz" }, { code: "09", operator: "SO", time: null, band: "3.4ghz" }, { code: "10", operator: "MO", time: null, band: "3.4ghz" }, { code: "11", operator: "SO", time: null, band: "5.7ghz" }, { code: "12", operator: "MO", time: null, band: "5.7ghz" }, { code: "13", operator: "SO", time: null, band: "10ghz" }, { code: "14", operator: "MO", time: null, band: "10ghz" }, { code: "15", operator: "SO", time: null, band: "24ghz" }, { code: "16", operator: "MO", time: null, band: "24ghz" }, { code: "17", operator: "SO", time: null, band: "47ghz" }, { code: "18", operator: "MO", time: null, band: "47ghz" }, { code: "19", operator: "SO", time: null, band: "76ghz" }, { code: "20", operator: "MO", time: null, band: "76ghz" }, { code: "21", operator: "SO", time: null, band: "120ghz" }, { code: "22", operator: "MO", time: null, band: "120ghz" }, { code: "23", operator: "SO", time: null, band: "134ghz" }, { code: "24", operator: "MO", time: null, band: "134ghz" }, { code: "25", operator: "SO", time: null, band: "248ghz" }, { code: "26", operator: "MO", time: null, band: "248ghz" }, { code: "50", operator: "SO", time: null, band: "50mhz" }, { code: "51", operator: "MO", time: null, band: "50mhz" }, { code: "61", operator: "SO", time: "6H", band: "145mhz" }, { code: "62", operator: "MO", time: "6H", band: "145mhz" }, { code: "63", operator: "SO", time: "6H", band: "435mhz" }, { code: "64", operator: "MO", time: "6H", band: "435mhz" }, ] as const; const FILE_NAME_CODE_MAP = new Map( FILE_NAME_RULES.map((rule) => [ `${rule.time ?? ""}|${rule.operator}|${rule.band}`, rule.code, ]) ); const normalizeBandKey = (value: string) => value.trim().toLowerCase().replace(",", ".").replace(/\s+/g, ""); export default function RoundFileUpload({ roundId, startTime, logsDeadline, onUploaded }: RoundFileUploadProps) { const user = useUserStore((s) => s.user); const selectedRound = useContestStore((s) => s.selectedRound); const { t } = useTranslation("common"); // Stav hlaviček, validací a vedlejších efektů formuláře const [selectedFiles, setSelectedFiles] = useState(null); const [headerForm, setHeaderForm] = useState({ TName: "", TDate: "", PCall: "", PWWLo: "", PSect: "", PBand: "", RHBBS: "", SPowe: "", SAnte: "", RCall: "", MOpe1: "", MOpe2: "", }); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [uploadedOnce, setUploadedOnce] = useState(false); const [downloadEdited, setDownloadEdited] = useState(false); const [normalizedFileName, setNormalizedFileName] = useState(null); const [qsoCountWarning, setQsoCountWarning] = useState(null); const [qsoCallsignInfo, setQsoCallsignInfo] = useState(null); const [pbandInfo, setPbandInfo] = useState(null); const [spoweInfo, setSpoweInfo] = useState(null); const [isEdiFile, setIsEdiFile] = useState(true); const [unsupportedInfo, setUnsupportedInfo] = useState(null); const { bands, bandsError, bandsLoading, ediBands } = useRoundMeta(roundId, selectedRound); const bandNames = useMemo(() => new Set(bands.map((b) => b.name)), [bands]); const bandValue = headerForm.PBand.trim(); const bandUnknown = bands.length > 0 && bandValue !== "" && !bandNames.has(bandValue); const bandMissing = bandValue === ""; const normalizedPwwlo = headerForm.PWWLo.trim().toUpperCase(); const pwwloEmpty = normalizedPwwlo === ""; const pwwloFormatInvalid = !pwwloEmpty && !PWWLO_REGEX.test(normalizedPwwlo); const pwwloInvalid = pwwloEmpty || pwwloFormatInvalid; const rhbbsValue = headerForm.RHBBS.trim(); const rhbbsEmpty = rhbbsValue === ""; const rhbbsFormatInvalid = !rhbbsEmpty && !EMAIL_REGEX.test(rhbbsValue); const rhbbsInvalid = rhbbsEmpty || rhbbsFormatInvalid; const rhbbsWarning = selectedFiles && selectedFiles.length === 1 && rhbbsInvalid ? rhbbsEmpty ? (t("upload_warning_rhbbs_required") as string) || "RHBBS chybí." : (t("upload_warning_rhbbs_format") as string) || "RHBBS není ve formátu e-mailu." : null; const pCallValidation = useMemo(() => validateCallsign(headerForm.PCall), [headerForm.PCall]); const pCallEmpty = headerForm.PCall.trim() === ""; const pCallInvalid = pCallEmpty || !pCallValidation.isValid; const rCallValidation = useMemo(() => validateCallsign(headerForm.RCall), [headerForm.RCall]); const tNameInvalid = headerForm.TName.trim() === ""; const tDateError = (() => { const value = headerForm.TDate.trim(); if (!value) return "TDate je povinné."; const parts = value.split(";"); const invalidCount = parts.length !== 2; const invalidFormat = parts.some((p) => !/^\d{8}$/.test(p)); const inverted = !invalidCount && !invalidFormat && parseInt(parts[0], 10) > parseInt(parts[1], 10); if (invalidCount || invalidFormat || inverted) { return "TDate musí být ve formátu YYYYMMDD;YYYYMMDD a první datum nesmí být větší."; } return null; })(); const spoweParsed = useMemo(() => normalizeSpowe(headerForm.SPowe), [headerForm.SPowe]); const psectValue = headerForm.PSect.trim(); const allowSixHour = !!selectedRound?.is_sixhr; const psectValidation = useMemo( // Formulář validujeme stejnými pravidly jako načtený soubor (STRICT režim) () => parsePSect(psectValue, { strict: true, allow6h: allowSixHour }), [psectValue, allowSixHour] ); const psectMissing = psectValue === ""; const psectHasErrors = !psectMissing && psectValidation.errors.length > 0; const spoweNumber = useMemo(() => { if (!spoweParsed.valid) return null; const parsed = parseFloat(spoweParsed.normalized); return Number.isFinite(parsed) ? parsed : null; }, [spoweParsed]); const bandAllowsPowerCategory = useMemo(() => { const band = bands.find((b) => b.name === bandValue); return !!band?.has_power_category; }, [bands, bandValue]); const shouldApplyLowPowerCategory = !psectValidation.isCheckLog && bandAllowsPowerCategory && spoweNumber !== null && spoweNumber <= 100; const psectTarget = useMemo( () => ({ ...psectValidation, powerClass: shouldApplyLowPowerCategory ? "LP" : psectValidation.powerClass, }), [psectValidation, shouldApplyLowPowerCategory] ); const psectCanonical = useMemo(() => buildIaruPsect(psectTarget), [psectTarget]); const psectNeedsFormat = !psectMissing && !!psectCanonical && psectValue.trim().toUpperCase().replace(/\s+/g, " ") !== psectCanonical.toUpperCase().replace(/\s+/g, " "); const shouldShowIaruAdjustButton = !!psectNeedsFormat && !!selectedFiles && selectedFiles.length === 1 && isEdiFile; const fileNameStatus = useMemo(() => { if (!selectedFiles || selectedFiles.length !== 1 || !isEdiFile) return null; if (psectValidation.isCheckLog) return null; const pcallRaw = headerForm.PCall.trim(); const baseCall = pcallRaw.split("/")[0]?.replace(/[^A-Za-z0-9]/g, "").toUpperCase() ?? ""; const bandKey = normalizeBandKey(headerForm.PBand); const operatorKey = psectValidation.operatorClass ?? ""; const timeKey = psectValidation.timeClass ?? ""; if (!baseCall || !bandKey || !operatorKey) { return { state: "pending", message: (t("upload_filename_missing_fields") as string) || "Doplň PCall, PSect a PBand, aby šlo ověřit název souboru.", }; } const code = FILE_NAME_CODE_MAP.get(`${timeKey}|${operatorKey}|${bandKey}`); if (!code) { return { state: "invalid", message: (t("upload_filename_unknown_code") as string) || "Nelze určit prefix názvu souboru pro zvolenou kategorii a pásmo.", }; } const expectedName = `${code}${baseCall}.edi`; const pattern = new RegExp(`^${code}${baseCall}[A-Za-z0-9._-]*\\.edi$`, "i"); const isValid = pattern.test(selectedFiles[0].name); return { state: isValid ? "valid" : "invalid", expectedName, message: isValid ? (t("upload_filename_valid") as string) || "Název souboru odpovídá pravidlům." : (t("upload_filename_invalid", { expected: expectedName }) as string) || `Název souboru neodpovídá pravidlům. Doporučený tvar: ${expectedName}`, }; }, [ selectedFiles, isEdiFile, headerForm.PCall, headerForm.PBand, psectValidation.operatorClass, psectValidation.timeClass, psectValidation.isCheckLog, t, ]); const isAuth = Boolean(user); const windowOpen = useMemo(() => isWithinWindow(startTime, logsDeadline), [startTime, logsDeadline]); const canUpload = isAuth || windowOpen; const anonymousBlocked = !isAuth && uploadedOnce; const readFileAsText = (file: File) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : ""); reader.onerror = () => reject(reader.error); reader.readAsText(file); }); // Mapuje alternativní zápis PBand (EDI pásma) na název pásma v DB const resolvePBand = (value: string): { resolved: string; source: string } | null => { const lower = value.trim().toLowerCase(); if (!lower) return null; const match = ediBands.find((b) => b.value.toLowerCase() === lower); if (!match) return null; const bandName = match.bands?.[0]?.name; if (!bandName) return null; if (bandNames.size && !bandNames.has(bandName)) return null; return { resolved: bandName, source: match.value }; }; const handleFiles = (files: FileList | null) => { if (!files || files.length === 0) { setSelectedFiles(null); } else if (!isAuth) { const firstFile = new DataTransfer(); firstFile.items.add(files[0]); setSelectedFiles(firstFile.files); if (files.length > 1) { setError((t("upload_hint_anonymous_once") as string) || "Anonymně lze nahrát jen jeden soubor."); } } else { setSelectedFiles(files); } setSuccess(null); if (files && files.length <= 1) { setError(null); } setNormalizedFileName(null); setQsoCountWarning(null); setDownloadEdited(false); setSpoweInfo(null); setUnsupportedInfo(null); setIsEdiFile(true); setQsoCallsignInfo(null); if (files && files.length === 1) { const file = files[0]; readFileAsText(file) .then((text) => { if (!hasEdiHeader(text)) { setIsEdiFile(false); setUnsupportedInfo((t("upload_file_not_supported") as string) || ""); setSelectedFiles(null); return; } const earlyValidationErrors = validateEdi(text, file.name); const earlyQsoErrors = earlyValidationErrors.filter( (msg) => msg.includes("QSO #") && msg.toLowerCase().includes("volací znak") ); if (earlyQsoErrors.length > 0) { const recommendation = "Doporučení: neplatné volací znaky v QSO oprav v souboru a zkus ho nahrát znovu."; setQsoCallsignInfo([...earlyQsoErrors, recommendation].join("\n")); } const headers = extractHeaderMap(text); setQsoCountWarning(computeQsoCountWarning(text, file.name)); const spoweParsedFromFile = normalizeSpowe(headers["SPowe"] ?? ""); const pbandMapping = resolvePBand(headers["PBand"] ?? ""); const mappedPBand = pbandMapping?.resolved ?? headers["PBand"] ?? ""; if (pbandMapping) { setPbandInfo( (t("upload_pband_mapped", { original: headers["PBand"], mapped: mappedPBand }) as string) || `PBand ${headers["PBand"]} -> ${mappedPBand}` ); } else if (!mappedPBand) { setPbandInfo((t("upload_pband_missing") as string) || "PBand není vyplněné v souboru, vyber pásmo ze seznamu."); } else { setPbandInfo(null); } if (spoweParsedFromFile.changed) { setSpoweInfo( (t("upload_spowe_normalized_from", { from: headers["SPowe"] ?? "", to: spoweParsedFromFile.normalized, }) as string) || "" ); } else { setSpoweInfo(null); } setHeaderForm((prev) => ({ ...prev, TName: headers["TName"] ?? "", TDate: headers["TDate"] ?? "", PCall: headers["PCall"] ?? "", PWWLo: headers["PWWLo"] ?? "", PSect: headers["PSect"] ?? "", PBand: mappedPBand, RHBBS: headers["RHBBS"] ?? "", SPowe: spoweParsedFromFile.normalized, SAnte: headers["SAnte"] ?? "", RCall: headers["RCall"] ?? "", MOpe1: headers["MOpe1"] ?? headers["Mope1"] ?? "", MOpe2: headers["MOpe2"] ?? headers["Mope2"] ?? "", })); }) .catch(() => { // leave editing disabled if reading fails setHeaderForm((prev) => ({ ...prev, TName: "", TDate: "", PCall: "", PWWLo: "", PSect: "", PBand: "", RHBBS: "", SPowe: "", SAnte: "", RCall: "", MOpe1: "", MOpe2: "", })); }); } else { setQsoCountWarning(null); setQsoCallsignInfo(null); setHeaderForm((prev) => ({ ...prev, TName: "", TDate: "", PCall: "", PWWLo: "", PSect: "", PBand: "", RHBBS: "", SPowe: "", SAnte: "", RCall: "", MOpe1: "", MOpe2: "", })); setPbandInfo(null); } }; const markEdited = () => { setDownloadEdited(true); }; const handlePbandChange = (value: string) => { setHeaderForm((prev) => ({ ...prev, PBand: value })); markEdited(); setPbandInfo(null); }; const handleSpoweChange = (value: string) => { const parsed = normalizeSpowe(value); setHeaderForm((prev) => ({ ...prev, SPowe: parsed.normalized })); if (parsed.changed) { setSpoweInfo( (t("upload_spowe_normalized_to", { value: parsed.normalized }) as string) || `SPowe bylo normalizováno na "${parsed.normalized}".` ); } else { setSpoweInfo(null); } markEdited(); }; const handleApplyPsectCanonical = () => { if (!psectCanonical) { setError( (t("upload_error_psect_normalize") as string) || "PSect nelze normalizovat, oprav chyby a zkus to znovu." ); return; } setHeaderForm((prev) => ({ ...prev, PSect: psectCanonical })); setError(null); }; // Reset formuláře při změně kola (např. v RoundDetail) useEffect(() => { setSelectedFiles(null); setSuccess(null); setError(null); setNormalizedFileName(null); setQsoCountWarning(null); setPbandInfo(null); setSpoweInfo(null); setUnsupportedInfo(null); setIsEdiFile(true); setQsoCallsignInfo(null); setHeaderForm({ TName: "", TDate: "", PCall: "", PWWLo: "", PSect: "", PBand: "", RHBBS: "", SPowe: "", SAnte: "", RCall: "", MOpe1: "", MOpe2: "", }); }, [roundId]); useEffect(() => { if (normalizedFileName) { setNormalizedFileName(null); } }, [headerForm.PCall, headerForm.PBand, headerForm.PSect, selectedFiles, normalizedFileName]); const uploadFiles = async () => { if (!roundId) { setError((t("upload_error_missing_round") as string) || "Chybí ID kola."); return; } if (!selectedFiles || selectedFiles.length === 0) { setError((t("upload_error_pick_file") as string) || "Vyber soubor."); return; } const isSingleUpload = selectedFiles.length === 1; if (bands.length > 0 && bandMissing) { if (isSingleUpload) { setError((t("upload_error_pick_band") as string) || "Vyber pásmo (PBand) ze seznamu."); return; } } if (bands.length > 0 && bandUnknown) { if (isSingleUpload) { setError( (t("upload_error_band_unknown", { band: bandValue }) as string) || `PBand "${bandValue}" neodpovídá známým pásmům, vyber správnou hodnotu ze seznamu.` ); return; } } if (pwwloInvalid) { if (isSingleUpload) { setError( pwwloEmpty ? (t("upload_error_pwwlo_required") as string) || "PWWLo je povinné." : (t("upload_error_pwwlo_format") as string) || "PWWLo musí mít 6 znaků ve formátu lokátoru (AA00AA)." ); return; } } if (rhbbsInvalid && isSingleUpload) { setError(null); } if (tNameInvalid) { if (isSingleUpload) { setError((t("upload_error_tname_required") as string) || "TName je povinné."); return; } } if (pCallInvalid) { if (isSingleUpload) { setError( pCallEmpty ? (t("upload_error_pcall_required") as string) || "PCall je povinné." : (t("upload_error_pcall_format") as string) || "PCall musí být validní volací znak." ); return; } } if (tDateError) { if (isSingleUpload) { setError(tDateError); return; } } if (psectMissing || psectHasErrors) { if (isSingleUpload) { setError( psectMissing ? (t("upload_error_psect_required") as string) || "PSect je povinné." : psectHasErrors ? psectValidation.errors.join(" ") || (t("upload_error_psect_invalid") as string) || "PSect není validní." : undefined ); return; } } if (spoweInvalid) { if (isSingleUpload) { setError( spoweEmpty ? (t("upload_error_spowe_required") as string) || "SPowe je povinné." : spoweTooLong ? (t("upload_error_spowe_length") as string) || "SPowe může mít maximálně 12 znaků." : (t("upload_error_spowe_format") as string) || "SPowe musí být celé číslo (bez jednotek)." ); return; } } if (!spoweInvalid && spoweOverLimit) { if (isSingleUpload) { setError( (t("upload_error_spowe_limit", { value: spoweParsed.normalized, category: psectValidation.powerClass ?? "-", limit: powerLimit ?? "-", }) as string) || "" ); return; } } if (santeInvalid) { if (isSingleUpload) { setError((t("upload_error_sante_required") as string) || "SAnte je povinné."); return; } } if (rcallInvalid) { if (isSingleUpload) { setError( headerForm.RCall.trim() === "" ? (t("upload_error_rcall_required") as string) || "RCall je povinné pro zvolenou kategorii." : (t("upload_error_rcall_format") as string) || "RCall musí být validní volací znak." ); return; } } if (mopeInvalid) { if (isSingleUpload) { const invalidTokens = mopeValidation.invalidTokens.length > 0 ? ` Neplatné značky: ${mopeValidation.invalidTokens.join(", ")}.` : ""; const mopeMsg = (t("upload_error_mope_missing") as string) || "Vyplň alespoň MOpe1 nebo MOpe2 (můžeš zadat více značek oddělených mezerou/středníkem/čárkou)."; setError(`${mopeMsg}${invalidTokens}`); return; } } if (!canUpload || anonymousBlocked) { setError((t("upload_error_not_allowed") as string) || "Nahrávání není povoleno."); return; } setUploading(true); setError(null); setSuccess(null); try { await axios.get("/sanctum/csrf-cookie", { withCredentials: true }); const filesArray = Array.from(selectedFiles); const results: string[] = []; const errors: string[] = []; const qsoCallsignWarnings: string[] = []; for (const file of filesArray) { const outputName = filesArray.length === 1 && normalizedFileName ? normalizedFileName : file.name; let preparedFile = file; let editedContentForDownload: string | null = null; // klientská validace EDI + případná úprava hlavičky u jednoho souboru try { let text = await readFileAsText(file); const initialHeaders = extractHeaderMap(text); const spoweParsedInitial = normalizeSpowe(initialHeaders["SPowe"] ?? ""); const pbandMapping = resolvePBand(initialHeaders["PBand"] ?? ""); if (pbandMapping) { text = updateContentWithHeaders(text, { PBand: pbandMapping.resolved }); if (filesArray.length === 1) { setPbandInfo( (t("upload_pband_mapped", { original: initialHeaders["PBand"], mapped: pbandMapping.resolved, }) as string) || "" ); } } if (spoweParsedInitial.changed) { text = updateContentWithHeaders(text, { SPowe: spoweParsedInitial.normalized }); if (filesArray.length === 1) { setSpoweInfo( `SPowe bylo normalizováno z "${initialHeaders["SPowe"] ?? ""}" na "${spoweParsedInitial.normalized}".` ); } } if (filesArray.length === 1) { const updatedContent = updateContentWithHeaders(text, headerForm); text = updatedContent; preparedFile = new File([updatedContent], outputName, { type: file.type || "text/plain" }); setQsoCountWarning(computeQsoCountWarning(updatedContent, file.name)); editedContentForDownload = updatedContent; } else if (outputName !== file.name) { preparedFile = new File([file], outputName, { type: file.type || "text/plain" }); } const headers = extractHeaderMap(text); const psect = (headers["PSect"] ?? "").trim(); const pband = (headers["PBand"] ?? "").trim(); const pcallValidation = validateCallsign(headers["PCall"] ?? ""); if (!pcallValidation.isValid) { errors.push( `${file.name}: ${(t("upload_error_pcall_format") as string) || "PCall musí být validní volací znak."}` ); continue; } if (psect && (sectionNeedsSingle || sectionNeedsMulti)) { const rcallValidation = validateCallsign(headers["RCall"] ?? ""); if (!rcallValidation.isValid) { errors.push( `${file.name}: ${(t("upload_error_rcall_format") as string) || "RCall musí být validní volací znak."}` ); continue; } } if (sectionNeedsMulti) { const mope1Val = validateCallsignList(headers["MOpe1"] ?? ""); const mope2Val = validateCallsignList(headers["MOpe2"] ?? ""); const invalidTokens = [...mope1Val.invalidTokens, ...mope2Val.invalidTokens]; if (!mope1Val.hasValid && !mope2Val.hasValid) { errors.push( `${file.name}: ${(t("upload_error_mope_missing") as string) || "Vyplň alespoň MOpe1 nebo MOpe2 (můžeš zadat více značek oddělených mezerou/středníkem/čárkou)." }` ); continue; } if (invalidTokens.length > 0) { errors.push( `${file.name}: ${(t("upload_error_mope_invalid", { invalid: invalidTokens.join(", ") }) as string) || `Neplatné značky v MOpe1/MOpe2 (${invalidTokens.join(", ")}).`}` ); continue; } } const psectResult = parsePSect(psect, { strict: true, allow6h: allowSixHour }); if (psectResult.errors.length > 0) { errors.push(`${file.name}: ${psectResult.errors.join(" ")}`); continue; } if (psect && hasPowerKeyword(psect)) { const bandMatch = bands.find((b) => b.name === pband); const allowsPower = !!bandMatch?.has_power_category; if (!allowsPower) { errors.push( `${file.name}: ${(t("upload_error_power_band_mismatch", { psect, band: pband || "?", }) as string) || ""}` ); continue; } } const validationErrors = validateEdi(text, file.name); const qsoCallsignErrors = validationErrors.filter( (msg) => msg.includes("QSO #") && msg.toLowerCase().includes("volací znak") ); const otherErrors = validationErrors.filter( (msg) => !qsoCallsignErrors.includes(msg) && !msg.includes("Záznamy QSO nelze upravit") ); if (qsoCallsignErrors.length > 0) { qsoCallsignWarnings.push( ...qsoCallsignErrors, "Doporučení: neplatné volací znaky v QSO oprav v souboru a zkus ho nahrát znovu." ); } if (otherErrors.length > 0) { errors.push(...otherErrors); continue; } } catch (readErr) { errors.push( `${file.name}: ${(t("upload_error_file_read") as string) || "Soubor se nepodařilo přečíst."}` ); continue; } try { const formData = new FormData(); formData.append("file", preparedFile); formData.append("round_id", String(roundId)); const res = await axios.post("/api/files", formData, { headers: { "Content-Type": "multipart/form-data", Accept: "application/json" }, withCredentials: true, withXSRFToken: true, }); const stored = res.data; results.push(stored?.filename ?? outputName); if (filesArray.length === 1 && downloadEdited && editedContentForDownload) { const blob = new Blob([editedContentForDownload], { type: preparedFile.type || "text/plain" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = outputName; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } } catch (apiErr: any) { const msg = apiErr?.response?.data?.message || apiErr?.response?.data?.errors?.file?.[0] || `${file.name}: ${(t("upload_failed") as string) || "Nahrávání se nezdařilo."}`; errors.push(msg); } } if (errors.length > 0) { setError(errors.join("\n")); } if (qsoCallsignWarnings.length > 0) { setQsoCallsignInfo(qsoCallsignWarnings.join("\n")); } if (results.length > 0) { setSuccess( (t("files_uploaded", { files: results.join(", ") }) as string) || `Nahráno: ${results.join(", ")}` ); setSelectedFiles(null); if (!isAuth) { setUploadedOnce(true); } onUploaded?.(); } } catch (e: any) { const message = e?.response?.data?.message || e?.response?.data?.errors?.file?.[0] || (t("upload_failed") as string) || "Nahrávání se nezdařilo."; setError(message); } finally { setUploading(false); } }; const helperText = (() => { if (isAuth) { return ""; } if (!windowOpen) { return ( (t("upload_hint_closed") as string) || "Nahrávání je dostupné během závodu do uzávěrky logů, nebo se přihlas." ); } if (uploadedOnce) { return (t("upload_hint_anonymous_once") as string) || "Anonymně lze nahrát jen jeden soubor."; } return ( (t("upload_hint_anonymous") as string) || "Anonymně lze nahrát jeden soubor během závodu do uzávěrky logů." ); })(); // Odvozené validace podle vstupů a zvolené kategorie const sectionNeedsSingle = isSingleOp(headerForm.PSect || ""); const sectionNeedsMulti = isMultiOp(headerForm.PSect || ""); const spoweRaw = headerForm.SPowe; const spoweEmpty = spoweParsed.normalized === ""; const spoweTooLong = spoweRaw.trim().length > 12; const spoweInvalid = !spoweParsed.valid || spoweTooLong; const santeValue = headerForm.SAnte.trim(); const santeTooLong = santeValue.length > 12; const santeInvalid = santeValue === ""; const rcallInvalid = (sectionNeedsSingle || sectionNeedsMulti) && (headerForm.RCall.trim() === "" || !rCallValidation.isValid); const mopeValidation = useMemo(() => { const first = validateCallsignList(headerForm.MOpe1); const second = validateCallsignList(headerForm.MOpe2); const invalidTokens = [...first.invalidTokens, ...second.invalidTokens]; const hasAnyValid = first.hasValid || second.hasValid; const hasAny = first.tokens.length > 0 || second.tokens.length > 0; return { invalidTokens, hasAnyValid, hasAny }; }, [headerForm.MOpe1, headerForm.MOpe2]); const mopeMissing = sectionNeedsMulti && !mopeValidation.hasAny; const mopeInvalidTokens = sectionNeedsMulti && mopeValidation.invalidTokens.length > 0; const mopeInvalid = sectionNeedsMulti && (!mopeValidation.hasAnyValid || mopeValidation.invalidTokens.length > 0); const powerLimit = useMemo(() => { switch (psectValidation.powerClass) { case "QRP": return 5; case "N": return 10; case "LP": return 100; default: return null; // A/HP nebo chybějící } }, [psectValidation.powerClass]); const spoweOverLimit = powerLimit !== null && spoweParsed.valid && spoweParsed.normalized !== "" && parseFloat(spoweParsed.normalized) > powerLimit; const spoweLimitError = powerLimit !== null && spoweOverLimit ? ((t("upload_error_spowe_limit", { value: spoweParsed.normalized, category: psectValidation.powerClass ?? "-", limit: powerLimit ?? "-", }) as string) || "") : null; if (!canUpload) return null; return (
{t("upload_log_title") ?? "Nahrát log"} {helperText}
{selectedFiles && selectedFiles.length > 1 && (
Hromadný upload: hlavičku nelze upravovat, soubory se odešlou tak jak jsou.
)} {selectedFiles && selectedFiles.length === 1 && isEdiFile && (
{fileNameStatus && (

{fileNameStatus.message}

{fileNameStatus.state === "invalid" && fileNameStatus.expectedName && ( )}
{normalizedFileName && (

{(t("upload_filename_used", { name: normalizedFileName }) as string) || `Při uploadu se použije: ${normalizedFileName}`}

)}
)}
)}
); }