const fs = require('fs').promises; const fsSync = require('fs'); const path = require('path'); const config = require('../../config'); const { scriptalizeBatch } = require('../../services/scriptalizer-service'); const { generateLetterSVG, generateEnvelopeSVG } = require('../../lib/svg-generator'); const { replacePlaceholders, generateAllCSVs } = require('../../services/placeholder-service'); const { TAB_MARKER } = require('../../lib/svg-font-engine'); /** * POST /api/order/finalize * Finalize order by copying cached previews to output directory */ async function finalizeOrder(req, res, next) { try { const { sessionId, orderNumber } = req.body; // Validation if (!sessionId || !orderNumber) { return res.status(400).json({ error: 'Invalid request', message: 'sessionId and orderNumber are required' }); } console.log(`[Order] Finalizing order: ${orderNumber} from session: ${sessionId}`); // Check if session cache exists const sessionDir = path.join(config.paths.previews, sessionId); try { await fs.access(sessionDir); } catch { return res.status(404).json({ error: 'Session not found', message: `Preview cache not found for session: ${sessionId}. Please generate previews first.` }); } // Check cache expiration const metadataPath = path.join(sessionDir, '.metadata.json'); try { const metadataContent = await fs.readFile(metadataPath, 'utf8'); const metadata = JSON.parse(metadataContent); const expiresAt = new Date(metadata.expiresAt); if (new Date() > expiresAt) { return res.status(410).json({ error: 'Cache expired', message: 'Preview cache has expired. Please regenerate previews.', expiredAt: metadata.expiresAt }); } } catch (err) { console.warn(`[Order] Metadata not found for session: ${sessionId}`); } // Create output directory and subdirectories const outputDir = path.join(config.paths.output, orderNumber); const envelopesDir = path.join(outputDir, 'umschlaege'); await fs.mkdir(outputDir, { recursive: true }); await fs.mkdir(envelopesDir, { recursive: true }); // Copy files from cache to output, separating letters and envelopes const files = await fs.readdir(sessionDir); const copiedLetters = []; const copiedEnvelopes = []; const copiedOther = []; for (const file of files) { // Skip metadata file if (file === '.metadata.json') { continue; } const sourcePath = path.join(sessionDir, file); // Determine destination based on file type if (file.startsWith('envelope_')) { // Umschläge in Unterordner 'umschlaege' const destPath = path.join(envelopesDir, file); await fs.copyFile(sourcePath, destPath); copiedEnvelopes.push(file); } else if (file.startsWith('letter_')) { // Briefe im Hauptordner const destPath = path.join(outputDir, file); await fs.copyFile(sourcePath, destPath); copiedLetters.push(file); } else { // Andere Dateien (CSV, etc.) im Hauptordner const destPath = path.join(outputDir, file); await fs.copyFile(sourcePath, destPath); copiedOther.push(file); } } // Create order metadata const meta = req.body.metadata || {}; const orderMetadata = { orderNumber, sessionId, finalizedAt: new Date().toISOString(), letterCount: copiedLetters.length, envelopeCount: copiedEnvelopes.length, letters: copiedLetters, envelopes: copiedEnvelopes, other: copiedOther, letterFormat: meta.letterFormat || null, envelopeFormat: meta.envelopeFormat || null, envelopeBeschriftung: meta.envelopeBeschriftung || null, }; const orderMetadataPath = path.join(outputDir, 'order-metadata.json'); await fs.writeFile(orderMetadataPath, JSON.stringify(orderMetadata, null, 2), 'utf8'); const totalFiles = copiedLetters.length + copiedEnvelopes.length + copiedOther.length; console.log(`[Order] Finalized order ${orderNumber}: ${copiedLetters.length} letters, ${copiedEnvelopes.length} envelopes, ${copiedOther.length} other files`); // Response res.status(200).json({ orderNumber, outputPath: outputDir, letters: copiedLetters, envelopes: copiedEnvelopes, other: copiedOther, totalFiles, envelopesGenerated: 0, timestamp: orderMetadata.finalizedAt }); } catch (err) { console.error('[Order] Error finalizing order:', err); next(err); } } /** * POST /api/order/generate * Generate order from scratch without using cache */ async function runGeneration({ orderNumber, letters, metadata }) { { // Validation if (!orderNumber) { const e = new Error('orderNumber is required'); e.status = 400; throw e; } if (!letters || !Array.isArray(letters) || letters.length === 0) { const e = new Error('Letters array is required and must not be empty'); e.status = 400; throw e; } console.log(`[Order] Generating order: ${orderNumber}`); console.log(`[Order] Total documents: ${letters.length}`); // Debug: Zeige welche Dokument-Typen ankommen const letterCount = letters.filter(l => l.type !== 'envelope').length; const envelopeCount = letters.filter(l => l.type === 'envelope').length; console.log(`[Order] Letters: ${letterCount}, Envelopes: ${envelopeCount}`); console.log(`[Order] Document types:`, letters.map(l => ({ index: l.index, type: l.type, envelopeType: l.envelopeType }))); // Create output directory and subdirectories const outputDir = path.join(config.paths.output, orderNumber); const envelopesDir = path.join(outputDir, 'umschlaege'); await fs.mkdir(outputDir, { recursive: true }); await fs.mkdir(envelopesDir, { recursive: true }); // Step 1: Prepare texts and replace placeholders const processedLetters = letters.map((letter, loopIndex) => { let text = letter.text || ''; // Signatur-Platzhalter separat extrahieren – sie werden NICHT als Text ersetzt const sigPlaceholders = {}; const nonSigPlaceholders = {}; if (letter.placeholders && typeof letter.placeholders === 'object') { Object.entries(letter.placeholders).forEach(([k, v]) => { if (/^signatur\d+$/i.test(k)) sigPlaceholders[k.toLowerCase()] = v; else nonSigPlaceholders[k] = v; }); text = replacePlaceholders(text, nonSigPlaceholders); } return { ...letter, index: letter.index !== undefined ? letter.index : loopIndex, loopIndex, processedText: text, signatures: sigPlaceholders, format: letter.format || 'a4', font: letter.font || 'tilda', type: letter.type || 'letter', envelopeType: letter.envelopeType || 'recipient', realisticHandwriting: letter.realisticHandwriting ?? false, }; }); // Step 2: Group letters by font for batch scriptalization const lettersByFont = {}; processedLetters.forEach(letter => { const font = letter.font; if (!lettersByFont[font]) { lettersByFont[font] = []; } lettersByFont[font].push(letter); }); // Step 3: Scriptalize texts by font const scriptalizedMap = new Map(); for (const [font, fontLetters] of Object.entries(lettersByFont)) { // Tabs (\t) und Signatur-Marker ([[signaturN]]) vor Scriptalisierung extrahieren, // danach mit TAB_MARKER bzw. [[signaturN]] restaurieren. const scriptalizerInputs = []; const splitMaps = []; for (const letter of fontLetters) { const raw = letter.processedText; const parts = raw.split(/(\t|\[\[signatur\d+\]\]|[&|])/gi); const segments = [], markers = []; for (let i = 0; i < parts.length; i++) { if (i % 2 === 0) segments.push(parts[i]); else markers.push(parts[i]); } // Leere Segmente NICHT an den Scriptalizer senden const startIdx = scriptalizerInputs.length; const hasContent = segments.map(s => s.trim().length > 0); segments.forEach((s, j) => { if (hasContent[j]) scriptalizerInputs.push(s); }); splitMaps.push({ loopIndex: letter.loopIndex, startIdx, segments, markers, hasContent, fallback: raw }); } console.log(`[Order] Scriptalizing ${scriptalizerInputs.length} segments with font: ${font}`); try { const scriptalizedAll = await scriptalizeBatch(scriptalizerInputs, font, config.scriptalizer.errFrequency); for (const { loopIndex, startIdx, segments, markers, hasContent, fallback } of splitMaps) { const nonEmptyCount = hasContent.filter(Boolean).length; if (!nonEmptyCount && !markers.length) { scriptalizedMap.set(loopIndex, fallback); continue; } let si = startIdx; const segs = segments.map((_s, j) => hasContent[j] ? (scriptalizedAll[si++] || '') : ''); let reconstructed = segs[0] || ''; for (let i = 0; i < markers.length; i++) { reconstructed += (markers[i] === '\t' ? TAB_MARKER : markers[i]); reconstructed += (segs[i + 1] || ''); } scriptalizedMap.set(loopIndex, reconstructed); } } catch (err) { console.error(`[Order] Scriptalization failed for font ${font}:`, err.message); const e = new Error(`Failed to scriptalize texts with font ${font}: ${err.message}`); e.status = 500; e.label = 'Scriptalization failed'; throw e; } } // Step 4: Generate SVG files - separate letters and envelopes const generatedLetters = []; const generatedEnvelopes = []; for (let i = 0; i < processedLetters.length; i++) { const letter = processedLetters[i]; // Verwende loopIndex für scriptalizedMap Zuordnung const scriptalizedText = scriptalizedMap.get(letter.loopIndex) || letter.processedText; let svgResult; try { if (letter.type === 'envelope') { svgResult = generateEnvelopeSVG( scriptalizedText, letter.format, letter.envelopeType, { font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false } ); } else { svgResult = generateLetterSVG( scriptalizedText, letter.format, { font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, signatures: letter.signatures || {} } ); } } catch (err) { console.error(`[Order] SVG generation failed for letter ${i}:`, err.message); const e = new Error(`Failed to generate SVG for letter ${i}: ${err.message}`); e.status = 500; e.label = 'SVG generation failed'; throw e; } // Save SVG file - unterscheide zwischen Briefen und Umschlägen const absoluteIndex = letter.index !== undefined ? letter.index : i; if (letter.type === 'envelope') { const filename = `envelope_${String(absoluteIndex).padStart(3, '0')}.svg`; const filepath = path.join(envelopesDir, filename); await fs.writeFile(filepath, svgResult.svg, 'utf8'); generatedEnvelopes.push(filename); } else { const filename = `letter_${String(absoluteIndex).padStart(3, '0')}.svg`; const filepath = path.join(outputDir, filename); await fs.writeFile(filepath, svgResult.svg, 'utf8'); generatedLetters.push(filename); } } console.log(`[Order] Generated ${generatedLetters.length} letters, ${generatedEnvelopes.length} envelopes`); // Step 5: Generate all CSV files if placeholders are present const hasPlaceholders = processedLetters.some(l => l.placeholders && Object.keys(l.placeholders).length > 0); const csvFiles = []; if (hasPlaceholders) { try { const csvs = generateAllCSVs(processedLetters); // Brief-Platzhalter CSV if (csvs.placeholders) { const filename = 'brief_platzhalter.csv'; await fs.writeFile(path.join(outputDir, filename), csvs.placeholders, 'utf8'); csvFiles.push(filename); console.log(`[Order] Generated brief_platzhalter.csv`); } // Empfänger CSV (für recipientData-Modus) if (csvs.recipients) { const filename = 'empfaenger.csv'; await fs.writeFile(path.join(outputDir, filename), csvs.recipients, 'utf8'); csvFiles.push(filename); console.log(`[Order] Generated empfaenger.csv`); } // Umschlag-Platzhalter CSV (für customText-Modus) if (csvs.envelopePlaceholders) { const filename = 'umschlag_platzhalter.csv'; await fs.writeFile(path.join(outputDir, filename), csvs.envelopePlaceholders, 'utf8'); csvFiles.push(filename); console.log(`[Order] Generated umschlag_platzhalter.csv`); } } catch (err) { console.error('[Order] CSV generation failed:', err.message); // Don't fail the request, CSV is optional } } // Step 6: Create order metadata const meta = metadata || {}; const orderMetadata = { orderNumber, generatedAt: new Date().toISOString(), letterCount: generatedLetters.length, envelopeCount: generatedEnvelopes.length, letters: generatedLetters, envelopes: generatedEnvelopes, fonts: Object.keys(lettersByFont), hasPlaceholders, csvFiles, letterFormat: meta.letterFormat || null, envelopeFormat: meta.envelopeFormat || null, envelopeBeschriftung: meta.envelopeBeschriftung || null, }; const orderMetadataPath = path.join(outputDir, 'order-metadata.json'); await fs.writeFile(orderMetadataPath, JSON.stringify(orderMetadata, null, 2), 'utf8'); console.log(`[Order] Order ${orderNumber} generated successfully`); return { orderNumber, outputPath: outputDir, letters: generatedLetters, envelopes: generatedEnvelopes, csvFiles, totalFiles: generatedLetters.length + generatedEnvelopes.length + csvFiles.length, timestamp: orderMetadata.generatedAt }; } } /** * POST /api/order/generate * HTTP-Wrapper um runGeneration – Verhalten unverändert. */ async function generateOrder(req, res, next) { try { const { orderNumber, letters, metadata } = req.body; const result = await runGeneration({ orderNumber, letters, metadata }); res.status(200).json(result); } catch (err) { if (err.status) { return res.status(err.status).json({ error: err.label || 'Invalid request', message: err.message }); } console.error('[Order] Error generating order:', err); next(err); } } /** * POST /api/order/motif * Upload a motif image for an order */ async function uploadMotif(req, res, next) { try { const { orderNumber } = req.body; // Validation if (!orderNumber) { return res.status(400).json({ error: 'Invalid request', message: 'orderNumber is required' }); } if (!req.file) { return res.status(400).json({ error: 'Invalid request', message: 'No file uploaded' }); } console.log(`[Order] Uploading motif for order: ${orderNumber}`); console.log(`[Order] File: ${req.file.originalname}, Size: ${req.file.size} bytes, Type: ${req.file.mimetype}`); // Create output directory if it doesn't exist const outputDir = path.join(config.paths.output, orderNumber); await fs.mkdir(outputDir, { recursive: true }); // Determine file extension const originalExt = path.extname(req.file.originalname).toLowerCase() || '.png'; const filename = `motif${originalExt}`; const filepath = path.join(outputDir, filename); // Write file to disk await fs.writeFile(filepath, req.file.buffer); console.log(`[Order] Motif saved to: ${filepath}`); // Response res.status(200).json({ success: true, orderNumber, filename, path: filepath, size: req.file.size, mimetype: req.file.mimetype }); } catch (err) { console.error('[Order] Error uploading motif:', err); next(err); } } /** * POST /api/order/files * Speichert eine bereits vom WordPress-Frontend hochgeladene Datei * (Adress-/Platzhalter-Excel, Briefdokument) im Auftragsordner ab, * damit sie zusammen mit den generierten Umschlägen vorliegt. */ async function uploadOrderFile(req, res, next) { try { const { orderNumber, fileType } = req.body; if (!orderNumber || !fileType) { return res.status(400).json({ error: 'Invalid request', message: 'orderNumber and fileType are required' }); } if (!req.file) { return res.status(400).json({ error: 'Invalid request', message: 'No file uploaded' }); } const outputDir = path.join(config.paths.output, orderNumber, 'dateien'); await fs.mkdir(outputDir, { recursive: true }); const ext = path.extname(req.file.originalname) || ''; const filename = `${fileType}${ext}`; const filepath = path.join(outputDir, filename); await fs.writeFile(filepath, req.file.buffer); console.log(`[Order] File saved for order ${orderNumber}: ${filename}`); res.status(200).json({ success: true, orderNumber, fileType, filename, path: filepath, size: req.file.size, }); } catch (err) { console.error('[Order] Error saving order file:', err); next(err); } } /** * POST /api/order/register * Speichert Metadaten für einen Auftrag ohne SVG-Generierung (z.B. gedruckte Briefe) */ async function registerOrder(req, res, next) { try { const { orderNumber, metadata } = req.body; if (!orderNumber) { return res.status(400).json({ error: 'Invalid request', message: 'orderNumber is required' }); } const outputDir = path.join(config.paths.output, orderNumber); await fs.mkdir(outputDir, { recursive: true }); const orderMetadata = { orderNumber, registeredAt: new Date().toISOString(), ...(metadata || {}), }; await fs.writeFile( path.join(outputDir, 'order-metadata.json'), JSON.stringify(orderMetadata, null, 2), 'utf8' ); console.log(`[Order] Registered order ${orderNumber}`); res.status(200).json({ orderNumber, registeredAt: orderMetadata.registeredAt }); } catch (err) { console.error('[Order] Error registering order:', err); next(err); } } module.exports = { finalizeOrder, generateOrder, runGeneration, uploadMotif, uploadOrderFile, registerOrder, };