@@ -598,6 +598,113 @@ async function registerOrder(req, res, next) {
}
}
}
}
// ── Empfänger-Datei einlesen (XLSX/CSV) nach Spaltennamen ────────────────────
// Spiegelt die Logik der Web-App (skrift-app/src/lib/excel.ts): feste Spalten,
// „Zeile N" → Freitext, unbekannte Spalten → Platzhalter.
const EMPF _SPALTEN = {
anrede : 'salutation' , vorname : 'first_name' , nachname : 'last_name' , name : 'last_name' ,
strasse : 'street' , 'straße' : 'street' , nr : 'house_no' , hausnummer : 'house_no' ,
plz : 'zip' , ort : 'city' , stadt : 'city' , land : 'country' ,
} ;
const normSpalte = ( s ) => String ( s || '' ) . trim ( ) . toLowerCase ( ) . replace ( /\s+/g , '' ) ;
function zelleText ( v ) {
if ( v === null || v === undefined ) return '' ;
if ( v instanceof Date ) return v . toISOString ( ) . slice ( 0 , 10 ) ;
if ( typeof v === 'object' ) {
if ( v . text !== undefined ) return String ( v . text ) ;
if ( v . result !== undefined ) return String ( v . result ) ;
if ( v . richText ) return v . richText . map ( ( t ) => t . text ) . join ( '' ) ;
if ( v . hyperlink !== undefined ) return String ( v . text || v . hyperlink ) ;
return '' ;
}
return String ( v ) ;
}
// Robuster CSV/TSV-Parser (Trenner-Erkennung, Anführungszeichen, "" = ").
function parseCsvText ( text ) {
const erste = String ( text ) . replace ( /\r\n?/g , '\n' ) . replace ( /^ / , '' ) . split ( '\n' ) [ 0 ] || '' ;
const anz = ( d ) => erste . split ( d ) . length - 1 ;
const delim = anz ( '\t' ) > 0 ? '\t' : ( ( anz ( ';' ) > 0 && anz ( ';' ) >= anz ( ',' ) ) ? ';' : ',' ) ;
const s = String ( text ) . replace ( /\r\n?/g , '\n' ) . replace ( /^ / , '' ) ;
const rows = [ ] ; let row = [ ] ; let cur = '' ; let q = false ;
for ( let i = 0 ; i < s . length ; i ++ ) {
const c = s [ i ] ;
if ( q ) { if ( c === '"' ) { if ( s [ i + 1 ] === '"' ) { cur += '"' ; i ++ ; } else { q = false ; } } else { cur += c ; } }
else if ( c === '"' ) { q = true ; }
else if ( c === delim ) { row . push ( cur ) ; cur = '' ; }
else if ( c === '\n' ) { row . push ( cur ) ; rows . push ( row ) ; row = [ ] ; cur = '' ; }
else { cur += c ; }
}
if ( cur !== '' || row . length ) { row . push ( cur ) ; rows . push ( row ) ; }
return rows . filter ( ( r ) => r . some ( ( c ) => String ( c ) . trim ( ) !== '' ) ) ;
}
async function tabelleAusDatei ( buffer , filename ) {
const ext = String ( filename || '' ) . toLowerCase ( ) ;
const istCsv = ext . endsWith ( '.csv' ) || ext . endsWith ( '.tsv' ) || ext . endsWith ( '.txt' ) ;
if ( ! istCsv ) {
try {
const ExcelJS = require ( 'exceljs' ) ;
const wb = new ExcelJS . Workbook ( ) ;
await wb . xlsx . load ( buffer ) ;
const ws = wb . worksheets [ 0 ] ;
if ( ws ) {
const breite = ws . actualColumnCount || ws . columnCount || 0 ;
const rows = [ ] ;
ws . eachRow ( { includeEmpty : false } , ( zeile ) => {
const a = [ ] ;
for ( let c = 1 ; c <= breite ; c ++ ) a . push ( zelleText ( zeile . getCell ( c ) . value ) ) ;
rows . push ( a ) ;
} ) ;
if ( rows . length ) return rows ;
}
} catch ( e ) { /* keine gültige XLSX → als CSV versuchen */ }
}
return parseCsvText ( buffer . toString ( 'utf8' ) ) ;
}
/**
* POST /api/order/parse-recipients
* Body: { data_base64, filename }. Antwort: { entries, columns, count }.
*/
async function parseRecipients ( req , res , next ) {
try {
const b64 = String ( ( req . body && req . body . data _base64 ) || '' ) . replace ( /^data:[^,]*,/ , '' ) ;
const filename = ( req . body && req . body . filename ) || '' ;
if ( ! b64 ) return res . status ( 400 ) . json ( { error : 'Keine Datei übermittelt.' } ) ;
const buffer = Buffer . from ( b64 , 'base64' ) ;
if ( ! buffer . length ) return res . status ( 400 ) . json ( { error : 'Leere Datei.' } ) ;
const tab = await tabelleAusDatei ( buffer , filename ) ;
if ( tab . length < 2 ) return res . status ( 422 ) . json ( { error : 'Keine Datenzeilen – Datei braucht eine Kopfzeile mit Spaltennamen und mindestens einen Empfänger.' } ) ;
const kopf = tab [ 0 ] . map ( ( h ) => String ( h || '' ) . trim ( ) ) ;
const erkannt = new Set ( ) ;
const entries = [ ] ;
for ( let r = 1 ; r < tab . length ; r ++ ) {
const zellen = tab [ r ] ;
const e = { salutation : '' , first _name : '' , last _name : '' , street : '' , house _no : '' , zip : '' , city : '' , country : '' , free _text : '' , placeholders : { } } ;
const freitextZeilen = [ ] ;
kopf . forEach ( ( spalte , ci ) => {
if ( ! spalte ) return ;
const wert = String ( zellen [ ci ] ? ? '' ) . trim ( ) ;
const zm = /^zeile\s*(\d+)$/i . exec ( spalte ) ;
const feld = EMPF _SPALTEN [ normSpalte ( spalte ) ] ;
if ( zm ) { freitextZeilen [ Number ( zm [ 1 ] ) - 1 ] = wert ; erkannt . add ( 'Zeile ' + zm [ 1 ] ) ; }
else if ( feld ) { e [ feld ] = wert ; erkannt . add ( spalte ) ; }
else if ( wert ) { e . placeholders [ spalte ] = wert ; erkannt . add ( spalte ) ; }
} ) ;
if ( freitextZeilen . length ) e . free _text = freitextZeilen . map ( ( z ) => z || '' ) . join ( '\n' ) . replace ( /\n+$/ , '' ) ;
entries . push ( e ) ;
}
return res . json ( { entries , columns : [ ... erkannt ] , count : entries . length } ) ;
} catch ( err ) {
console . error ( '[Order] parseRecipients:' , err ) ;
return res . status ( 500 ) . json ( { error : 'Datei konnte nicht gelesen werden.' } ) ;
}
}
module . exports = {
module . exports = {
finalizeOrder ,
finalizeOrder ,
generateOrder ,
generateOrder ,
@@ -605,4 +712,5 @@ module.exports = {
uploadMotif ,
uploadMotif ,
uploadOrderFile ,
uploadOrderFile ,
registerOrder ,
registerOrder ,
parseRecipients ,
} ;
} ;