import { PWWLO_REGEX, normalizeContent, validateCallsign, validateCallsignList, isMultiOp, isSingleOp, } from "@/utils/ediValidation"; import i18n from "@/i18n"; import { type EdiHeaderForm } from "@/types/edi"; const translate = (key: string, options?: Record, fallback?: string) => { const value = i18n.t(key, options); if (fallback && (value === key || value === "")) return fallback; return value as string; }; export const validateEdi = (content: string, name: string): string[] => { const errors: string[] = []; const lines = normalizeContent(content).split("\n"); const nonEmpty = lines.map((l) => l.trim()).filter((l) => l.length > 0); let tDateStart: number | null = null; let tDateEnd: number | null = null; let tDateStartYear: number | null = null; let tDateEndYear: number | null = null; let tDateValid = false; if (nonEmpty.length === 0) { errors.push( translate("edi_error_file_empty", { name }, `${name}: soubor je prázdný.`) ); return errors; } if (!nonEmpty[0].startsWith("[REG1TEST")) { errors.push( translate( "edi_error_missing_header", { name }, `${name}: chybí hlavička [REG1TEST;1].` ) ); } const headerMap: Record = {}; let remarksStart = -1; let qsoStart = -1; nonEmpty.forEach((line, idx) => { if (line.startsWith("[Remarks")) remarksStart = idx; if (line.startsWith("[QSORecords")) qsoStart = idx; const m = line.match(/^([A-Za-z0-9]+)=(.*)$/); if (m) { headerMap[m[1]] = m[2].trim(); } }); // Required header fields const requiredFields: Array<[keyof EdiHeaderForm, string]> = [ ["TName", translate("edi_field_tname", undefined, "název soutěže (TName)")], ["TDate", translate("edi_field_tdate", undefined, "datum závodu (TDate=YYYYMMDD;YYYYMMDD)")], ["PCall", translate("edi_field_pcall", undefined, "volací znak (PCall)")], ["PWWLo", translate("edi_field_pwwlo", undefined, "lokátor (PWWLo)")], ["PSect", translate("edi_field_psect", undefined, "sekce/kategorie (PSect)")], ["PBand", translate("edi_field_pband", undefined, "použité pásmo (PBand)")], ["SPowe", translate("edi_field_spowe", undefined, "výkon (SPowe)")], ["SAnte", translate("edi_field_sante", undefined, "anténa (SAnte)")], ["CQSOP", translate("edi_field_cqsop", undefined, "QSO body (CQSOP)")], ["CToSc", translate("edi_field_ctosc", undefined, "celkové body (CToSc)")], ]; requiredFields.forEach(([key, label]) => { if (!headerMap[key] || headerMap[key].trim() === "") { errors.push( translate( "edi_error_missing_field", { name, field: label }, `${name}: chybí ${label}.` ) ); } }); if (headerMap["TDate"]) { const parts = headerMap["TDate"].split(";"); const invalidCount = parts.length !== 2; const invalidFormat = parts.some((p) => !/^\d{8}$/.test(p)); const invertedRange = !invalidCount && !invalidFormat && parseInt(parts[0], 10) > parseInt(parts[1], 10); if (invalidCount || invalidFormat || invertedRange) { errors.push( translate( "edi_error_tdate_format", { name }, `${name}: TDate musí být ve formátu YYYYMMDD;YYYYMMDD a první datum nesmí být větší než druhé.` ) ); } else { tDateStart = parseInt(parts[0], 10); tDateEnd = parseInt(parts[1], 10); tDateStartYear = parseInt(parts[0].slice(0, 4), 10); tDateEndYear = parseInt(parts[1].slice(0, 4), 10); tDateValid = true; } } if (headerMap["PWWLo"]) { const normalizedPwwlo = headerMap["PWWLo"].trim().toUpperCase(); if (!PWWLO_REGEX.test(normalizedPwwlo)) { errors.push( translate( "edi_error_pwwlo_format", { name }, `${name}: PWWLo musí mít 6 znaků ve formátu lokátoru (AA00AA).` ) ); } headerMap["PWWLo"] = normalizedPwwlo; } if (headerMap["RHBBS"] && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(headerMap["RHBBS"])) { // měkká validace, řeší se upozorněním ve formuláři } const pcallValidation = validateCallsign(headerMap["PCall"] ?? ""); if (!pcallValidation.isValid) { errors.push( translate( "edi_error_pcall_format", { name }, `${name}: PCall musí být validní volací znak.` ) ); } else { headerMap["PCall"] = pcallValidation.normalized; } const section = headerMap["PSect"] ?? ""; const requiresSingle = isSingleOp(section); const requiresMulti = isMultiOp(section); if (requiresSingle || requiresMulti) { const rcallValidation = validateCallsign(headerMap["RCall"] ?? ""); if (!rcallValidation.isValid) { errors.push( translate( "edi_error_rcall_for_psect", { name, psect: section }, `${name}: chybí nebo je neplatné RCall pro PSect ${section}.` ) ); } if (requiresMulti) { const mope1Val = validateCallsignList(headerMap["MOpe1"] ?? ""); const mope2Val = validateCallsignList(headerMap["MOpe2"] ?? ""); const hasAnyValid = mope1Val.hasValid || mope2Val.hasValid; const invalidTokens = [...mope1Val.invalidTokens, ...mope2Val.invalidTokens]; if (!hasAnyValid) { errors.push( translate( "edi_error_mope_missing", { name }, `${name}: chybí MOpe1/MOpe2 pro multi operátory.` ) ); } if (invalidTokens.length > 0) { errors.push( translate( "edi_error_mope_invalid", { name, invalid: invalidTokens.join(", ") }, `${name}: neplatné značky v MOpe1/MOpe2 (${invalidTokens.join(", ")}).` ) ); } } } if (qsoStart === -1) { errors.push( translate( "edi_error_missing_qso_records", { name }, `${name}: chybí sekce [QSORecords;N].` ) ); return errors; } const qsoHeader = nonEmpty[qsoStart]; const m = qsoHeader.match(/^\[QSORecords;(\d+)\]/); const expectedCount = m ? parseInt(m[1], 10) : null; const qsoLines: string[] = []; for (let i = qsoStart + 1; i < nonEmpty.length; i++) { const line = nonEmpty[i]; if (line.startsWith("[END")) break; if (line.startsWith("[") && !line.startsWith("[QSORecords")) break; qsoLines.push(line); } if (expectedCount !== null && qsoLines.length !== expectedCount) { // upozornění řeší computeQsoCountWarning, tady neblokujeme upload } let hasQsoValidationError = false; qsoLines.forEach((line, idx) => { const parts = line.split(";"); if (parts.length < 10) { errors.push( translate( "edi_error_qso_fields_min", { name, index: idx + 1, min: 10 }, `${name}: QSO #${idx + 1} má málo polí (minimálně 10 očekáváno).` ) ); hasQsoValidationError = true; return; } const date = parts[0] ?? ""; const time = parts[1] ?? ""; const call = parts[2] ?? ""; if (!/^\d{6}$/.test(date)) { errors.push( translate( "edi_error_qso_date_format", { name, index: idx + 1 }, `${name}: QSO #${idx + 1} má neplatné datum (YYMMDD).` ) ); hasQsoValidationError = true; } else if (tDateValid && tDateStart !== null && tDateEnd !== null) { const yy = parseInt(date.slice(0, 2), 10); const mmdd = date.slice(2); let fullYear: number | null = null; if (tDateStartYear !== null && tDateEndYear !== null) { if (tDateStartYear === tDateEndYear) { fullYear = tDateStartYear; } else if (yy === tDateStartYear % 100) { fullYear = tDateStartYear; } else if (yy === tDateEndYear % 100) { fullYear = tDateEndYear; } } if (fullYear === null) { errors.push( translate( "edi_error_qso_year_out_of_range", { name, index: idx + 1 }, `${name}: QSO #${idx + 1} má rok mimo rozsah TDate.` ) ); hasQsoValidationError = true; } else { const fullDate = parseInt(`${fullYear}${mmdd}`, 10); if (fullDate < tDateStart || fullDate > tDateEnd) { errors.push( translate( "edi_error_qso_date_out_of_range", { name, index: idx + 1 }, `${name}: QSO #${idx + 1} má datum mimo rozsah TDate.` ) ); hasQsoValidationError = true; } } } if (!/^\d{4}$/.test(time)) { errors.push( translate( "edi_error_qso_time_format", { name, index: idx + 1 }, `${name}: QSO #${idx + 1} má neplatný čas (HHMM UTC).` ) ); hasQsoValidationError = true; } const qsoCallValidation = validateCallsign(call); if (!qsoCallValidation.isValid) { errors.push( translate( "edi_error_qso_callsign", { name, index: idx + 1 }, `${name}: QSO #${idx + 1} má neplatný volací znak.` ) ); hasQsoValidationError = true; } }); if (hasQsoValidationError) { errors.push( translate( "edi_error_qso_edit_in_file", { name }, `${name}: Záznamy QSO nelze upravit ve formuláři, oprav je v souboru a zkus ho nahrát znovu.` ) ); } return errors; };