Unterschriftenservice als echter Auftrag: signature-order (Seiten-SVGs schreiben + Auftrag anlegen), Kundenmail-Feld + 'Auftrag anlegen'-Button, Output-Mount schreibbar
This commit is contained in:
@@ -42,7 +42,9 @@ services:
|
||||
- ./extensions:/directus/extensions
|
||||
# Generierte Auftragsdateien read-only einblenden (gleicher Ordner wie
|
||||
# das Backend beschreibt / SFTP-Ziel) – für Direktzugriff statt Kopie.
|
||||
- /var/skrift-output:/var/skrift-output:ro
|
||||
# Schreibbar: die Unterschriftenservice-Auftragsanlage schreibt die
|
||||
# Seiten-SVGs direkt in den Auftragsordner (sonst nur lesend).
|
||||
- /var/skrift-output:/var/skrift-output
|
||||
environment:
|
||||
SECRET: ${DIRECTUS_SECRET}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const NeuAuftrag = defineComponent({
|
||||
const sig = reactive({
|
||||
modus: 'auftrag', // 'auftrag' (aus Auftrag laden) oder 'manuell' (direkt hochladen)
|
||||
orderNr: '', order: null, docs: [], docId: null, docName: '', signaturen: [],
|
||||
laden: false, pdf: '', msg: '', err: '',
|
||||
customerEmail: '', laden: false, pdf: '', msg: '', err: '',
|
||||
});
|
||||
// Datei nach Directus hochladen (liefert die Datei-ID).
|
||||
async function sigUpload(file) {
|
||||
@@ -646,6 +646,18 @@ const NeuAuftrag = defineComponent({
|
||||
sig.msg = 'ZIP heruntergeladen – A4-Signaturen an den Plotter geben.';
|
||||
} catch (e) { sig.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Erzeugung fehlgeschlagen.'; sig.msg = ''; }
|
||||
}
|
||||
// Legt einen echten Auftrag an: erzeugt je PDF-Seite eine A4-SVG (leer wo kein
|
||||
// Marker) und schreibt sie als Schriftstück. Danach im Produktions-Board unter
|
||||
// „Neu" → dort Drucken → Plotter (wie ein normaler Auftrag).
|
||||
async function sigAuftrag() {
|
||||
if (!sig.docId) { sig.err = 'Bitte ein Dokument wählen/hochladen.'; return; }
|
||||
if (!sigAktive().length) { sig.err = 'Mindestens eine Unterschrift aktivieren.'; return; }
|
||||
sig.err = ''; sig.msg = 'Auftrag wird angelegt …';
|
||||
try {
|
||||
const res = await api.post('/skrift-orders/signature-order', { ...sigPayload(), customer_email: sig.customerEmail || undefined });
|
||||
sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n). Im Produktions-Board unter „Neu" → Drucken.`;
|
||||
} catch (e) { sig.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Auftrag konnte nicht angelegt werden.'; sig.msg = ''; }
|
||||
}
|
||||
|
||||
function signaturPanel() {
|
||||
const sigFeld = (s, k, lbl, step) => h('div', { style: 'display:flex;flex-direction:column;gap:3px;flex:1;min-width:90px;' }, [
|
||||
@@ -707,8 +719,14 @@ const NeuAuftrag = defineComponent({
|
||||
sigFeld(s, 'offset_y_mm', 'Y mm'),
|
||||
]),
|
||||
])),
|
||||
h('div', { class: 'sk-btnrow', style: 'display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;' }, [
|
||||
// Kunde zuordnen (für den anzulegenden Auftrag) + Aktionen.
|
||||
h('div', { style: 'margin-top:12px;max-width:360px;' }, [
|
||||
label('Kundenmail (Auftrag zuordnen, optional)'),
|
||||
h('input', { value: sig.customerEmail, type: 'email', placeholder: 'kunde@…', style: WFELD + 'width:100%;', onInput: (e) => { sig.customerEmail = e.target.value; } }),
|
||||
]),
|
||||
h('div', { class: 'sk-btnrow', style: 'display:flex;gap:12px;flex-wrap:wrap;margin-top:12px;' }, [
|
||||
btn(sig.msg ? '…' : '👁 Vorschau', sigVorschau, 'primary'),
|
||||
btn('🖨 Auftrag anlegen', sigAuftrag, 'primary'),
|
||||
btn('⤓ A4-Signaturen (ZIP)', sigDownload),
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -245,6 +245,96 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
// A4-Signatur-SVGs als ZIP (zum Plotten in der lokalen Produktion).
|
||||
router.post('/signature-render', (req, res) => signatureAufruf(req, res, '/render', 'application/zip', 'zip_base64'));
|
||||
|
||||
// Unterschriftenservice als ECHTEN Auftrag anlegen: erzeugt je PDF-Seite eine
|
||||
// A4-SVG (leer, wo kein Marker), schreibt sie als Schriftstück in den Auftrags-
|
||||
// ordner und legt den Auftrag an. Danach läuft alles über die normale Produktion
|
||||
// (Drucken → Plotter). KEIN neuer Job-Typ. Setzt den schreibbaren Output-Mount voraus.
|
||||
router.post('/signature-order', async (req, res) => {
|
||||
const acc = req.accountability;
|
||||
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
|
||||
const body = req.body || {};
|
||||
if (!body.order && !body.document_id) return res.status(400).json({ error: 'order oder document_id erforderlich.' });
|
||||
const sigList = Array.isArray(body.signatures) ? body.signatures : [];
|
||||
if (!sigList.length) return res.status(400).json({ error: 'Mindestens eine Unterschrift wählen.' });
|
||||
try {
|
||||
const schema = await getSchema();
|
||||
const svc = (c) => new ItemsService(c, { schema, accountability: null });
|
||||
|
||||
let ord = null;
|
||||
if (body.order) {
|
||||
ord = await svc('orders').readOne(body.order, { fields: ['id', 'source_file', 'source_files'] });
|
||||
if (!ord) return res.status(404).json({ error: 'Auftrag nicht gefunden.' });
|
||||
}
|
||||
const pdfId = body.document_id || (ord ? (ord.source_file || (Array.isArray(ord.source_files) ? ord.source_files[0] : null)) : null);
|
||||
if (!pdfId) return res.status(400).json({ error: 'Kein Dokument (document_id fehlt).' });
|
||||
|
||||
const prodKey = body.product || 'unterschriftenservice';
|
||||
let product = (await svc('products').readByQuery({ filter: { key: { _eq: prodKey } }, limit: 1, fields: ['id', 'key', 'name'] }))?.[0];
|
||||
if (!product) product = (await svc('products').readByQuery({ filter: { input_mode: { _eq: 'datei' } }, limit: 1, fields: ['id', 'key', 'name'] }))?.[0];
|
||||
if (!product) return res.status(400).json({ error: 'Kein Unterschriftenservice-/Datei-Produkt gefunden.' });
|
||||
|
||||
let customerId = null, customerEmail = null;
|
||||
if (body.customer_email && isEmail(norm(body.customer_email))) {
|
||||
const u = (await new UsersService({ schema, accountability: null }).readByQuery({
|
||||
filter: { email: { _eq: norm(body.customer_email) } }, limit: 1, fields: ['id', 'email'],
|
||||
}))?.[0];
|
||||
if (u) { customerId = u.id; customerEmail = u.email; } else customerEmail = norm(body.customer_email);
|
||||
}
|
||||
|
||||
// A4-Signaturen erzeugen (eine je Seite) – JSON-Variante.
|
||||
const { form, jobCount } = await signatureForm(schema, pdfId, sigList);
|
||||
if (!jobCount) return res.status(400).json({ error: 'Keine gültige Signatur (SVG-Datei fehlt).' });
|
||||
let r;
|
||||
try { r = await fetch(`${SIGN_URL}/render-json`, { method: 'POST', body: form }); }
|
||||
catch (e) { return res.status(502).json({ error: 'Signatur-Service nicht erreichbar.', detail: `${SIGN_URL} – ${e.message}` }); }
|
||||
if (!r.ok) {
|
||||
let d = ''; try { d = (await r.json())?.detail || ''; } catch { /* ignore */ }
|
||||
return res.status(502).json({ error: 'Signatur-Service-Fehler (Marker im PDF?).', detail: String(Array.isArray(d) ? d.join('; ') : d).slice(0, 400) });
|
||||
}
|
||||
const pages = (await r.json())?.pages || [];
|
||||
if (!pages.length) return res.status(422).json({ error: 'Keine Seiten erzeugt.' });
|
||||
|
||||
const jt = new Date();
|
||||
const praefix = `${String(jt.getDate()).padStart(2, '0')}-${String(jt.getMonth() + 1).padStart(2, '0')}-${String(jt.getFullYear()).slice(2)}`;
|
||||
const naechsteNummer = async () => {
|
||||
const heutige = await svc('orders').readByQuery({ filter: { order_number: { _starts_with: `${praefix}-` } }, limit: -1, fields: ['id'] });
|
||||
return `${praefix}-${String((heutige?.length ?? 0) + 1).padStart(3, '0')}`;
|
||||
};
|
||||
const orderData = {
|
||||
customer: customerId, customer_email: customerEmail, product: product.id,
|
||||
person_type: 'privat', source: 'operator', status: 'in_queue', production_status: 'eingegangen',
|
||||
payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel',
|
||||
needs_envelope: false, envelope_labeling: 'keine',
|
||||
source_file: pdfId, source_files: [pdfId],
|
||||
entries_count: pages.length, count_inland: pages.length, count_ausland: 0,
|
||||
};
|
||||
let orderId, orderNumber;
|
||||
for (let v = 0; v < 6; v += 1) {
|
||||
orderNumber = await naechsteNummer();
|
||||
try { orderId = await svc('orders').createOne({ order_number: orderNumber, ...orderData }); break; }
|
||||
catch (e) { if (v === 5 || !/unique|duplicate|bereits|exists/i.test(String(e?.message || ''))) throw e; }
|
||||
}
|
||||
|
||||
const entriesSvc = svc('order_entries');
|
||||
for (let i = 0; i < pages.length; i += 1) {
|
||||
await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` });
|
||||
}
|
||||
await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Unterschriftenservice – manuell angelegt.' }).catch(() => {});
|
||||
|
||||
// Seiten-SVGs als Schriftstück in den Auftragsordner schreiben.
|
||||
const ordner = path.join(OUTPUT_DIR, orderNumber);
|
||||
fs.mkdirSync(ordner, { recursive: true });
|
||||
pages.forEach((p, i) => {
|
||||
fs.writeFileSync(path.join(ordner, `letter_${String(i + 1).padStart(3, '0')}.svg`), p.svg, 'utf8');
|
||||
});
|
||||
|
||||
return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length });
|
||||
} catch (err) {
|
||||
logger.error(`[skrift-orders] signature-order: ${err.stack || err.message}`);
|
||||
return res.status(500).json({ error: 'Auftrag konnte nicht angelegt werden.', detail: String(err.message || err).slice(0, 400) });
|
||||
}
|
||||
});
|
||||
|
||||
// Policy-konformes Zufallspasswort (Kunde meldet sich ohnehin passwortlos an).
|
||||
const zufallsPasswort = () =>
|
||||
`Aa1!${crypto.randomBytes(24).toString('base64url').replace(/[^A-Za-z0-9]/g, '')}`;
|
||||
|
||||
Reference in New Issue
Block a user