Muster-Workflow: Mustertext wählen, Adresse aufs Kuvert, Platzhalter füllen

- Produktion-Muster-Dialog: benannten Mustertext (muster_texts) wählen; Platzhalter
  ([[Vorname]]/[[Nachname]]/[[Ort]]/[[Strasse]]/[[PLZ]]/[[Land]]) aus der Anfrage-
  Adresse automatisch füllen; Text vor dem Anlegen bearbeitbar; „Adresse aufs
  Kuvert" (Standard an) mit Format/Template-Wahl.
- manual-print: erzeugt optional zusätzlich ein Kuvert (Umschlag-Job) mit der Adresse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-11 21:11:08 +02:00
parent 365158c43b
commit 54de08448a
2 changed files with 106 additions and 20 deletions

View File

@@ -746,7 +746,7 @@ const handler = (router, { services, getSchema, logger }) => {
letters.push({
index: nr, type: 'postcard', text: combined, format: briefFormat, font,
realisticHandwriting: realistisch, placeholders: platzhalterVon(e, i),
fontScale: fsLetter, senderScale: Number(body.envelope_sender_scale) || 1,
fontScale: fsLetter, senderScale: Number(body.envelope_sender_scale) || 1, recipientScale: fsEnv,
});
return;
}
@@ -1241,12 +1241,23 @@ const handler = (router, { services, getSchema, logger }) => {
const font = ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda';
const format = TEST_FORMAT[body.format] || 'a4';
// Kuvert (Adresse aus der Anfrage) optional mitdrucken.
const ENV_FMT = { dinlang: 'din_lang', c6: 'c6' };
const envAddr = clean(String(body.envelope || '')).trim();
const wantsEnv = !!(envAddr && body.envelope_template);
const envFormat = ENV_FMT[body.envelope_format] || 'din_lang';
const letters = [{ index: 1, type: 'letter', text: clean(String(body.text)), format, font, realisticHandwriting: body.realistic !== false }];
if (wantsEnv) {
letters.push({ index: 1, type: 'envelope', envelopeType: 'recipient', text: envAddr, format: envFormat, font, realisticHandwriting: body.realistic !== false });
}
const gen = await fetch(`${BACKEND_URL}/api/order/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
body: JSON.stringify({
orderNumber: ref,
letters: [{ index: 1, type: 'letter', text: clean(String(body.text)), format, font, realisticHandwriting: body.realistic !== false }],
letters,
metadata: { rawText: String(body.text) },
}),
});
@@ -1265,10 +1276,17 @@ const handler = (router, { services, getSchema, logger }) => {
machine: body.machine, template: body.template, type: 'brief', status: 'queued', output_ref: ref, priority: prio,
format: body.format || 'a4', format_label: LETTER_LABEL[body.format] || body.format || 'A4',
});
// Zweiter Job für das Kuvert (gleicher Ausgabeordner, Typ umschlag).
if (wantsEnv) {
await svc('jobs').createOne({
machine: body.machine, template: body.envelope_template, type: 'umschlag', status: 'queued', output_ref: ref, priority: prio,
format: body.envelope_format || 'dinlang', format_label: ENV_LABEL[body.envelope_format] || 'Kuvert',
});
}
if (body.offer_request) {
try { await svc('offer_requests').updateOne(body.offer_request, { status: 'in_bearbeitung' }); } catch { /* nicht kritisch */ }
}
return res.json({ ok: true, ref, job, mode: prio ? 'direct' : 'queue', busy });
return res.json({ ok: true, ref, job, mode: prio ? 'direct' : 'queue', busy, kuvert: wantsEnv });
} catch (err) {
logger.error(`[skrift-orders] manual-print: ${err.stack || err.message}`);
return res.status(500).json({ error: 'Manueller Druck fehlgeschlagen.' });

View File

@@ -179,10 +179,14 @@ const Board = defineComponent({
const machines = ref([]);
const templates = ref([]);
const muster = ref({ text_du: '', text_sie: '' });
// Benannte Mustertexte (Collection muster_texts) – im Muster-Dialog wählbar.
const musterListe = ref([]);
// ── Modale ──
const druck = reactive({ open: false, order: null, machine: null, mode: 'queue', busy: false, brief: null, umschlag: null, msg: '', err: '', deNums: '', intNums: '', deN: 0, intN: 0 });
const manuell = reactive({ open: false, req: null, form: 'sie', font: 'tilda', format: 'a4', template: null, machine: null, mode: 'queue', text: '', msg: '', err: '' });
const manuell = reactive({ open: false, req: null, form: 'sie', vorlageId: '', roh: '', font: 'tilda', format: 'a4', template: null, machine: null, mode: 'queue', text: '',
// Adresse automatisch aufs Kuvert (Standard an) + eigenes Kuvert-Template/-Format.
kuvert: true, envFormat: 'dinlang', envTemplate: null, msg: '', err: '' });
const empf = reactive({ open: false, mode: 'order', titel: '', order: null, suche: '', rows: [], files: [],
info: '', laden: false, selected: null, pl: '', pe: '', pLoad: false, perr: '', lb: '', lbTitel: '' });
// Massenauswahl (Auftrags-IDs) + Sammel-Druck-Popup (pro Format Template+Maschine).
@@ -337,7 +341,7 @@ const Board = defineComponent({
// leise = Hintergrund-Aktualisierung: kein Spinner, kein Fehlerbanner.
if (!leise) { loading.value = true; fehler.value = ''; }
try {
const [o, r, j, m, t, ms] = await Promise.all([
const [o, r, j, m, t, ms, mtx] = await Promise.all([
ladeOrders(),
api.get('/items/offer_requests', { params: {
limit: -1, sort: '-date_created',
@@ -348,6 +352,7 @@ const Board = defineComponent({
ladeMachines(),
api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format' } }),
api.get('/items/muster_settings', { params: { fields: 'text_du,text_sie' } }).catch(() => ({ data: { data: { text_du: '', text_sie: '' } } })),
api.get('/items/muster_texts', { params: { limit: -1, filter: { active: { _eq: true } }, sort: 'sort', fields: 'id,label,text' } }).catch(() => ({ data: { data: [] } })),
]);
orders.value = o.data?.data || [];
requests.value = r.data?.data || [];
@@ -355,6 +360,7 @@ const Board = defineComponent({
machines.value = (m.data?.data || []);
templates.value = t.data?.data || [];
muster.value = ms.data?.data || { text_du: '', text_sie: '' };
musterListe.value = mtx.data?.data || [];
// Empfänger-Index nur beim vollen Laden neu bauen (nicht bei der 15-s-
// Hintergrund-Aktualisierung) – im Hintergrund, blockiert das Board nicht.
if (!leise) { ladeEmpfIndex(); ladeSendIndex(); ladeChargeNotizen(); }
@@ -685,38 +691,83 @@ const Board = defineComponent({
}
// ── Muster: manueller Druck ──
function musterAufloesen() {
const roh = manuell.form === 'du' ? muster.value.text_du : muster.value.text_sie;
// Namensteile der Anfrage (Adresse hat Vorrang vor dem freien Namensfeld).
function musterNamen() {
const a = manuell.req?.address && typeof manuell.req.address === 'object' ? manuell.req.address : {};
const name = manuell.req?.name || '';
const teile = name.split(' ');
const vor = teile[0] || ''; const nach = teile.slice(1).join(' ');
manuell.text = String(roh || '')
.replace(/\[\[Vorname\]\]/gi, a.first_name || a.vorname || vor)
.replace(/\[\[Nachname\]\]/gi, a.last_name || a.nachname || nach)
.replace(/\[\[Anrede\]\]/gi, a.salutation || a.anrede || '')
.replace(/\[\[Name\]\]/gi, name);
return {
anrede: a.salutation || a.anrede || '',
vor: a.first_name || a.vorname || teile[0] || '',
nach: a.last_name || a.nachname || teile.slice(1).join(' '),
strasse: a.street || a.strasse || '',
plz: a.zip || a.plz || '',
ort: a.city || a.stadt || a.ort || '',
land: a.country || a.land || '',
firma: a.company || a.firma || '',
name,
};
}
// Adressblock fürs Kuvert (Empfänger). Land nur bei Ausland.
function musterAdresse() {
const n = musterNamen();
const person = [n.anrede, n.vor, n.nach].filter(Boolean).join(' ') || n.name;
const ort = [n.plz, n.ort].filter(Boolean).join(' ');
const land = (n.land && !/^(de|deu|d|deutschland|germany)$/i.test(String(n.land).trim())) ? n.land : '';
return [n.firma, person, n.strasse, ort, land].filter(Boolean).join('\n');
}
// Platzhalter im gewählten Rohtext füllen → manuell.text.
function musterAufloesen() {
const n = musterNamen();
manuell.text = String(manuell.roh || '')
.replace(/\[\[Vorname\]\]/gi, n.vor)
.replace(/\[\[Nachname\]\]/gi, n.nach)
.replace(/\[\[Anrede\]\]/gi, n.anrede)
.replace(/\[\[Strasse\]\]/gi, n.strasse)
.replace(/\[\[PLZ\]\]/gi, n.plz)
.replace(/\[\[Ort\]\]/gi, n.ort)
.replace(/\[\[Stadt\]\]/gi, n.ort)
.replace(/\[\[Land\]\]/gi, n.land)
.replace(/\[\[Name\]\]/gi, n.name);
}
// Vorlage wählen: 'du'/'sie' (aus muster_settings) oder eine muster_texts-ID.
function musterVorlage(wert) {
manuell.vorlageId = wert;
if (wert === 'du') manuell.roh = muster.value.text_du || '';
else if (wert === 'sie') manuell.roh = muster.value.text_sie || '';
else manuell.roh = (musterListe.value.find((m) => String(m.id) === String(wert)) || {}).text || '';
musterAufloesen();
}
function openMuster(req) {
manuell.req = req; manuell.form = 'sie'; manuell.font = 'tilda'; manuell.format = 'a4';
manuell.req = req; manuell.font = 'tilda'; manuell.format = 'a4';
manuell.template = tplByKey('a4_single', 'brief')?.id ?? templates.value.find((t) => t.type === 'brief')?.id ?? null;
manuell.machine = aktiveMaschinen.value[0]?.id ?? null; manuell.mode = 'queue';
// Kuvert automatisch: A4 → DIN Lang, sonst C6; passendes Umschlag-Template.
manuell.kuvert = true;
manuell.envFormat = 'dinlang';
manuell.envTemplate = tplForFormat('dinlang', 'umschlag')?.id ?? templates.value.find((t) => t.type === 'umschlag')?.id ?? null;
manuell.msg = ''; manuell.err = '';
musterAufloesen();
// Erste benannte Vorlage bevorzugen, sonst Sie-Form.
musterVorlage(musterListe.value[0] ? musterListe.value[0].id : 'sie');
manuell.open = true;
}
async function submitMuster() {
if (!manuell.text.trim()) { manuell.err = 'Text fehlt.'; return; }
if (!manuell.template || !manuell.machine) { manuell.err = 'Template und Maschine wählen.'; return; }
if (manuell.kuvert && !manuell.envTemplate) { manuell.err = 'Kuvert-Template wählen (oder Kuvert deaktivieren).'; return; }
manuell.err = ''; manuell.msg = 'Wird erzeugt …';
try {
const res = await api.post('/skrift-orders/manual-print', {
text: manuell.text, font: manuell.font, format: manuell.format,
template: manuell.template, machine: manuell.machine, mode: manuell.mode,
offer_request: manuell.req?.id || null,
// Adresse automatisch aufs Kuvert.
envelope: manuell.kuvert ? musterAdresse() : '',
envelope_format: manuell.kuvert ? manuell.envFormat : '',
envelope_template: manuell.kuvert ? manuell.envTemplate : null,
});
const modus = res.data?.mode === 'direct' ? 'Direktdruck' : 'Warteschlange';
flash(`Muster erzeugt & eingereiht (${modus}).`);
flash(`Muster erzeugt & eingereiht (${modus})${manuell.kuvert ? ' inkl. Kuvert' : ''}.`);
manuell.open = false;
await laden();
} catch (e) { manuell.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Fehlgeschlagen.'; manuell.msg = ''; }
@@ -1273,13 +1324,30 @@ const Board = defineComponent({
function musterModal() {
if (!manuell.open) return null;
const briefTpls = templates.value.filter((t) => t.type === 'brief');
const umschlagTpls = templates.value.filter((t) => t.type === 'umschlag');
const body = h('div', { style: 'padding:18px 20px;display:flex;flex-direction:column;gap:12px;' }, [
h('div', { style: 'display:flex;gap:16px;align-items:center;' }, [
h('label', { style: 'display:flex;align-items:center;gap:6px;' }, [h('input', { type: 'radio', name: 'anrede', checked: manuell.form === 'sie', onChange: () => { manuell.form = 'sie'; musterAufloesen(); } }), 'Sie-Form']),
h('label', { style: 'display:flex;align-items:center;gap:6px;' }, [h('input', { type: 'radio', name: 'anrede', checked: manuell.form === 'du', onChange: () => { manuell.form = 'du'; musterAufloesen(); } }), 'Du-Form']),
subdued('Adresse aus der Anfrage übernommen · [[abschnitt]] bitte füllen'),
// Vorlage wählen (benannte Mustertexte + Du/Sie). Platzhalter werden gefüllt.
h('div', { style: 'display:flex;gap:10px;align-items:center;flex-wrap:wrap;' }, [
h('label', { style: 'font-size:12px;color:var(--foreground-subdued);font-weight:600;' }, 'Mustertext:'),
h('select', { style: S_SELECT + 'flex:1;min-width:220px;', value: String(manuell.vorlageId), onChange: (e) => musterVorlage(e.target.value) }, [
...musterListe.value.map((m) => h('option', { value: String(m.id), selected: String(manuell.vorlageId) === String(m.id) }, m.label || `Text ${m.id}`)),
h('option', { value: 'sie', selected: manuell.vorlageId === 'sie' }, 'Standard Sie-Form'),
h('option', { value: 'du', selected: manuell.vorlageId === 'du' }, 'Standard Du-Form'),
]),
subdued('Platzhalter aus der Anfrage-Adresse automatisch gefüllt · vor dem Anlegen bearbeitbar'),
]),
h('textarea', { style: S_INPUT + 'width:100%;min-height:200px;font-family:var(--family-monospace,monospace);', value: manuell.text, onInput: (e) => { manuell.text = e.target.value; } }),
// Kuvert automatisch mit der Adresse aus der Anfrage.
h('div', { style: 'display:flex;gap:12px;align-items:center;flex-wrap:wrap;background:var(--background-subdued,#f1f4f9);border-radius:8px;padding:10px 12px;' }, [
h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;font-weight:600;' }, [
h('input', { type: 'checkbox', checked: manuell.kuvert, onChange: (e) => { manuell.kuvert = e.target.checked; }, style: 'width:16px;height:16px;' }),
'✉ Adresse aufs Kuvert',
]),
manuell.kuvert ? h('select', { style: S_SELECT + 'width:auto;', value: manuell.envFormat, onChange: (e) => { manuell.envFormat = e.target.value; manuell.envTemplate = tplForFormat(e.target.value, 'umschlag')?.id ?? manuell.envTemplate; } },
[['dinlang', 'DIN Lang'], ['c6', 'C6']].map(([v, l]) => h('option', { value: v, selected: manuell.envFormat === v }, l))) : null,
manuell.kuvert ? h('select', { style: S_SELECT + 'flex:1;min-width:160px;', value: manuell.envTemplate, onChange: (e) => { manuell.envTemplate = e.target.value; } }, umschlagTpls.map((t) => h('option', { value: t.id, selected: String(manuell.envTemplate) === String(t.id) }, t.name))) : null,
manuell.kuvert ? subdued('→ ' + (musterAdresse().replace(/\n/g, ', ') || 'keine Adresse')) : null,
]),
h('div', { style: 'display:grid;grid-template-columns:repeat(4,1fr);gap:8px;' }, [
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Schrift'),
h('select', { style: S_SELECT + 'width:100%;', value: manuell.font, onChange: (e) => { manuell.font = e.target.value; } }, ['tilda', 'alva', 'ellie'].map((f) => h('option', { value: f, selected: manuell.font === f }, f)))]),