Files
skrift-programme/Docker/directus/extensions/directus-extension-skrift-neuauftrag/dist/index.js
Lucas Orth 6232cecdb1 Produktion: Format-Warteschlangen, Sammeldruck, Webhook, Preis-a/e
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>
2026-07-26 11:51:24 +02:00

352 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 }],
};