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:
@@ -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.' });
|
||||
|
||||
@@ -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)))]),
|
||||
|
||||
Reference in New Issue
Block a user