Batch-Reiter Umbau: Uebernahme in Produktion, Tag-Gruppen, Auswahl, Versendet, Loeschen
Uebermittlungen-Reiter neu: nach Tag gruppiert, Mehrfachauswahl (Strg/ Shift, board-weit auch fuer Auftragskarten). Aktionen auf Auswahl: - "In Produktion uebernehmen" -> neuer /skrift-orders/batch-uebernehmen: PDF -> Datei-Auftrag (PDF = Druckdatei), Text -> Handschrift-Brief (Text + 5-Zeilen-Adresse), Kuvert mit Empfaenger; Status uebernommen; Generierung angestossen. Danach in "Neu" (dort wie bisher buendeln). - "Versendet" / "Nicht versendet" (neues Feld batch_submissions.versendet). - "Loeschen" (mit Rueckfrage). FTP-"Jetzt buendeln"-Knopf entfernt (FTP-Weg entfaellt). Bootstrap: Status 'uebernommen' + Feld versendet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -710,8 +710,9 @@ const collections = [
|
||||
f('land', 'string', { interface: 'input' }),
|
||||
f('text', 'text', { interface: 'input-multiline', note: 'Schriftstücktext (Handschrift), falls keine Druck-PDF.' }),
|
||||
m2o('pdf_file', 'directus_files', { note: 'Fertige Druck-PDF (wird 1:1 übernommen), falls kein Text.' }),
|
||||
dropdown('status', ['pending', 'bundled'], { default: 'pending', note: 'pending = noch nicht gebündelt.' }),
|
||||
f('bundle_name', 'string', { interface: 'input', readonly: true, note: 'Name der Bündel-Datei, in die dieser Datensatz ging.' }),
|
||||
dropdown('status', ['pending', 'uebernommen', 'bundled'], { default: 'pending', note: 'pending = offen; uebernommen = als Auftrag in die Produktion übernommen.' }),
|
||||
f('versendet', 'boolean', { interface: 'boolean', default: false, note: 'Im Board manuell als versendet markiert.' }),
|
||||
f('bundle_name', 'string', { interface: 'input', readonly: true, note: 'Name der Bündel-Datei / Referenz.' }),
|
||||
f('bundled_at', 'timestamp', { interface: 'datetime', readonly: true }),
|
||||
{ field: 'date_created', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-created'] }, schema: {} },
|
||||
] },
|
||||
|
||||
@@ -1388,6 +1388,80 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Batch-Übermittlungen in die Produktion übernehmen (Reiter „Neu") ───────
|
||||
// Ausgewählte batch_submissions werden Aufträge: PDF → Datei-Auftrag (die PDF
|
||||
// ist die Druckdatei, source_file), Text → Handschrift-Brief (Text + 5-Zeilen-
|
||||
// Adresse). Kuvert mit Empfängeradresse. Danach Generierung + Status uebernommen.
|
||||
router.post('/batch-uebernehmen', async (req, res) => {
|
||||
const acc = req.accountability;
|
||||
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
|
||||
const ids = Array.isArray(req.body?.ids) ? req.body.ids : [];
|
||||
if (!ids.length) return res.status(400).json({ error: 'Keine Übermittlungen gewählt.' });
|
||||
try {
|
||||
const schema = await getSchema();
|
||||
const svc = (c) => new ItemsService(c, { schema, accountability: null });
|
||||
const { settings } = await loadPricingContext(schema);
|
||||
const clean = makeSanitizer(settings.char_whitelist || '\\x20-\\x7E');
|
||||
|
||||
const dateiProd = (await svc('products').readByQuery({ filter: { input_mode: { _eq: 'datei' }, active: { _eq: true } }, limit: 1, fields: ['id'] }))?.[0];
|
||||
let briefProd = (await svc('products').readByQuery({ filter: { type: { _eq: 'letter' }, active: { _eq: true } }, limit: 1, sort: ['sort'], fields: ['id'] }))?.[0];
|
||||
if (!briefProd) briefProd = (await svc('products').readByQuery({ filter: { input_mode: { _neq: 'datei' }, active: { _eq: true } }, limit: 1, sort: ['sort'], fields: ['id'] }))?.[0];
|
||||
const fmtA4 = (await svc('formats').readByQuery({ filter: { key: { _eq: 'a4' } }, limit: 1, fields: ['id'] }))?.[0]?.id ?? null;
|
||||
|
||||
const subs = await svc('batch_submissions').readMany(ids, {
|
||||
fields: ['id', 'firma', 'vorname', 'nachname', 'zeile1', 'zeile2', 'zeile3', 'plz', 'stadt', 'land', 'text', 'pdf_file', 'status', 'customer'],
|
||||
});
|
||||
|
||||
const naechsteNummer = async () => {
|
||||
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 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 angelegt = [];
|
||||
const uebersprungen = [];
|
||||
for (const s of subs || []) {
|
||||
if (!s || s.status !== 'pending') { uebersprungen.push({ id: s?.id, grund: 'nicht offen' }); continue; }
|
||||
const istPdf = !!s.pdf_file;
|
||||
const product = istPdf ? dateiProd : briefProd;
|
||||
if (!product) { uebersprungen.push({ id: s.id, grund: istPdf ? 'kein Datei-Produkt' : 'kein Brief-Produkt' }); continue; }
|
||||
const adr = [s.firma, [s.vorname, s.nachname].filter(Boolean).join(' '), s.zeile1, s.zeile2, s.zeile3, [s.plz, s.stadt].filter(Boolean).join(' '), s.land]
|
||||
.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 5);
|
||||
const orderData = {
|
||||
customer: s.customer || null,
|
||||
product: product.id, format: fmtA4,
|
||||
person_type: 'unternehmen', font: 'tilda',
|
||||
source: 'operator', status: 'in_queue', production_status: 'eingegangen',
|
||||
payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel',
|
||||
needs_envelope: true, envelope_labeling: 'empfaenger', envelope_format: 'dinlang',
|
||||
text_template: istPdf ? null : (clean(s.text) || null),
|
||||
source_file: istPdf ? s.pdf_file : null,
|
||||
source_files: istPdf ? [s.pdf_file] : null,
|
||||
entries_count: 1, count_inland: 1, count_ausland: 0,
|
||||
multi_page: !istPdf,
|
||||
};
|
||||
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; }
|
||||
}
|
||||
await svc('order_entries').createOne({ order: orderId, letter_number: 1, free_text: adr.join('\n') || null, country: String(s.land || '').trim() || null });
|
||||
await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Aus Batch-Übermittlung übernommen.' }).catch(() => {});
|
||||
await svc('batch_submissions').updateOne(s.id, { status: 'uebernommen', bundled_at: new Date().toISOString(), bundle_name: orderNumber });
|
||||
fetch(`${BACKEND_URL}/api/order/from-directus`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, body: JSON.stringify({ orderId }),
|
||||
}).catch((e) => logger.warn(`[skrift-orders] batch-uebernehmen Generierung: ${e.message}`));
|
||||
angelegt.push({ id: s.id, order_number: orderNumber });
|
||||
}
|
||||
return res.json({ ok: true, created: angelegt.length, orders: angelegt, skipped: uebersprungen });
|
||||
} catch (err) {
|
||||
logger.error(`[skrift-orders] batch-uebernehmen: ${err.stack || err.message}`);
|
||||
return res.status(500).json({ error: 'Übernahme fehlgeschlagen.', detail: String(err.message || err).slice(0, 300) });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Testdruck: beliebiges Template + Text generieren und einreihen ─────────
|
||||
const TEST_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' };
|
||||
router.post('/test-print', async (req, res) => {
|
||||
|
||||
@@ -197,6 +197,7 @@ const Board = defineComponent({
|
||||
// getrennt vom normalen Auftragsboard im Reiter „Übermittlungen".
|
||||
const batch = ref([]);
|
||||
const batchBusy = ref('');
|
||||
const batchAuswahl = ref([]);
|
||||
|
||||
// ── Modale ──
|
||||
const druck = reactive({ open: false, order: null, machine: null, mode: 'queue', busy: false, brief: null, umschlag: null, spaltenweise: false, msg: '', err: '', deNums: '', intNums: '', deN: 0, intN: 0 });
|
||||
@@ -407,7 +408,7 @@ const Board = defineComponent({
|
||||
api.get('/items/batch_submissions', { params: {
|
||||
limit: -1, sort: 'date_created',
|
||||
filter: { status: { _eq: 'pending' } },
|
||||
fields: 'id,api_client.id,api_client.name,firma,vorname,nachname,zeile1,zeile2,zeile3,plz,stadt,land,text,pdf_file,status,date_created',
|
||||
fields: 'id,api_client.id,api_client.name,firma,vorname,nachname,zeile1,zeile2,zeile3,plz,stadt,land,text,pdf_file,status,versendet,date_created',
|
||||
} }).catch(() => ({ data: { data: [] } })),
|
||||
]);
|
||||
orders.value = o.data?.data || [];
|
||||
@@ -639,9 +640,24 @@ const Board = defineComponent({
|
||||
|
||||
// ── Massenauswahl + Sammel-Druck ─────────────────────────────────────────
|
||||
const istGewaehlt = (id) => auswahl.value.includes(String(id));
|
||||
function toggleAuswahl(id) {
|
||||
// Zuletzt angeklickte Karte – für Shift-Bereichsauswahl im ganzen Board.
|
||||
let letzteAuswahlId = null;
|
||||
function toggleAuswahl(id, ev) {
|
||||
id = String(id);
|
||||
if (ev && (ev.shiftKey || ev.ctrlKey || ev.metaKey) && letzteAuswahlId && letzteAuswahlId !== id) {
|
||||
const ids = sichtbareOrders().map((o) => String(o.id));
|
||||
const a = ids.indexOf(letzteAuswahlId), b = ids.indexOf(id);
|
||||
if (a !== -1 && b !== -1) {
|
||||
const [lo, hi] = a < b ? [a, b] : [b, a];
|
||||
const set = new Set(auswahl.value);
|
||||
ids.slice(lo, hi + 1).forEach((x) => set.add(x));
|
||||
auswahl.value = [...set];
|
||||
letzteAuswahlId = id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
auswahl.value = istGewaehlt(id) ? auswahl.value.filter((x) => x !== id) : [...auswahl.value, id];
|
||||
letzteAuswahlId = id;
|
||||
}
|
||||
const auswahlLeeren = () => { auswahl.value = []; };
|
||||
// Aufträge, die im aktuellen Tab als Karten sichtbar sind (für „Alle auswählen").
|
||||
@@ -922,34 +938,79 @@ const Board = defineComponent({
|
||||
} catch (e) { flash('Übernahme fehlgeschlagen.'); }
|
||||
}
|
||||
|
||||
// ── Batch-Übermittlungen (alternativer Weg) ──────────────────────────────
|
||||
// Offene Datensätze je Zugang gruppieren.
|
||||
const batchGruppen = () => {
|
||||
// ── Batch-Übermittlungen ─────────────────────────────────────────────────
|
||||
// Nach Tag gruppieren (neueste zuerst); innerhalb je Datensatz eine Zeile.
|
||||
const batchGruppenTag = () => {
|
||||
const m = new Map();
|
||||
for (const s of batch.value) {
|
||||
const cid = (s.api_client && s.api_client.id != null) ? s.api_client.id : (s.api_client ?? '—');
|
||||
const key = String(cid);
|
||||
if (!m.has(key)) m.set(key, { id: cid, name: (s.api_client && s.api_client.name) || `Zugang ${cid}`, subs: [] });
|
||||
m.get(key).subs.push(s);
|
||||
const tag = String(s.date_created || '').slice(0, 10) || '—';
|
||||
if (!m.has(tag)) m.set(tag, []);
|
||||
m.get(tag).push(s);
|
||||
}
|
||||
return [...m.values()];
|
||||
return [...m.entries()].sort((a, b) => String(b[0]).localeCompare(String(a[0])));
|
||||
};
|
||||
// Sofort bündeln (xlsx + Sammel-PDF + FTP) für EINEN Zugang.
|
||||
async function batchBuild(clientId, clientName) {
|
||||
if (batchBusy.value) return;
|
||||
batchBusy.value = String(clientId);
|
||||
fehler.value = '';
|
||||
// Mehrfachauswahl der Übermittlungen (mit Shift-Bereich).
|
||||
let letzteBatchId = null;
|
||||
const batchGewaehlt = (id) => batchAuswahl.value.includes(String(id));
|
||||
function batchToggle(id, ev) {
|
||||
id = String(id);
|
||||
const alleIds = batch.value.map((s) => String(s.id));
|
||||
if (ev && (ev.shiftKey || ev.ctrlKey || ev.metaKey) && letzteBatchId && letzteBatchId !== id) {
|
||||
const a = alleIds.indexOf(letzteBatchId), b = alleIds.indexOf(id);
|
||||
if (a !== -1 && b !== -1) {
|
||||
const [lo, hi] = a < b ? [a, b] : [b, a];
|
||||
const set = new Set(batchAuswahl.value);
|
||||
alleIds.slice(lo, hi + 1).forEach((x) => set.add(x));
|
||||
batchAuswahl.value = [...set]; letzteBatchId = id; return;
|
||||
}
|
||||
}
|
||||
batchAuswahl.value = batchGewaehlt(id) ? batchAuswahl.value.filter((x) => x !== id) : [...batchAuswahl.value, id];
|
||||
letzteBatchId = id;
|
||||
}
|
||||
const batchAuswahlLeeren = () => { batchAuswahl.value = []; };
|
||||
|
||||
// Ausgewählte in die Produktion übernehmen (PDF→Datei-Auftrag, Text→Brief).
|
||||
async function batchUebernehmen() {
|
||||
const ids = [...batchAuswahl.value];
|
||||
if (!ids.length || batchBusy.value) return;
|
||||
batchBusy.value = 'uebernehmen'; fehler.value = '';
|
||||
try {
|
||||
const res = await api.post('/skrift-orders/batch-build', { client_id: clientId });
|
||||
const bundles = res.data?.bundles || [];
|
||||
const meins = bundles.find((x) => String(x.client_id) === String(clientId)) || bundles[0] || {};
|
||||
if (meins.error) fehler.value = `Bündelung: ${meins.error}`;
|
||||
else flash(`✓ ${clientName || clientId}: ${meins.count || 0} Übermittlung(en) gebündelt${meins.file ? ` → ${meins.file}.{xlsx,pdf}` : ''}.`);
|
||||
const res = await api.post('/skrift-orders/batch-uebernehmen', { ids });
|
||||
const n = res.data?.created || 0;
|
||||
const skip = (res.data?.skipped || []).length;
|
||||
flash(`✓ ${n} Auftrag/Aufträge unter „Neu" angelegt${skip ? ` · ${skip} übersprungen` : ''}.`);
|
||||
batchAuswahlLeeren();
|
||||
await laden();
|
||||
} catch (e) {
|
||||
fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Bündelung fehlgeschlagen.';
|
||||
fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Übernahme fehlgeschlagen.';
|
||||
} finally { batchBusy.value = ''; }
|
||||
}
|
||||
// Ausgewählte als versendet / nicht versendet markieren.
|
||||
async function batchVersendet(val) {
|
||||
const ids = [...batchAuswahl.value];
|
||||
if (!ids.length || batchBusy.value) return;
|
||||
batchBusy.value = 'versendet'; fehler.value = '';
|
||||
try {
|
||||
await api.patch('/items/batch_submissions', { keys: ids, data: { versendet: !!val } });
|
||||
flash(`${ids.length} als ${val ? 'versendet' : 'nicht versendet'} markiert.`);
|
||||
await laden();
|
||||
} catch (e) { fehler.value = e?.message || 'Konnte nicht gespeichert werden.'; }
|
||||
finally { batchBusy.value = ''; }
|
||||
}
|
||||
// Ausgewählte löschen (mit Rückfrage).
|
||||
async function batchLoeschen() {
|
||||
const ids = [...batchAuswahl.value];
|
||||
if (!ids.length || batchBusy.value) return;
|
||||
if (!window.confirm(`${ids.length} Übermittlung(en) endgültig löschen?`)) return;
|
||||
batchBusy.value = 'loeschen'; fehler.value = '';
|
||||
try {
|
||||
await api.delete('/items/batch_submissions', { data: ids });
|
||||
flash(`${ids.length} Übermittlung(en) gelöscht.`);
|
||||
batchAuswahlLeeren();
|
||||
await laden();
|
||||
} catch (e) { fehler.value = e?.message || 'Löschen fehlgeschlagen.'; }
|
||||
finally { batchBusy.value = ''; }
|
||||
}
|
||||
// Text-Übermittlung als Schreibvorlage in den manuellen Druck übernehmen.
|
||||
// 5-Zeilen-Adressblock: Firma / Vorname Nachname / Zeile1-3 / PLZ Stadt / Land (leere weg, max 5).
|
||||
function batchSchreibvorlage(sub) {
|
||||
@@ -1243,7 +1304,7 @@ const Board = defineComponent({
|
||||
const o = order;
|
||||
const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;' }, [
|
||||
h('div', { style: 'display:flex;gap:10px;align-items:flex-start;' }, [
|
||||
h('input', { type: 'checkbox', checked: istGewaehlt(o.id), onChange: () => toggleAuswahl(o.id),
|
||||
h('input', { type: 'checkbox', checked: istGewaehlt(o.id), onClick: (e) => toggleAuswahl(o.id, e),
|
||||
title: 'Für Sammel-Druck auswählen', style: 'width:16px;height:16px;margin-top:3px;cursor:pointer;' }),
|
||||
h('div', {}, [
|
||||
h('div', { style: 'font-weight:700;font-size:15px;' }, `#${o.order_number}`),
|
||||
@@ -1408,35 +1469,47 @@ const Board = defineComponent({
|
||||
return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile)));
|
||||
}
|
||||
|
||||
// ── Reiter „Übermittlungen" (Batch, getrennt vom Auftragsboard) ──────────
|
||||
// ── Reiter „Übermittlungen" (Batch, nach Tag gruppiert, mit Auswahl) ──────
|
||||
function batchInhalt() {
|
||||
const gruppen = batchGruppen();
|
||||
if (!gruppen.length) return subdued('Keine offenen Übermittlungen.');
|
||||
return gruppen.map((g) => {
|
||||
const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;' }, [
|
||||
h('div', {}, [
|
||||
h('div', { style: 'font-weight:700;font-size:15px;' }, g.name),
|
||||
subdued(`Zugang #${g.id} · ${g.subs.length} offene Übermittlung(en)`),
|
||||
const gruppen = batchGruppenTag();
|
||||
const auswahlN = batchAuswahl.value.length;
|
||||
// Aktionsleiste (nur bei Auswahl) – Übernehmen / Versendet / Löschen.
|
||||
const leiste = auswahlN ? h('div', { style: `display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:${ACCENT};color:#fff;padding:10px 16px;border-radius:8px;margin-bottom:14px;` }, [
|
||||
h('span', { style: 'font-weight:600;' }, `${auswahlN} ausgewählt`),
|
||||
h('button', { type: 'button', onClick: batchUebernehmen, disabled: !!batchBusy.value, style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;` }, batchBusy.value === 'uebernehmen' ? 'Übernimmt …' : '➡ In Produktion übernehmen'),
|
||||
h('button', { type: 'button', onClick: () => batchVersendet(true), disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '✓ Versendet'),
|
||||
h('button', { type: 'button', onClick: () => batchVersendet(false), disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '✗ Nicht versendet'),
|
||||
h('button', { type: 'button', onClick: batchLoeschen, disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '🗑 Löschen'),
|
||||
h('button', { type: 'button', onClick: batchAuswahlLeeren, style: 'cursor:pointer;border:none;background:transparent;color:#fff;border-radius:7px;padding:6px 8px;font:inherit;font-size:13px;text-decoration:underline;' }, 'Auswahl aufheben'),
|
||||
]) : subdued('Übermittlungen auswählen (Strg/Shift für mehrere), dann übernehmen/markieren/löschen.', 'margin-bottom:12px;');
|
||||
|
||||
if (!gruppen.length) return [leiste, subdued('Keine offenen Übermittlungen.')];
|
||||
|
||||
const zeile = (s) => {
|
||||
const person = [s.firma, [s.vorname, s.nachname].filter(Boolean).join(' ')].filter(Boolean).join(' · ') || '—';
|
||||
const ort = [s.plz, s.stadt].filter(Boolean).join(' ');
|
||||
const istText = !s.pdf_file;
|
||||
const gew = batchGewaehlt(s.id);
|
||||
const zugang = (s.api_client && s.api_client.name) || (s.api_client ? `Zugang ${s.api_client.id ?? s.api_client}` : '');
|
||||
return h('div', { key: 'bs' + s.id, style: CARD + 'margin-bottom:8px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (gew ? `border-color:${ACCENT};` : '') }, [
|
||||
h('input', { type: 'checkbox', checked: gew, onClick: (e) => batchToggle(s.id, e), style: 'width:16px;height:16px;flex-shrink:0;' }),
|
||||
h('div', { style: 'min-width:0;flex:1;' }, [
|
||||
h('div', { style: 'font-weight:600;' }, person),
|
||||
subdued([ort, s.land].filter(Boolean).join(' · ') || '—'),
|
||||
]),
|
||||
h('div', { style: 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;' }, [
|
||||
zugang ? subdued(zugang) : null,
|
||||
chip(istText ? '✍ Text' : '📄 Druck-PDF'),
|
||||
s.versendet ? chip('✓ versendet', 'var(--success-25,rgba(46,125,50,.15))') : null,
|
||||
istText ? btn('✍ Schreibvorlage', () => batchSchreibvorlage(s)) : null,
|
||||
]),
|
||||
btn(batchBusy.value === String(g.id) ? 'Bündelt …' : '📤 Jetzt bündeln & hochladen', () => batchBuild(g.id, g.name), 'primary'),
|
||||
]);
|
||||
const zeilen = g.subs.map((s) => {
|
||||
const person = [s.firma, [s.vorname, s.nachname].filter(Boolean).join(' ')].filter(Boolean).join(' · ') || '—';
|
||||
const ort = [s.plz, s.stadt].filter(Boolean).join(' ');
|
||||
const istText = !s.pdf_file;
|
||||
return h('div', { key: 'bs' + s.id, style: CARD + 'margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;' }, [
|
||||
h('div', { style: 'min-width:0;' }, [
|
||||
h('div', { style: 'font-weight:600;' }, person),
|
||||
subdued([ort, s.land].filter(Boolean).join(' · ') || '—'),
|
||||
]),
|
||||
h('div', { style: 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;' }, [
|
||||
chip(istText ? '✍ Text' : '📄 Druck-PDF'),
|
||||
istText ? btn('✍ Schreibvorlage', () => batchSchreibvorlage(s)) : null,
|
||||
]),
|
||||
]);
|
||||
});
|
||||
return h('div', { style: SECTION }, [kopf, h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, zeilen)]);
|
||||
});
|
||||
};
|
||||
const bloecke = gruppen.map(([tag, subs]) => h('div', { style: SECTION }, [
|
||||
h('div', { style: 'font-weight:700;font-size:14px;color:' + ACCENT + ';margin-bottom:10px;' }, `📅 ${tag} · ${subs.length} Übermittlung(en)`),
|
||||
h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, subs.map(zeile)),
|
||||
]));
|
||||
return [leiste, ...bloecke];
|
||||
}
|
||||
|
||||
function tabInhalt() {
|
||||
|
||||
Reference in New Issue
Block a user