Board: Suche entlaggt, Order-Sprung, Versandadresse, Motiv-Hinweis; Mail: tägliche Statusübersicht

produktion:
- Empfängersuche entlaggt (uncontrolled Input + 250ms-Debounce + vorab
  kleingeschriebenes Haystack) - kein Full-Rerender je Tastendruck mehr.
- "Auftrag öffnen"-Link je Karte (-> Directus-Order, neuer Tab).
- Sammelversand: Lieferadresse direkt auf der Karte (abweichende Order-
  Lieferadresse -> Kunden-Liefer-/Rechnungsadresse als Fallback).
- Postkarten-Motiv wird als Badge hervorgehoben.

mailer:
- API-Einzelzusammenfassung entfernt; stattdessen taegliche Gesamtuebersicht
  um 03:00 ueber ALLE Auftraege inkl. Mengen je Produktionsstatus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-10-01 07:50:21 +02:00
parent bbbc637a81
commit 79d6b4f64b
2 changed files with 87 additions and 41 deletions

View File

@@ -260,44 +260,59 @@ export default (register, { services, getSchema, logger }) => {
}
});
// ── Tagesübersicht der API-Aufträge (statt Einzelmail je Auftrag) ───────────
// Einmal täglich (Default 18:00): pro API-Kunde eine Zusammenfassung der heute
// eingegangenen Aufträge, aufgeschlüsselt nach Versandtag.
const API_DIGEST_CRON = process.env.SKRIFT_API_DIGEST_CRON || '0 18 * * *';
register.schedule(API_DIGEST_CRON, async () => {
// ── Tägliche Gesamtübersicht (03:00) – alle Aufträge nach Produktionsstatus ──
// Ersetzt die frühere API-Einzelzusammenfassung: jeden Morgen eine Übersicht
// ALLER Bestellungen inkl. Mengen (Schriftstücke) je Status. Keine separate
// API-Zusammenfassung mehr.
const UEBERSICHT_CRON = process.env.SKRIFT_UEBERSICHT_CRON || '0 3 * * *';
const PSTATUS_ORDER = [
['eingegangen', 'Eingegangen / Neu'],
['im_druck', 'Im Druck'],
['kuvertieren', 'Kuvertierung'],
['versandfertig', 'Versandfertig'],
['versendet', 'Versendet'],
['storniert', 'Storniert'],
];
register.schedule(UEBERSICHT_CRON, async () => {
try {
const schema = await getSchema();
const to = await teamEmpfaenger(schema);
if (!to) return;
const settings = await loadSettings(schema);
const start = new Date(); start.setHours(0, 0, 0, 0);
const orders = await new ItemsService('orders', { schema, accountability: null }).readByQuery({
filter: { _and: [{ source: { _eq: 'api' } }, { date_created: { _gte: start.toISOString() } }] },
fields: ['order_number', 'shipping_day', 'api_client.id', 'api_client.name', 'product.name'],
limit: -1, sort: ['order_number'],
fields: ['production_status', 'entries_count'], limit: -1,
});
if (!orders || !orders.length) return;
const byClient = new Map();
for (const o of orders) {
const kid = o.api_client?.id ?? 'x';
let g = byClient.get(kid);
if (!g) { g = { name: o.api_client?.name || 'API-Kunde', orders: [] }; byClient.set(kid, g); }
g.orders.push(o);
const agg = {};
let gesamtA = 0, gesamtM = 0;
for (const o of (orders || [])) {
const st = o.production_status || 'eingegangen';
if (!agg[st]) agg[st] = { auftraege: 0, menge: 0 };
agg[st].auftraege += 1; agg[st].menge += Number(o.entries_count) || 0;
gesamtA += 1; gesamtM += Number(o.entries_count) || 0;
}
for (const g of byClient.values()) {
const byDay = {};
for (const o of g.orders) { const d = o.shipping_day || 'ohne Versandtag'; byDay[d] = (byDay[d] || 0) + 1; }
const zeilen = Object.entries(byDay).map(([d, n]) => `<li>${esc(d)}: <strong>${n}</strong></li>`).join('');
const html = `<p>Heute über die API eingegangen von <strong>${esc(g.name)}</strong>: <strong>${g.orders.length}</strong> Auftrag/Aufträge.</p>`
+ `<p>Nach Versandtag:</p><ul>${zeilen}</ul>`
+ '<p>Im Produktions-Board unter „Neu" als Charge(n) gruppiert.</p>';
const nachricht = { to, subject: `API-Tagesübersicht ${g.name} – ${g.orders.length} neue`, html, text: htmlZuText(html), replyTo: REPLY_TO };
if (settings.sender_email) nachricht.from = settings.sender_name ? `${settings.sender_name} <${settings.sender_email}>` : settings.sender_email;
try { await new MailService({ schema, accountability: null }).send(nachricht); }
catch (e) { logger.warn(`[skrift-mailer] API-Tagesmail ${g.name}: ${e.message}`); }
}
logger.info(`[skrift-mailer] API-Tagesübersicht verschickt: ${byClient.size} Kunde(n)`);
} catch (e) { logger.warn(`[skrift-mailer] API-Tageslauf: ${e.message}`); }
// Bekannte Status in sinnvoller Reihenfolge, unbekannte hinten anhängen.
const reihen = [...PSTATUS_ORDER];
for (const st of Object.keys(agg)) if (!reihen.some(([k]) => k === st)) reihen.push([st, st]);
const td = 'padding:4px 10px;';
const zeilen = reihen.filter(([k]) => agg[k]).map(([k, label]) =>
`<tr><td style="${td}">${esc(label)}</td>`
+ `<td style="${td}text-align:right;"><strong>${agg[k].auftraege}</strong></td>`
+ `<td style="${td}text-align:right;">${agg[k].menge}</td></tr>`).join('');
const heute = new Date().toLocaleDateString('de-DE');
const html = `<p>Gesamtübersicht aller Aufträge (Stand ${esc(heute)}):</p>`
+ '<table style="border-collapse:collapse;font-size:14px;">'
+ `<thead><tr><th style="text-align:left;${td}border-bottom:1px solid #ccc;">Status</th>`
+ `<th style="text-align:right;${td}border-bottom:1px solid #ccc;">Aufträge</th>`
+ `<th style="text-align:right;${td}border-bottom:1px solid #ccc;">Schriftstücke</th></tr></thead>`
+ `<tbody>${zeilen || `<tr><td style="${td}" colspan="3">Keine Aufträge.</td></tr>`}</tbody>`
+ `<tfoot><tr><td style="${td}border-top:2px solid #333;"><strong>Gesamt</strong></td>`
+ `<td style="${td}text-align:right;border-top:2px solid #333;"><strong>${gesamtA}</strong></td>`
+ `<td style="${td}text-align:right;border-top:2px solid #333;"><strong>${gesamtM}</strong></td></tr></tfoot>`
+ '</table>';
const nachricht = { to, subject: `Auftragsübersicht ${heute} – ${gesamtA} Aufträge`, html, text: htmlZuText(html), replyTo: REPLY_TO };
if (settings.sender_email) nachricht.from = settings.sender_name ? `${settings.sender_name} <${settings.sender_email}>` : settings.sender_email;
await new MailService({ schema, accountability: null }).send(nachricht);
logger.info(`[skrift-mailer] Auftragsübersicht verschickt: ${gesamtA} Aufträge in ${Object.keys(agg).length} Status`);
} catch (e) { logger.warn(`[skrift-mailer] Übersicht-Tageslauf: ${e.message}`); }
});
};