Kontakt-CRM, Chargen-Sammeldruck, API-Status & Auto-Generierung
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>
This commit is contained in:
@@ -127,8 +127,10 @@ export default (register, { services, getSchema, logger }) => {
|
||||
if (!to) return;
|
||||
const settings = await loadSettings(schema);
|
||||
const order = await new ItemsService('orders', { schema, accountability: null })
|
||||
.readOne(orderId, { fields: ['order_number', 'product.name'] });
|
||||
.readOne(orderId, { fields: ['order_number', 'product.name', 'source'] });
|
||||
if (!order) return;
|
||||
// API-Aufträge NICHT einzeln melden – dafür gibt es die Tagesübersicht (18:00).
|
||||
if (order.source === 'api') return;
|
||||
const html = `<p>Neue Bestellung <strong>${order.order_number}</strong>${order.product?.name ? ` (${order.product.name})` : ''} – im Produktions-Board unter „Neu".</p>`;
|
||||
const nachricht = {
|
||||
to,
|
||||
@@ -245,4 +247,45 @@ export default (register, { services, getSchema, logger }) => {
|
||||
catch (e) { logger.warn(`[skrift-mailer] status ${status}: ${e.message}`); }
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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 () => {
|
||||
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'],
|
||||
});
|
||||
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);
|
||||
}
|
||||
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}`); }
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user