feat(leads): CSV-Import für Leads
ci / quality (push) Failing after 1m11s
ci / build (push) Has been skipped

"CSV importieren"-Dialog auf /leads. Serverseitiger lead.importCsv:
eigener CSV-Parser (Quotes/Kommata/Zeilenumbrüche, mit Tests),
header-basiertes Mapping (umlaut-tolerant). Pro Zeile entsteht ein Lead
(Adresse → standorte) + primärer Kontakt; Notizen werden als erster
Austausch-Log-Eintrag gespeichert. Dedup über Firmenname.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-05 18:17:50 +02:00
1 parent 864a1cef6a
commit 9f3ee13094
5 files changed
+329 -1

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import { parseCsv, normalizeHeader } from './csv.js';
describe('parseCsv', () => {
it('parst einfache Zeilen', () => {
expect(parseCsv('a,b,c\n1,2,3')).toEqual([
['a', 'b', 'c'],
['1', '2', '3'],
]);
});
it('ignoriert einen abschließenden Zeilenumbruch', () => {
expect(parseCsv('a,b\n1,2\n')).toEqual([
['a', 'b'],
['1', '2'],
]);
});
it('behandelt \\r\\n', () => {
expect(parseCsv('a,b\r\n1,2')).toEqual([
['a', 'b'],
['1', '2'],
]);
});
it('respektiert Anführungszeichen mit Komma und Quote-Escape', () => {
expect(parseCsv('a,b\n"x, y","sagt ""hi"""')).toEqual([
['a', 'b'],
['x, y', 'sagt "hi"'],
]);
});
it('erlaubt Zeilenumbruch innerhalb von Quotes', () => {
expect(parseCsv('a,b\n"erste\nzweite",2')).toEqual([
['a', 'b'],
['erste\nzweite', '2'],
]);
});
it('behält leere Felder', () => {
expect(parseCsv('a,b,c\n1,,3')).toEqual([
['a', 'b', 'c'],
['1', '', '3'],
]);
});
});
describe('normalizeHeader', () => {
it('normalisiert Umlaute und Sonderzeichen', () => {
expect(normalizeHeader('E-Mail')).toBe('email');
expect(normalizeHeader('Straße')).toBe('strasse');
expect(normalizeHeader(' Quelle ')).toBe('quelle');
});
});
+54
View File
@@ -0,0 +1,54 @@
// Minimaler, robuster CSV-Parser (RFC-4180-nah): unterstützt Anführungszeichen,
// eingebettete Kommata/Zeilenumbrüche und doppelte Quotes ("") als Escape.
export function parseCsv(input: string): string[][] {
const text = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const rows: string[][] = [];
let row: string[] = [];
let field = '';
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQuotes) {
if (c === '"') {
if (text[i + 1] === '"') {
field += '"';
i++;
} else {
inQuotes = false;
}
} else {
field += c;
}
} else if (c === '"') {
inQuotes = true;
} else if (c === ',') {
row.push(field);
field = '';
} else if (c === '\n') {
row.push(field);
rows.push(row);
row = [];
field = '';
} else {
field += c;
}
}
// Letztes Feld / letzte Zeile (falls Datei nicht mit \n endet).
if (field !== '' || row.length > 0) {
row.push(field);
rows.push(row);
}
return rows;
}
// Header-Zelle auf einen vergleichbaren Schlüssel normalisieren
// ("E-Mail" → "email", "Straße" → "strasse").
export function normalizeHeader(h: string): string {
return h
.trim()
.toLowerCase()
.replace(/ß/g, 'ss')
.replace(/[^a-z0-9]/g, '');
}