1f85277a3b
- neue Seitenleisten-Kategorie accounting (Finanzbuchhaltung / Financial accounting) - CSV (UTF-8, UTF-8 mit BOM, Windows-1252) pruefen, Vorschau mit Zeilenfehlern, DATEV-Lohn-ASCII-Export - Beraternummer, Mandantennummer, Lohnart je Mandant als Admin-Einstellung (KantineDatevConfig mit Zeilenschutz), anfangs leer - hochgeladene Daten werden nicht gespeichert; Download per Blob (auch im Desktop-Client) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
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<string>();
|
|
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 };
|
|
}
|