@@ -5,33 +5,48 @@
* Unterschriften-Service) zu EINER PDF zusammen, die am Bürodrucker in einem
* Rutsch gedruckt werden kann. Reihenfolge = Auftragsnummer (aufsteigend), damit
* sie zur Kuvert-Reihenfolge im Plotter passt.
*
* Dazu passend: Excel-Export aller Adressen in EXAKT derselben Reihenfolge wie
* das Sammel-PDF (gleiche Auftragsabfrage, gleiche Datei-Reihenfolge).
*/
const { PDFDocument } = require ( 'pdf-lib' ) ;
const ExcelJS = require ( 'exceljs' ) ;
const directus = require ( '../../services/directus-service' ) ;
/** Druckdateien eines Auftrags in Stapel-Reihenfolge (geteilt von PDF + Excel). */
function dateienVon ( order ) {
const sf = Array . isArray ( order . source _files ) ? order . source _files . filter ( Boolean ) : [ ] ;
if ( sf . length ) return sf ;
return order . source _file ? [ order . source _file ] : [ ] ;
}
/** Ausgewählte Aufträge laden – sortiert nach Auftragsnummer (wie bisher). */
async function ladeAuftraege ( req , res ) {
const ids = Array . isArray ( req . body ? . orderIds ) ? req . body . orderIds . filter ( ( v ) => v != null ) : [ ] ;
if ( ! ids . length ) {
res . status ( 400 ) . json ( { error : 'Invalid request' , message : 'orderIds (array) is required.' } ) ;
return null ;
}
const orders = await directus . fetchOrdersMinimal ( ids ) ;
if ( ! orders || ! orders . length ) {
res . status ( 404 ) . json ( { error : 'Not found' , message : 'Keine Aufträge gefunden.' } ) ;
return null ;
}
return orders ;
}
/**
* POST /api/charge/merge-pdf
* Body: { orderIds: (number|string)[] } → application/pdf (zusammengeführt)
*/
async function mergePdf ( req , res , next ) {
try {
const id s = Array . isArray ( req . body ? . orderIds ) ? req . body . orderIds . filter ( ( v ) => v != null ) : [ ] ;
if ( ! ids . length ) {
return res . status ( 400 ) . json ( { error : 'Invalid request' , message : 'orderIds (array) is required.' } ) ;
}
const orders = await directus . fetchOrdersMinimal ( ids ) ;
if ( ! orders || ! orders . length ) {
return res . status ( 404 ) . json ( { error : 'Not found' , message : 'Keine Aufträge gefunden.' } ) ;
}
const order s = await ladeAuftraege ( req , res ) ;
if ( ! orders ) return undefined ;
// Datei-IDs in Auftragsnummern-Reihenfolge (orders ist bereits sortiert).
const fileIds = [ ] ;
for ( const o of orders ) {
const sf = Array . isArray ( o . source _files ) ? o . source _files . filter ( Boolean ) : [ ] ;
if ( sf . length ) fileIds . push ( ... sf ) ;
else if ( o . source _file ) fileIds . push ( o . source _file ) ;
}
for ( const o of orders ) fileIds . push ( ... dateienVon ( o ) ) ;
if ( ! fileIds . length ) {
return res . status ( 400 ) . json ( { error : 'No files' , message : 'Keine Druckdateien an den Aufträgen.' } ) ;
}
@@ -66,4 +81,103 @@ async function mergePdf(req, res, next) {
}
}
module . exports = { mergePdf } ;
const seitenText = ( von , bis ) => ( bis < von ? '' : ( von === bis ? String ( von ) : ` ${ von } – ${ bis } ` ) ) ;
/**
* POST /api/charge/merge-xlsx
* Body: { orderIds: (number|string)[] } → .xlsx (Adressen in Sammel-PDF-Reihenfolge)
*
* Je Empfänger eine Zeile. Aufträge in derselben Reihenfolge wie merge-pdf,
* innerhalb des Auftrags nach Briefnummer. „PDF-Seiten" zeigt, wo die Zeile im
* Sammel-PDF liegt: je Brief, wenn der Auftrag genau eine PDF je Empfänger hat,
* sonst den Seitenbereich des ganzen Auftrags.
*/
async function mergeXlsx ( req , res , next ) {
try {
const orders = await ladeAuftraege ( req , res ) ;
if ( ! orders ) return undefined ;
const entries = await directus . fetchEntriesForOrders ( orders . map ( ( o ) => o . id ) ) ;
const proAuftrag = new Map ( ) ;
for ( const e of entries ) {
const k = String ( e . order ) ;
if ( ! proAuftrag . has ( k ) ) proAuftrag . set ( k , [ ] ) ;
proAuftrag . get ( k ) . push ( e ) ;
}
const wb = new ExcelJS . Workbook ( ) ;
const ws = wb . addWorksheet ( 'Adressen' ) ;
ws . columns = [
{ header : 'Nr.' , key : 'nr' , width : 6 } ,
{ header : 'Auftragsnummer' , key : 'auftrag' , width : 16 } ,
{ header : 'Briefnummer' , key : 'brief' , width : 11 } ,
{ header : 'Anrede' , key : 'salutation' , width : 12 } ,
{ header : 'Vorname' , key : 'first_name' , width : 16 } ,
{ header : 'Nachname' , key : 'last_name' , width : 18 } ,
{ header : 'Straße' , key : 'street' , width : 22 } ,
{ header : 'Hausnr.' , key : 'house_no' , width : 8 } ,
{ header : 'PLZ' , key : 'zip' , width : 8 } ,
{ header : 'Ort' , key : 'city' , width : 16 } ,
{ header : 'Land' , key : 'country' , width : 12 } ,
{ header : 'Adresse (Freitext)' , key : 'free_text' , width : 36 } ,
{ header : 'PDF-Seiten' , key : 'seiten' , width : 11 } ,
{ header : 'Hinweis' , key : 'hinweis' , width : 34 } ,
] ;
ws . getRow ( 1 ) . font = { bold : true } ;
ws . views = [ { state : 'frozen' , ySplit : 1 } ] ;
// Seitenzähler läuft über alle Dateien – identisch zum Zusammenfügen in merge-pdf
// (defekte/fehlende PDFs werden dort übersprungen und belegen keine Seiten).
let seite = 0 ;
let nr = 0 ;
for ( const o of orders ) {
const empf = proAuftrag . get ( String ( o . id ) ) || [ ] ;
const dateien = [ ] ;
for ( const fid of dateienVon ( o ) ) {
try {
const buf = await directus . fetchAssetBuffer ( fid ) ;
const src = await PDFDocument . load ( buf , { ignoreEncryption : true } ) ;
const n = src . getPageCount ( ) ;
dateien . push ( { von : seite + 1 , bis : seite + n , ok : true } ) ;
seite += n ;
} catch ( e ) {
dateien . push ( { ok : false } ) ;
console . warn ( ` [Charge] PDF ${ fid } (Excel) übersprungen: ${ e . message } ` ) ;
}
}
const okDateien = dateien . filter ( ( d ) => d . ok ) ;
const auftragSeiten = okDateien . length ? seitenText ( okDateien [ 0 ] . von , okDateien [ okDateien . length - 1 ] . bis ) : '' ;
const jeBrief = dateien . length > 0 && dateien . length === empf . length && okDateien . length === dateien . length ;
let hinweis = '' ;
if ( ! dateien . length ) hinweis = 'Keine Druckdatei – nicht im Sammel-PDF.' ;
else if ( okDateien . length < dateien . length ) hinweis = ` ${ dateien . length - okDateien . length } PDF(s) defekt/fehlend – im Sammel-PDF übersprungen. ` ;
else if ( ! jeBrief ) hinweis = 'Seitenbereich gilt für den ganzen Auftrag.' ;
const zeilen = empf . length ? empf : [ { } ] ; // Auftrag ohne Empfänger trotzdem sichtbar
zeilen . forEach ( ( e , i ) => {
nr += 1 ;
ws . addRow ( {
nr ,
auftrag : o . order _number || '' ,
brief : e . letter _number ? ? '' ,
salutation : e . salutation || '' , first _name : e . first _name || '' , last _name : e . last _name || '' ,
street : e . street || '' , house _no : e . house _no || '' , zip : e . zip || '' , city : e . city || '' ,
country : e . country || '' , free _text : e . free _text || '' ,
seiten : jeBrief ? seitenText ( dateien [ i ] . von , dateien [ i ] . bis ) : auftragSeiten ,
hinweis : empf . length ? hinweis : 'Keine Empfänger am Auftrag.' ,
} ) ;
} ) ;
}
ws . getColumn ( 'free_text' ) . alignment = { wrapText : true , vertical : 'top' } ;
const buf = Buffer . from ( await wb . xlsx . writeBuffer ( ) ) ;
res . setHeader ( 'Content-Type' , 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) ;
res . setHeader ( 'Content-Disposition' , 'attachment; filename="charge.xlsx"' ) ;
return res . status ( 200 ) . send ( buf ) ;
} catch ( err ) {
return next ( err ) ;
}
}
module . exports = { mergePdf , mergeXlsx } ;