import type { KantinenRawRow, ValidationError, ValidationResult, ValidationWarning, } from './kantine-datev.types'; /** Zahl im deutschen Format (Komma als Dezimaltrenner), wie in der Vorlage. */ export function isValidGermanNumber(value: string): boolean { return /^\d+([,]\d+)?$/.test(value.trim()); } /** Datum im Format TT.MM.JJJJ. */ function isValidDate(value: string): boolean { return /^\d{2}\.\d{2}\.\d{4}$/.test(value.trim()); } function extractMonthYear(dateStr: string): { month: string; year: string } | null { const match = dateStr.trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/); if (!match) return null; return { month: match[2], year: match[3] }; } /** * Validiert die geparsten Kantinen-Zeilen (Regeln und Meldungen wie in der * Desktop-Vorlage): * 1. Personalnummer: vorhanden und numerisch * 2. Betrag: vorhanden, deutsches Zahlenformat * 3. Abrechnung von/bis: Format TT.MM.JJJJ * 4. von und bis muessen im selben Monat liegen * 5. Abrechnungsmonat kommt aus "Abrechnung bis" -> MM/YYYY */ export function validateKantinenData(rows: KantinenRawRow[]): ValidationResult { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; const detectedMonths = new Set(); let abrechnungsMonat: string | null = null; for (let i = 0; i < rows.length; i++) { const row = rows[i]; // Echte Dateizeile, falls bekannt; sonst 1-basiert + 1 fuer den Kopf. const rowNum = row.line ?? i + 2; if (!row.personalNr || row.personalNr.trim() === '') { errors.push({ row: rowNum, field: 'personalNr', code: 'personalNrMissing', message: 'Personalnummer fehlt', }); } else if (!/^\d+$/.test(row.personalNr.trim())) { errors.push({ row: rowNum, field: 'personalNr', code: 'personalNrNotNumeric', message: 'Personalnummer muss numerisch sein', }); } if (!row.betrag || row.betrag.trim() === '') { errors.push({ row: rowNum, field: 'betrag', code: 'betragMissing', message: 'Betrag fehlt' }); } else if (!isValidGermanNumber(row.betrag)) { errors.push({ row: rowNum, field: 'betrag', code: 'betragFormat', message: 'Betrag muss im deutschen Zahlenformat vorliegen (Komma als Dezimaltrenner)', }); } if (!row.abrechnungVon || row.abrechnungVon.trim() === '') { errors.push({ row: rowNum, field: 'abrechnungVon', code: 'vonMissing', message: 'Abrechnung von fehlt', }); } else if (!isValidDate(row.abrechnungVon)) { errors.push({ row: rowNum, field: 'abrechnungVon', code: 'vonFormat', message: 'Abrechnung von muss im Format TT.MM.JJJJ vorliegen', }); } if (!row.abrechnungBis || row.abrechnungBis.trim() === '') { errors.push({ row: rowNum, field: 'abrechnungBis', code: 'bisMissing', message: 'Abrechnung bis fehlt', }); } else if (!isValidDate(row.abrechnungBis)) { errors.push({ row: rowNum, field: 'abrechnungBis', code: 'bisFormat', message: 'Abrechnung bis muss im Format TT.MM.JJJJ vorliegen', }); } const vonParsed = extractMonthYear(row.abrechnungVon); const bisParsed = extractMonthYear(row.abrechnungBis); if (vonParsed && bisParsed) { if (vonParsed.month !== bisParsed.month || vonParsed.year !== bisParsed.year) { errors.push({ row: rowNum, field: 'abrechnungVon/abrechnungBis', code: 'multiMonthRange', message: 'Abrechnungszeitraum erstreckt sich über mehrere Monate', }); } } if (bisParsed) { detectedMonths.add(`${bisParsed.month}/${bisParsed.year}`); } } if (detectedMonths.size === 1) { abrechnungsMonat = [...detectedMonths][0]; } else if (detectedMonths.size > 1) { const months = [...detectedMonths]; warnings.push({ code: 'multipleMonths', message: `Verschiedene Abrechnungsmonate erkannt: ${months.join(', ')}. ` + 'Alle Zeilen sollten im selben Abrechnungsmonat liegen.', params: { months }, }); // Fallback wie in der Vorlage: der erste erkannte Monat. abrechnungsMonat = months[0]; } return { isValid: errors.length === 0, errors, warnings, abrechnungsMonat }; }