Files
skrift-backend/src/api/controllers/directus-controller.js
s4luorth 8030d3d0ec
All checks were successful
Build & Deploy / deploy (push) Successful in 13s
Postkarten-Layout: Text links, Absender+Empfänger rechts, Briefmarkenfeld frei
Neues render_format-kind 'postkarte'. generatePostcardSVG setzt den Text auf die
linke Kartenhälfte, Absender (klein, oben) und Empfänger (Anschriftzone unten) auf
die rechte Hälfte; oben rechts bleibt das Briefmarkenfeld frei. Ein SVG je Karte
(ein Plotter-Durchlauf), geschrieben als letter_NNN.svg.

- page-layout: kind je Format mitführen + getFormatKind().
- svg-generator: generatePostcardSVG (Body/Absender/Empfänger über Marker
  [[pk_abs]]/[[pk_adr]] getrennt, damit alle Teile scriptalisiert werden).
- order-/preview-/directus-controller: type 'postcard' dispatchen, Marker in der
  Scriptalisierung erhalten, Postkarten-Dokument aus render_format-kind bauen.
- directus-service: kind aus render_formats mitladen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-11 13:58:06 +02:00

249 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Generierung auf Basis eines Directus-Auftrags.
*
* Ablauf: Directus meldet "Auftrag X ist bezahlt" → dieser Endpunkt holt den
* Auftrag samt Empfängerzeilen aus Directus, baut daraus die Dokumentliste für
* die bestehende Pipeline (runGeneration) und schreibt Ergebnis + Status zurück.
*
* Antwortet sofort mit 202 – die Generierung läuft im Hintergrund weiter, weil
* sie je nach Menge deutlich länger dauern kann als ein HTTP-Timeout erlaubt.
*/
const { runGeneration } = require('./order-controller');
const directus = require('../../services/directus-service');
const { countLetterPages } = require('../../lib/svg-generator');
const { getFormatKind } = require('../../lib/page-layout');
const { replacePlaceholders } = require('../../services/placeholder-service');
// Directus-Formatschlüssel → Backend-Formate
const LETTER_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' };
const ENVELOPE_FORMAT = { c6: 'c6', dinlang: 'din_lang' };
/** Baut den Adressblock für einen Umschlag aus einer Empfängerzeile. */
function addressText(entry) {
if (entry.free_text) return String(entry.free_text).trim();
const name = [entry.first_name, entry.last_name].filter(Boolean).join(' ');
const street = [entry.street, entry.house_no].filter(Boolean).join(' ');
const city = [entry.zip, entry.city].filter(Boolean).join(' ');
const isForeign = entry.country && !/^(de|deutschland|germany)$/i.test(String(entry.country).trim());
return [name, street, city, isForeign ? entry.country : null]
.filter(Boolean).join('\n');
}
/** Platzhalterwerte einer Zeile (strukturierte Felder + freie Platzhalter). */
function placeholdersOf(entry) {
return {
Anrede: entry.salutation || '',
Vorname: entry.first_name || '',
Nachname: entry.last_name || '',
Strasse: [entry.street, entry.house_no].filter(Boolean).join(' '),
PLZ: entry.zip || '',
Ort: entry.city || '',
Land: entry.country || '',
Briefnummer: entry.letter_number ?? '',
...(entry.placeholders && typeof entry.placeholders === 'object' ? entry.placeholders : {}),
};
}
/** Übersetzt einen Directus-Auftrag in die Dokumentliste für runGeneration. */
function buildDocuments(order, entries) {
const font = order.font || 'tilda';
// Eigene Format-Schlüssel (freie Formate) unverändert durchreichen.
const letterFormat = LETTER_FORMAT[order.format?.key] || order.format?.key || 'a4';
const envFormat = ENVELOPE_FORMAT[order.envelope_format] || 'c6';
const wantsEnvelope = !!order.needs_envelope && order.envelope_labeling !== 'keine';
const envelopeType = order.envelope_labeling === 'freitext' ? 'custom' : 'recipient';
// Datei-Produkte (z. B. Unterschriftenservice): das „Schriftstück" ist das vom
// Kunden hochgeladene Dokument – es wird NICHT generiert. Kuverts hingegen schon.
const istDatei = order.product?.input_mode === 'datei';
// Postkarte: Text links, Absender+Empfänger rechts, Briefmarkenfeld frei – ein SVG
// je Karte (kein separater Umschlag). Erkennung über die render_format-Art.
const istPostkarte = getFormatKind(letterFormat) === 'postkarte';
// Kuvert-only-Produkt: es wird KEIN Schriftstück generiert, nur die (adressierten)
// Kuverts. Etwaige hochgeladene PDF-Briefe liegen als source_files am Auftrag.
const istKuvertOnly = order.product?.type === 'envelope';
// Natürliche Handschrift-Variation (Default an), am Auftrag gespeichert.
const realistisch = order.realistic !== false;
// Relative Schriftgröße (getrennt Brief/Kuvert), aus dem Auftrag. 1 = Standard.
const fontScaleLetter = Number(order.font_scale_letter) || 1;
const fontScaleEnv = Number(order.font_scale_envelope) || 1;
// Signatur(en) auf dem Brief (manuelle Aufträge): SVG je [[signaturN]] + Feinjustierung.
const sigManual = order.signatures_manual && typeof order.signatures_manual === 'object' ? order.signatures_manual : null;
const sigMap = sigManual?.signatures || {};
const sigOff = sigManual?.offsets || {};
// Mehrseitiges Schriftstück (Auto-Fluss über N Seiten) – nur wenn am Auftrag gesetzt.
const istMehrseitig = !!order.multi_page;
const documents = [];
entries.forEach((entry, i) => {
const placeholders = placeholdersOf(entry);
// Dateinummer = Briefnummer (1-basiert), damit letter_001.svg zu
// Briefnummer 1 gehört. Vorschau nummeriert identisch (siehe skrift-orders).
const nr = Number(entry.letter_number) || (i + 1);
// Abweichender Text je Schriftstück hat Vorrang; sonst Standardtext.
const briefText = (entry.text && String(entry.text).trim()) ? entry.text : (order.text_template || '');
// Postkarte: EIN Dokument mit Text + Absender + Empfänger (über Marker getrennt).
if (istPostkarte) {
const recip = addressText(entry);
const sender = order.envelope_sender ? String(order.envelope_sender).trim() : '';
let combined = String(briefText || '');
if (sender) combined += `[[pk_abs]]${sender}`;
if (recip) combined += `[[pk_adr]]${recip}`;
documents.push({
index: nr, type: 'postcard', text: combined, format: letterFormat, font,
placeholders, fontScale: fontScaleLetter, realisticHandwriting: realistisch,
});
return;
}
// Kein Schriftstück erzeugen, wenn kein Text hinterlegt ist (z. B. „nur Kuverts drucken").
if (!istDatei && !istKuvertOnly && String(briefText).trim()) {
documents.push({
index: nr,
type: 'letter',
text: briefText,
format: letterFormat,
font,
placeholders,
fontScale: fontScaleLetter,
signatures: sigMap,
signatureOffsets: sigOff,
multiPage: istMehrseitig,
realisticHandwriting: realistisch,
});
}
if (wantsEnvelope) {
documents.push({
index: nr,
type: 'envelope',
envelopeType,
text: envelopeType === 'recipient' ? addressText(entry) : (order.envelope_text || order.text_template || ''),
format: envFormat,
font,
placeholders,
fontScale: fontScaleEnv,
realisticHandwriting: realistisch,
});
}
});
return { documents, letterFormat, envFormat, wantsEnvelope };
}
/**
* POST /api/order/from-directus
* Body: { orderId }
*/
async function generateFromDirectus(req, res, next) {
const { orderId } = req.body || {};
if (!orderId) {
return res.status(400).json({ error: 'Invalid request', message: 'orderId is required' });
}
let order;
try {
const loaded = await directus.fetchOrder(orderId);
order = loaded.order;
if (!loaded.entries.length) {
return res.status(400).json({
error: 'Invalid order',
message: `Auftrag ${order.order_number} hat keine Empfängerzeilen.`
});
}
// Sofort quittieren – der Rest läuft im Hintergrund.
res.status(202).json({
accepted: true,
orderId,
orderNumber: order.order_number,
documents: loaded.entries.length * (order.needs_envelope ? 2 : 1),
});
const { documents, letterFormat, envFormat, wantsEnvelope } = buildDocuments(order, loaded.entries);
// Nichts zu generieren (z. B. Datei-Produkt ohne Kuvert) → sauber überspringen.
if (!documents.length) {
console.log(`[Directus] ${order.order_number}: nichts zu generieren (Datei-Produkt ohne Kuvert).`);
return;
}
console.log(`[Directus] Generiere ${order.order_number}: ${documents.length} Dokumente`);
const result = await runGeneration({
orderNumber: order.order_number,
letters: documents,
metadata: {
letterFormat,
envelopeFormat: wantsEnvelope ? envFormat : null,
envelopeBeschriftung: wantsEnvelope ? order.envelope_labeling : null,
directusOrderId: orderId,
customerEmail: order.customer_email,
// Vorschau-Session → gezeigte Dokumente 1:1 übernehmen.
previewSession: order.preview_session || null,
// Eingegebener Brieftext → als brieftext.txt im Ausgabeordner ablegen.
rawText: order.text_template || null,
// Absender fürs Kuvert (manuelle Aufträge): oben links, Empfänger unten rechts.
envelopeSender: order.envelope_sender || null,
envelopeSenderScale: Number(order.envelope_sender_scale) || 1,
},
});
await directus.updateOrder(orderId, {
status: 'in_queue',
artifact_path: result.outputPath,
});
await directus.addStatusHistory(orderId, 'in_queue',
`${result.letters.length} Schriftstücke, ${result.envelopes.length} Umschläge generiert.`);
console.log(`[Directus] ${order.order_number} fertig → in_queue`);
} catch (err) {
console.error('[Directus] Generierung fehlgeschlagen:', err.message);
// Fehler in Directus sichtbar machen, statt still zu scheitern.
if (order?.id || orderId) {
try {
await directus.addStatusHistory(orderId, 'fehler', `Generierung fehlgeschlagen: ${err.message}`);
} catch (e) {
console.error('[Directus] Konnte Fehler nicht zurückschreiben:', e.message);
}
}
// Antwort wurde ggf. schon gesendet (202) – dann nur noch loggen.
if (!res.headersSent) {
if (err.status) return res.status(err.status).json({ error: 'Generation failed', message: err.message });
return next(err);
}
}
}
/**
* POST /api/order/pagecount
* Body: { text, format, fontScale, entries: [{ placeholders }] }
* Liefert je Dokument die Seitenzahl (nur Umbruch-Zählung, kein Scriptalizer/SVG).
*/
function pageCounts(req, res) {
const { text, format, fontScale, entries } = req.body || {};
if (!text || !String(text).trim()) {
return res.status(400).json({ error: 'Invalid request', message: 'text is required' });
}
const fmt = format || 'a4';
const scale = Number(fontScale) || 1;
const list = Array.isArray(entries) && entries.length ? entries : [{}];
const counts = list.map((e, i) => {
const ph = { ...(e && typeof e.placeholders === 'object' ? e.placeholders : {}), Briefnummer: String(i + 1) };
let pages = 1;
try { pages = countLetterPages(replacePlaceholders(String(text), ph), fmt, { fontScale: scale }); }
catch { pages = 1; }
return { index: i + 1, pages };
});
return res.json({ counts });
}
module.exports = { generateFromDirectus, pageCounts };