Kontakte/CRM (Directus): - Neue Collections `contacts` (Leads) + `contact_actions` (Rückruf/Angebot/Mail). - Hook `skrift-contacts`: Kontakte werden nur auf Anforderung angelegt (Auftrag "In Kontakte speichern"); Rückruf-Erinnerung (Zeitplan) und Angebots-/Mail-an-mich-Mails; Auto-Löschung nur bei Lead-Status "kein_interesse" nach 30 Tagen; optionaler DSGVO-Strip (per Env, Dry-Run). - Modul `skrift-kontakte`: handytaugliches Board (Anruf via groundwire:, Notizen mit Autosave, Durchblättern, CSV-Export, Aktionen als Buttons). API-Chargen & PDF-Sammeldruck: - Produktions-Board gruppiert API-Aufträge unter "Neu" zu Chargen (Kunde + Typ + Versandtag + Eingangstag), aufklappbar, mit "fällig bis" (Versandtag der übernächsten Woche). - Mehrfachauswahl -> Sammel-PDF (Backend `pdf-lib`, /api/charge/merge-pdf, Directus-Proxy /skrift-orders/charge/pdf), Kuverts an Plotter und Sammel-Status; PDF- und Kuvert-Reihenfolge nach Auftragsnummer (Kollation). - Mailer: keine Einzelmail je API-Auftrag mehr, stattdessen Tagesübersicht (18:00) pro Kunde, nach Versandtag aufgeschlüsselt. Status-API & Generierung: - Public-API-Status liest jetzt production_status (Board) statt nur kaufmännischem Status; neues Feld `status_since` in der Antwort. - Auftrag stempelt `production_status_changed_at` bei jedem Statuswechsel. - Generierung wird IMMER beim Eingang angestoßen (auch Datei-Produkte mit handgeschriebenem Kuvert), nicht nur bei letter/postcard. - Versandtag im Produktionsdashboard sichtbar. Rechte: Service-Rolle darf directus_files lesen (Backend lädt PDFs für den Sammeldruck über /assets). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
/**
|
|
* Directus-Client für das Backend.
|
|
*
|
|
* Directus ist die Datenquelle für Aufträge; dieses Backend bleibt die reine
|
|
* Generierungs-Engine. Zugriff über einen Service-Token (Rolle "Service").
|
|
*/
|
|
|
|
const config = require('../config');
|
|
|
|
function requireConfig() {
|
|
if (!config.directus.url || !config.directus.token) {
|
|
const e = new Error('DIRECTUS_URL und DIRECTUS_TOKEN müssen gesetzt sein.');
|
|
e.status = 500;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function directusFetch(path, options = {}) {
|
|
requireConfig();
|
|
const res = await fetch(config.directus.url.replace(/\/$/, '') + path, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${config.directus.token}`,
|
|
...(options.headers || {}),
|
|
},
|
|
});
|
|
const text = await res.text();
|
|
let data = null;
|
|
try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
|
|
if (!res.ok) {
|
|
const msg = data?.errors?.[0]?.message || text || res.statusText;
|
|
const e = new Error(`Directus ${options.method || 'GET'} ${path}: ${msg}`);
|
|
e.status = res.status === 404 ? 404 : 502;
|
|
throw e;
|
|
}
|
|
return data?.data ?? data;
|
|
}
|
|
|
|
/** Lädt einen Auftrag inkl. Empfängerzeilen, Produkt und Format. */
|
|
async function fetchOrder(orderId) {
|
|
const fields = [
|
|
'id', 'order_number', 'customer_email', 'font', 'status',
|
|
'needs_envelope', 'envelope_labeling', 'envelope_text', 'envelope_format',
|
|
'text_template', 'motif_mode', 'own_cards', 'entries_count', 'preview_session',
|
|
'source_file', 'product.key', 'product.type', 'product.input_mode', 'format.key',
|
|
'font_scale_letter', 'font_scale_envelope', 'signatures_manual', 'multi_page', 'realistic',
|
|
].join(',');
|
|
|
|
const order = await directusFetch(`/items/orders/${encodeURIComponent(orderId)}?fields=${fields}`);
|
|
if (!order) {
|
|
const e = new Error(`Auftrag ${orderId} nicht gefunden.`);
|
|
e.status = 404;
|
|
throw e;
|
|
}
|
|
|
|
const entries = await directusFetch(
|
|
`/items/order_entries?filter[order][_eq]=${encodeURIComponent(orderId)}` +
|
|
`&sort=letter_number&limit=-1&fields=*`
|
|
);
|
|
|
|
return { order, entries: entries || [] };
|
|
}
|
|
|
|
/** Schreibt Status und Artefakt-Pfad zurück. */
|
|
async function updateOrder(orderId, patch) {
|
|
return directusFetch(`/items/orders/${encodeURIComponent(orderId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
}
|
|
|
|
/** Protokolliert einen Statuswechsel in der Timeline. */
|
|
async function addStatusHistory(orderId, status, note) {
|
|
return directusFetch('/items/status_history', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ order: orderId, status, note }),
|
|
});
|
|
}
|
|
|
|
/** Lädt mehrere Aufträge (nur die für den Charge-Sammeldruck nötigen Felder),
|
|
* sortiert nach Auftragsnummer. */
|
|
async function fetchOrdersMinimal(ids) {
|
|
const idList = ids.map((v) => encodeURIComponent(v)).join(',');
|
|
return directusFetch(
|
|
`/items/orders?filter[id][_in]=${idList}&limit=-1&sort=order_number` +
|
|
'&fields=id,order_number,source_files,source_file',
|
|
);
|
|
}
|
|
|
|
/** Lädt eine Directus-Datei (z. B. hochgeladenes Brief-PDF) als Buffer. */
|
|
async function fetchAssetBuffer(fileId) {
|
|
requireConfig();
|
|
const res = await fetch(config.directus.url.replace(/\/$/, '') + '/assets/' + encodeURIComponent(fileId), {
|
|
headers: { Authorization: `Bearer ${config.directus.token}` },
|
|
});
|
|
if (!res.ok) {
|
|
const e = new Error(`Directus asset ${fileId}: ${res.status}`);
|
|
e.status = res.status === 404 ? 404 : 502;
|
|
throw e;
|
|
}
|
|
return Buffer.from(await res.arrayBuffer());
|
|
}
|
|
|
|
/** Zentrale Format-Definitionen (Seitengröße, Ränder, Zeilenlimit) aus Directus. */
|
|
async function fetchRenderFormats() {
|
|
return directusFetch(
|
|
'/items/render_formats?filter[active][_eq]=true&limit=-1' +
|
|
'&fields=key,page_width_mm,page_height_mm,margin_top_mm,margin_left_mm,margin_right_mm,line_height_factor,line_limit',
|
|
);
|
|
}
|
|
|
|
module.exports = { fetchOrder, updateOrder, addStatusHistory, fetchRenderFormats, fetchOrdersMinimal, fetchAssetBuffer };
|