Produktions-Board: - Massenauswahl (+ "Alle auswählen") mit Sammel-Druck und Sammel-Status - Warteschlange je Maschine nach Format gebündelt; Formatabfrage (machines.current_format) statt automatischem Anschluss - resiliente Ladeabfragen (Board bleibt ohne neue Felder nutzbar) Agent (skrift-agent): - druckt strikt ein Dokument nach dem anderen (Idle-Gate) -> behebt Verlust von Folgejobs während eines laufenden Drucks - druckt nur Jobs des eingelegten Formats (current_format), kein Auto-Anschluss - Heartbeat gedrosselt (nur bei Statuswechsel / alle 60 s) Neues Modul "Manueller Druck" (skrift-neuauftrag): - interner Auftrag ohne Zahlung, Vorschau, Platzhalter/Excel-Import, Felder bleiben nach dem Absenden stehen Backend/Endpunkte (skrift-orders): - jobs tragen format/format_label; /enqueue-bulk für Sammeldruck - Webhook /inbound/:kind (JetFormBuilder) mit Token + tolerantem Body-Parser - Kleinmengen-Aufschlag je Produkt über Faktoren a (Deckel) und e (Exponent) - Fix: norm()-Shadowing in calculatePrice (norm is not a function) Bootstrap: - jobs.format/format_label, machines.current_format - products.price_factor_a/e; Multiplikator-Formel auf a/e umgestellt inkl. verhaltensneutraler Migration bestehender Formel/Produkte skrift-app (Konfigurator): - Zeilenumbrüche im Textfeld erlaubt; Umschlag-Individualtext als Textfeld - Platzhalter-Tabelle im Text-Schritt immer verfügbar (auch Sammelversand ohne Kuvert) inkl. Excel-Upload nur für Platzhalterwerte - Danke-Seite: Button zurück zu skrift.de - z-index-Fix: Platzhalter-Tabelle überlagert die Vorschau nicht mehr WordPress-Preisrechner: a/e-Faktoren in PHP- und JS-Rechnung Directus: SKRIFT_WEBHOOK_TOKEN in compose/.env.example Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
352 lines
22 KiB
JavaScript
352 lines
22 KiB
JavaScript
/**
|
||
* Skrift – Modul „Neuer Auftrag"
|
||
* ===========================================================================
|
||
* Eigenständige, manuelle Auftragsanlage im Admin (eigenes Modul, kein Teil des
|
||
* Produktions-Boards). Nutzt ausschließlich die zentralen Backend-Endpunkte:
|
||
* POST /skrift-orders/preview – Vorschau (Admin ohne Kontingent)
|
||
* POST /skrift-orders/manual-order – Auftrag intern anlegen (keine Zahlung)
|
||
* Der Auftrag erscheint danach als „eingegangen" im Produktions-Board.
|
||
*
|
||
* Von Hand als ESM (kein Build-Schritt) – Vue + SDK stellt Directus bereit.
|
||
*/
|
||
import { defineComponent, ref, reactive, onMounted, h, resolveComponent, Teleport } from 'vue';
|
||
import { useApi } from '@directus/extensions-sdk';
|
||
|
||
const ACCENT = '#1E50A5';
|
||
// Weißes, randloses Eingabefeld – hebt sich durch Kontrast + leichten Schatten
|
||
// vom hellblauen Bereich ab (kein Rahmen).
|
||
const WFELD = 'background:#fff;border:none;border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;color:#1A1A1A;outline:none;box-shadow:0 1px 2px rgba(0,0,0,.08);';
|
||
const WSELECT = WFELD + 'cursor:pointer;';
|
||
// Hellblauer Bereich (kein Rahmen).
|
||
const BEREICH = 'background:rgba(30,80,165,.06);border-radius:12px;padding:18px 20px;';
|
||
const STANDARD_PLATZHALTER = ['Anrede', 'Vorname', 'Nachname', 'Strasse', 'PLZ', 'Ort', 'Land', 'Briefnummer'];
|
||
|
||
function btn(label, onClick, kind) {
|
||
const base = 'cursor:pointer;border-radius:8px;padding:8px 16px;font:inherit;font-size:14px;border:none;';
|
||
const style = kind === 'primary' ? base + `background:${ACCENT};color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15);`
|
||
: base + `background:#fff;color:${ACCENT};box-shadow:0 1px 2px rgba(0,0,0,.1);`;
|
||
return h('button', { type: 'button', onClick, style }, label);
|
||
}
|
||
function subdued(t, extra) { return h('div', { style: `color:var(--foreground-subdued);font-size:13px;${extra || ''}` }, t); }
|
||
function segToggle(value, opts, onSel) {
|
||
return h('div', { style: 'display:flex;width:100%;border-radius:8px;overflow:hidden;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.08);' },
|
||
opts.map((o) => h('button', {
|
||
type: 'button', onClick: () => onSel(o.key),
|
||
style: `flex:1;padding:10px 8px;font:inherit;font-size:13px;text-align:center;cursor:pointer;border:none;`
|
||
+ (value === o.key ? `background:${ACCENT};color:#fff;font-weight:700;` : `background:transparent;color:${ACCENT};`),
|
||
}, o.label)));
|
||
}
|
||
function bereichKopf(txt) { return h('div', { style: `font-size:15px;font-weight:800;color:${ACCENT};margin-bottom:14px;` }, txt); }
|
||
function label(txt) { return h('label', { style: 'display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);margin-bottom:4px;' }, txt); }
|
||
|
||
const NeuAuftrag = defineComponent({
|
||
setup() {
|
||
const api = useApi();
|
||
const produkte = ref([]);
|
||
const formate = ref([]);
|
||
const verbotenRe = ref(null);
|
||
const musterTexte = ref({ text_du: '', text_sie: '' });
|
||
const geladen = ref(false);
|
||
|
||
const leereZeile = () => ({ free_text: '', placeholders: {} });
|
||
const neu = reactive({
|
||
product: '', format: '', font: 'tilda',
|
||
// Kuvert-Adresse je Empfänger: '5zeilen' (5 Zeilen) oder 'individuell' (einzeilig).
|
||
adressModus: '5zeilen', kuverts: false, realistic: true, customer_email: '', text: '', rows: [leereZeile()],
|
||
importOpen: false, importText: '',
|
||
docs: [], previewSession: null, pLaedt: false, msg: '', err: '', hinweis: '', lb: '',
|
||
});
|
||
// Kuverts nur generieren, wenn angehakt (Beschriftung aus der Empfängeradresse).
|
||
const kuvertPayload = () => (neu.kuverts
|
||
? { needs_envelope: true, envelope_labeling: 'empfaenger', envelope_text: '' }
|
||
: { needs_envelope: false, envelope_labeling: 'keine', envelope_text: '' });
|
||
|
||
async function laden() {
|
||
try {
|
||
const [p, f, ps, mt] = await Promise.all([
|
||
api.get('/items/products', { params: { limit: -1, filter: { active: { _eq: true }, type: { _in: ['letter', 'postcard'] } }, fields: 'key,name,type' } }),
|
||
api.get('/items/formats', { params: { limit: -1, filter: { active: { _eq: true } }, fields: 'key,name,product.key' } }),
|
||
api.get('/items/pricing_settings', { params: { fields: 'char_whitelist' } }).catch(() => ({ data: { data: {} } })),
|
||
api.get('/items/muster_settings', { params: { fields: 'text_du,text_sie' } }).catch(() => ({ data: { data: {} } })),
|
||
]);
|
||
produkte.value = p.data?.data || [];
|
||
formate.value = f.data?.data || [];
|
||
musterTexte.value = mt.data?.data || { text_du: '', text_sie: '' };
|
||
const wl = ps.data?.data?.char_whitelist;
|
||
// Zeilenumbrüche immer erlauben (Brieftext ist mehrzeilig). Einzeilige
|
||
// Felder entfernen \n/\r ohnehin separat, daher hier gefahrlos.
|
||
try { verbotenRe.value = wl ? new RegExp(`[^${wl}\\n\\r]`, 'g') : null; } catch { verbotenRe.value = null; }
|
||
neu.product = produkte.value[0]?.key || 'briefe';
|
||
neu.format = neuFormate()[0]?.key || '';
|
||
} catch (e) {
|
||
neu.err = e?.response?.data?.errors?.[0]?.message || e?.message || 'Laden fehlgeschlagen.';
|
||
} finally { geladen.value = true; }
|
||
}
|
||
onMounted(laden);
|
||
|
||
const saeubere = (v) => {
|
||
if (!verbotenRe.value) return v;
|
||
const treffer = String(v).match(verbotenRe.value);
|
||
if (!treffer || !treffer.length) return v;
|
||
const einmalig = [...new Set(treffer)].map((c) => (c === ' ' ? '␣' : c)).join(' ');
|
||
neu.hinweis = `Zeichen ${einmalig} ist nicht erlaubt und wurde entfernt.`;
|
||
setTimeout(() => { if (neu.hinweis.includes(einmalig)) neu.hinweis = ''; }, 4000);
|
||
return String(v).replace(verbotenRe.value, '');
|
||
};
|
||
// Alle aktiven Formate zur Auswahl (nicht aufs Produkt eingeschränkt) –
|
||
// so lassen sich auch freie/eigene Formate direkt wählen.
|
||
const neuFormate = () => formate.value;
|
||
// Wie viele Adresszeilen je Empfänger: 5 („5 Zeilen") bzw. 1 („Individueller Text").
|
||
const zeilenAnzahl = () => (neu.adressModus === 'individuell' ? 1 : 5);
|
||
const freitext = (e, z) => (String(e.free_text || '').split('\n')[z] ?? '');
|
||
const setFreitext = (i, z, v) => {
|
||
const max = zeilenAnzahl();
|
||
const zeilen = String(neu.rows[i].free_text || '').split('\n');
|
||
while (zeilen.length < max) zeilen.push('');
|
||
zeilen[z] = saeubere(String(v).replace(/[\r\n]/g, ''));
|
||
neu.rows[i].free_text = zeilen.slice(0, max).join('\n').replace(/\n+$/, '');
|
||
neu.previewSession = null;
|
||
};
|
||
// Individuelle Platzhalter je Empfänger (z. B. [[Gutschein]]).
|
||
const setPlatzhalter = (i, name, v) => {
|
||
neu.rows[i].placeholders = { ...(neu.rows[i].placeholders || {}), [name]: saeubere(v) };
|
||
neu.previewSession = null;
|
||
};
|
||
// Platzhalter aus dem Brieftext ziehen (ohne den automatischen [[Briefnummer]]).
|
||
const customPlaceholders = () => {
|
||
const set = new Set();
|
||
for (const m of String(neu.text || '').matchAll(/\[\[([^\]]+)\]\]/g)) {
|
||
const name = m[1].trim();
|
||
if (name && name.toLowerCase() !== 'briefnummer') set.add(name);
|
||
}
|
||
return [...set];
|
||
};
|
||
const addZeile = () => neu.rows.push(leereZeile());
|
||
const delZeile = (i) => { neu.rows.splice(i, 1); if (!neu.rows.length) neu.rows.push(leereZeile()); };
|
||
// Empfänger zählt, wenn eine Adresszeile ODER ein Platzhalterwert gefüllt ist.
|
||
const entries = () => neu.rows.filter((e) => String(e.free_text || '').trim()
|
||
|| (e.placeholders && Object.values(e.placeholders).some((v) => String(v || '').trim())));
|
||
|
||
// Aus Excel/CSV eingefügte Zeilen übernehmen. Erst die individuellen
|
||
// Platzhalter-Spalten, dann die Adresszeilen (Tab/Semikolon/Komma-getrennt).
|
||
function importUebernehmen() {
|
||
const ph = customPlaceholders();
|
||
const max = zeilenAnzahl();
|
||
const zeilen = String(neu.importText || '').split(/\r?\n/).map((z) => z.trim()).filter(Boolean);
|
||
if (!zeilen.length) { neu.importOpen = false; return; }
|
||
const rows = zeilen.map((z) => {
|
||
const zellen = z.split(/\t|;|,/).map((c) => saeubere(c.trim()));
|
||
const row = leereZeile();
|
||
ph.forEach((name, i) => { row.placeholders = { ...row.placeholders, [name]: zellen[i] || '' }; });
|
||
row.free_text = zellen.slice(ph.length, ph.length + max).join('\n');
|
||
return row;
|
||
});
|
||
neu.rows = rows;
|
||
neu.importText = ''; neu.importOpen = false; neu.previewSession = null;
|
||
}
|
||
|
||
async function preview() {
|
||
// Vorschau ohne Empfänger erlaubt – das Backend nutzt dann eine Blankozeile.
|
||
if (!neu.text.trim()) { neu.err = 'Bitte zuerst einen Text eingeben.'; return; }
|
||
neu.pLaedt = true; neu.err = ''; neu.msg = '';
|
||
try {
|
||
const kv = kuvertPayload();
|
||
const res = await api.post('/skrift-orders/preview', {
|
||
text: neu.text, font: neu.font, format: neu.format, realistic: neu.realistic,
|
||
envelope_labeling: kv.envelope_labeling, envelope_text: kv.envelope_text,
|
||
entries: entries().slice(0, 3),
|
||
});
|
||
const d = res.data || {};
|
||
if (d.has_overflow) { neu.err = 'Text ist zu lang für das Format – bitte kürzen.'; neu.docs = []; neu.previewSession = null; }
|
||
else { neu.docs = d.docs || d.previews || []; neu.previewSession = d.session || d.sessionId || null; }
|
||
} catch (e) { neu.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Vorschau fehlgeschlagen.'; }
|
||
finally { neu.pLaedt = false; }
|
||
}
|
||
async function absenden() {
|
||
// Empfänger sind optional – ohne Empfänger genau ein Schriftstück (Blankozeile).
|
||
const ent = entries();
|
||
const payloadEntries = ent.length ? ent : [leereZeile()];
|
||
neu.err = ''; neu.msg = 'Wird angelegt …';
|
||
try {
|
||
const kv = kuvertPayload();
|
||
const res = await api.post('/skrift-orders/manual-order', {
|
||
product: neu.product, format: neu.format, font: neu.font,
|
||
needs_envelope: kv.needs_envelope, envelope_labeling: kv.envelope_labeling, envelope_text: kv.envelope_text,
|
||
shipping_type: 'sammel',
|
||
customer_email: neu.customer_email.trim() || undefined,
|
||
text_template: neu.text, preview_session: neu.previewSession || undefined,
|
||
entries: payloadEntries,
|
||
});
|
||
const nr = res.data?.order_number || '';
|
||
// Felder bewusst NICHT zurücksetzen – der letzte Auftrag bleibt stehen,
|
||
// damit er sich als Vorlage für den nächsten leicht anpassen lässt.
|
||
neu.msg = `✓ Auftrag ${nr} angelegt – liegt im Produktions-Board unter „Neu". Felder bleiben stehen; mit „Formular leeren" zurücksetzen.`;
|
||
} catch (e) { neu.err = e?.response?.data?.error || e?.message || 'Anlegen fehlgeschlagen.'; neu.msg = ''; }
|
||
}
|
||
function zuruecksetzen() {
|
||
Object.assign(neu, {
|
||
product: produkte.value[0]?.key || 'briefe', format: neuFormate()[0]?.key || '', font: 'tilda',
|
||
adressModus: '5zeilen', kuverts: false, realistic: true, customer_email: '', text: '', rows: [leereZeile()],
|
||
importOpen: false, importText: '',
|
||
docs: [], previewSession: null, pLaedt: false, msg: '', err: '', hinweis: '', lb: '',
|
||
});
|
||
}
|
||
const musterEinfuegen = (form) => {
|
||
const t = form === 'du' ? musterTexte.value.text_du : musterTexte.value.text_sie;
|
||
if (!t) return;
|
||
neu.text = neu.text.trim() ? `${neu.text}\n\n${t}` : t;
|
||
neu.previewSession = null;
|
||
};
|
||
|
||
// Weißes Dropdown / Eingabefeld.
|
||
const sel = (val, opts, on, extra) => h('select', { style: WSELECT + 'width:100%;' + (extra || ''), value: val, onChange: on },
|
||
opts.map((o) => h('option', { value: o.v, selected: String(val) === String(o.v) }, o.t)));
|
||
const svgUri = (svg) => { try { return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); } catch { return null; } };
|
||
const docBild = (d) => {
|
||
const svg = d && typeof d.svg === 'string' ? d.svg : null;
|
||
const src = svg ? svgUri(svg) : (typeof d === 'string' ? d : (d?.dataUrl || d?.url || d?.image || null));
|
||
const txt = (d?.art === 'umschlag' ? '✉ Kuvert' : '📄 Schriftstück');
|
||
return h('div', { style: 'display:flex;flex-direction:column;align-items:center;gap:4px;' }, [
|
||
src ? h('img', { src, onClick: () => { neu.lb = src; }, style: 'width:100%;max-height:260px;object-fit:contain;border-radius:6px;background:#fff;cursor:zoom-in;box-shadow:0 1px 3px rgba(0,0,0,.12);' })
|
||
: h('div', { style: 'width:100%;height:200px;border-radius:6px;display:flex;align-items:center;justify-content:center;background:#fff;color:var(--foreground-subdued);' }, 'Dok.'),
|
||
h('span', { style: 'font-size:11px;color:var(--foreground-subdued);' }, txt),
|
||
]);
|
||
};
|
||
// Weißes, einzeiliges Zellenfeld (feste Breite → Zeile scrollt bei Bedarf).
|
||
const feld = (lbl, val, on, ph) => h('div', { style: 'display:flex;flex-direction:column;gap:3px;flex:0 0 150px;' }, [
|
||
h('label', { style: 'font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);' }, lbl),
|
||
h('input', { value: val, placeholder: ph || '', style: WFELD + 'width:100%;padding:7px 9px;font-size:13px;', onInput: on }),
|
||
]);
|
||
|
||
function formular() {
|
||
// Empfängerzeile: erst individuelle Platzhalter, dann die Adresszeilen.
|
||
const zeile = (e, i) => [
|
||
...customPlaceholders().map((name) => feld(name, e.placeholders?.[name] || '', (ev) => setPlatzhalter(i, name, ev.target.value))),
|
||
...Array.from({ length: zeilenAnzahl() }, (_, z) => feld('Zeile ' + (z + 1), freitext(e, z), (ev) => setFreitext(i, z, ev.target.value))),
|
||
];
|
||
|
||
// Bereich 1 – Produkt
|
||
const b1 = h('div', { style: BEREICH }, [
|
||
bereichKopf('📄 Produkt'),
|
||
h('div', { style: 'display:grid;grid-template-columns:repeat(3,1fr);gap:12px;' }, [
|
||
h('div', {}, [label('Produkt'), sel(neu.product, produkte.value.map((p) => ({ v: p.key, t: p.name })), (e) => { neu.product = e.target.value; neu.format = neuFormate()[0]?.key || ''; neu.previewSession = null; })]),
|
||
h('div', {}, [label('Format'), sel(neu.format, neuFormate().map((f) => ({ v: f.key, t: f.name })), (e) => { neu.format = e.target.value; neu.previewSession = null; })]),
|
||
h('div', {}, [label('Schrift'), sel(neu.font, [['tilda', 'Tilda'], ['alva', 'Alva'], ['ellie', 'Ellie']].map(([v, t]) => ({ v, t })), (e) => { neu.font = e.target.value; neu.previewSession = null; })]),
|
||
]),
|
||
h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;margin-top:12px;cursor:pointer;' }, [
|
||
h('input', { type: 'checkbox', checked: neu.realistic, onChange: (e) => { neu.realistic = e.target.checked; neu.previewSession = null; }, style: 'width:16px;height:16px;' }),
|
||
'Natürliche Handschrift (leichte Variation)',
|
||
]),
|
||
]);
|
||
|
||
// Bereich 2 – Kuvert (Empfängertabelle)
|
||
const b2 = h('div', { style: BEREICH }, [
|
||
h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;' }, [
|
||
bereichKopf('✉ Kuvert'),
|
||
h('div', { style: 'width:300px;' }, [segToggle(neu.adressModus, [{ key: '5zeilen', label: '5 Zeilen' }, { key: 'individuell', label: 'Individueller Text' }], (k) => { neu.adressModus = k; neu.previewSession = null; })]),
|
||
]),
|
||
h('div', { style: 'display:flex;flex-direction:column;gap:8px;max-height:34vh;overflow:auto;' },
|
||
neu.rows.map((e, i) => h('div', { style: 'display:flex;gap:8px;align-items:center;' }, [
|
||
h('span', { style: `min-width:22px;font-weight:700;color:${ACCENT};` }, `#${i + 1}`),
|
||
h('div', { style: 'flex:1;min-width:0;display:flex;gap:6px;flex-wrap:nowrap;overflow-x:auto;padding-bottom:2px;' }, zeile(e, i)),
|
||
h('button', { type: 'button', onClick: () => delZeile(i), style: 'cursor:pointer;border:none;background:transparent;color:var(--danger,#c62828);font-size:18px;padding:2px 6px;' }, '×'),
|
||
]))),
|
||
h('div', { style: 'margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px;' }, [
|
||
btn('+ Empfänger', addZeile),
|
||
btn(neu.importOpen ? 'Import schließen' : '⇪ Aus Excel einfügen', () => { neu.importOpen = !neu.importOpen; }),
|
||
]),
|
||
h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-top:12px;' }, [
|
||
h('input', { type: 'checkbox', checked: neu.kuverts, onChange: (e) => { neu.kuverts = e.target.checked; neu.previewSession = null; }, style: 'width:16px;height:16px;' }),
|
||
'Kuverts generieren',
|
||
]),
|
||
neu.importOpen ? h('div', { style: 'margin-top:12px;' }, [
|
||
subdued('Aus Excel kopieren und einfügen – eine Zeile pro Empfänger, Spalten mit Tab/Semikolon/Komma. '
|
||
+ 'Reihenfolge: erst individuelle Platzhalter, dann ' + (neu.adressModus === 'individuell' ? 'die eine Adresszeile.' : 'Zeile 1–5.'), 'margin-bottom:6px;'),
|
||
h('textarea', { value: neu.importText, rows: 5, style: WFELD + 'width:100%;font-family:var(--family-monospace,monospace);', onInput: (e) => { neu.importText = e.target.value; } }),
|
||
h('div', { style: 'margin-top:6px;' }, [btn('Übernehmen', importUebernehmen, 'primary')]),
|
||
]) : null,
|
||
]);
|
||
|
||
// Bereich 3 – Text (Mustertext-Dropdown direkt darüber)
|
||
const b3 = h('div', { style: BEREICH }, [
|
||
h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;' }, [
|
||
bereichKopf('✍ Text'),
|
||
h('select', { style: WSELECT + 'font-size:13px;padding:7px 10px;width:auto;', onChange: (e) => { if (e.target.value) { musterEinfuegen(e.target.value); e.target.value = ''; } } }, [
|
||
h('option', { value: '' }, 'Mustertext einfügen …'),
|
||
h('option', { value: 'sie' }, 'Sie-Form'),
|
||
h('option', { value: 'du' }, 'Du-Form'),
|
||
]),
|
||
]),
|
||
label(`Brieftext · ${neu.text.length} Zeichen · [[Platzhalter]] erlaubt`),
|
||
h('textarea', { value: neu.text, rows: 8, style: WFELD + 'width:100%;font-family:var(--family-monospace,monospace);', onInput: (e) => { neu.text = saeubere(e.target.value); neu.previewSession = null; } }),
|
||
]);
|
||
|
||
// Bereich 4 – Kunde
|
||
const b4 = h('div', { style: BEREICH }, [
|
||
bereichKopf('👤 Kunde'),
|
||
h('div', { style: 'max-width:420px;' }, [
|
||
label('Kunde verknüpfen (E-Mail, optional)'),
|
||
h('input', { type: 'email', value: neu.customer_email, placeholder: 'kunde@example.de', style: WFELD + 'width:100%;', onInput: (e) => { neu.customer_email = e.target.value; } }),
|
||
]),
|
||
]);
|
||
|
||
const aktionen = h('div', { class: 'sk-btnrow', style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [
|
||
neu.hinweis ? h('div', { style: `color:var(--danger,#c62828);font-size:13px;width:100%;` }, neu.hinweis) : null,
|
||
neu.err ? h('div', { style: 'color:var(--danger,#c62828);font-size:13px;width:100%;' }, neu.err) : null,
|
||
neu.msg ? h('div', { style: 'width:100%;' }, subdued(neu.msg)) : null,
|
||
btn(neu.pLaedt ? 'Vorschau …' : '👁 Vorschau', preview),
|
||
btn('In Druck geben', absenden, 'primary'),
|
||
btn('Formular leeren', zuruecksetzen),
|
||
]);
|
||
|
||
const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, b3, b4, aktionen]);
|
||
|
||
// Vorschau rechts daneben (eigener hellblauer Bereich).
|
||
const rechts = h('div', { style: BEREICH + 'flex:0 0 340px;position:sticky;top:20px;align-self:flex-start;' }, [
|
||
h('div', { style: 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;' }, [
|
||
bereichKopf('👁 Vorschau'),
|
||
btn(neu.pLaedt ? '…' : 'Aktualisieren', preview),
|
||
]),
|
||
neu.pLaedt ? subdued('Wird erzeugt …')
|
||
: neu.docs.length ? h('div', { style: 'display:flex;flex-direction:column;gap:12px;' }, neu.docs.slice(0, 4).map(docBild))
|
||
: subdued('Noch keine Vorschau. „Aktualisieren" klicken (Klick aufs Bild = groß).'),
|
||
]);
|
||
|
||
return h('div', { style: 'display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;' }, [links, rechts]);
|
||
}
|
||
|
||
// Linke Navigationsspalte: aktuelle Werkzeuge dieses Moduls. Der
|
||
// Unterschriftenservice folgt als weiterer Eintrag.
|
||
const navItem = (label, aktiv, disabled) => h('div', {
|
||
style: 'display:flex;align-items:center;gap:8px;border-radius:6px;padding:10px 12px;margin-bottom:3px;font-size:14px;'
|
||
+ (aktiv ? `background:${ACCENT};color:#fff;font-weight:600;` : disabled ? 'color:var(--foreground-subdued);opacity:.7;' : 'color:var(--foreground-normal);'),
|
||
}, label);
|
||
|
||
return () => h(resolveComponent('private-view'), { title: 'Neuer Auftrag' }, {
|
||
navigation: () => h('div', { style: 'padding:12px 10px;' }, [
|
||
navItem('📝 Neuer Auftrag', true, false),
|
||
navItem('✍ Unterschriftenservice (folgt)', false, true),
|
||
]),
|
||
default: () => [
|
||
// Mobil: Button-Reihen bleiben in einer Zeile und füllen die Breite.
|
||
h('style', {}, '@media(max-width:640px){.sk-btnrow>button{flex:1 1 auto!important;}}'),
|
||
h('div', { style: 'padding:24px 28px;background:var(--background-subdued,#f1f4f9);min-height:100%;' },
|
||
!geladen.value ? subdued('Lädt …') : formular()),
|
||
neu.lb ? h(Teleport, { to: 'body' }, [
|
||
h('div', {
|
||
onClick: () => { neu.lb = ''; },
|
||
style: 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:1100;display:flex;align-items:center;justify-content:center;padding:4vh 4vw;cursor:zoom-out;',
|
||
}, [h('img', { src: neu.lb, style: 'max-width:100%;max-height:90vh;object-fit:contain;background:#fff;border-radius:8px;' })]),
|
||
]) : null,
|
||
],
|
||
});
|
||
},
|
||
});
|
||
|
||
export default {
|
||
id: 'skrift-neuauftrag',
|
||
name: 'Manueller Druck',
|
||
icon: 'print',
|
||
routes: [{ path: '', component: NeuAuftrag }],
|
||
};
|