Batch-Reiter: FTP-Upload (primär) zurück, Tag+Kunde-Gruppen aufklappbar
- "Per FTP hochladen" als Primär-Aktion (wirkt auf Auswahl, ids an /skrift-orders/batch-build -> Backend), "In Produktion übernehmen" sekundär; dazu Versendet/Nicht versendet/Löschen. - Gruppierung Tag → Kunde, beide aufklappbar, Gruppen-Kopf mit Haken (wählt alle in der Gruppe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1377,7 +1377,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
const r = await fetch(`${BACKEND_URL}/api/batch/build`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
|
||||
body: JSON.stringify({ client_id: req.body?.client_id ?? null }),
|
||||
body: JSON.stringify({ client_id: req.body?.client_id ?? null, ids: Array.isArray(req.body?.ids) ? req.body.ids : null }),
|
||||
});
|
||||
const txt = await r.text();
|
||||
let data = null; try { data = txt ? JSON.parse(txt) : null; } catch { /* egal */ }
|
||||
|
||||
@@ -198,6 +198,7 @@ const Board = defineComponent({
|
||||
const batch = ref([]);
|
||||
const batchBusy = ref('');
|
||||
const batchAuswahl = ref([]);
|
||||
const batchOffen = ref({}); // Gruppen-Aufklappstatus (Default: aufgeklappt)
|
||||
|
||||
// ── 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 });
|
||||
@@ -985,6 +986,53 @@ const Board = defineComponent({
|
||||
fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Übernahme fehlgeschlagen.';
|
||||
} finally { batchBusy.value = ''; }
|
||||
}
|
||||
// Ausgewählte per FTP hochladen (xlsx + Sammel-Druck-PDF). Nur PDF-Übermittlungen.
|
||||
async function batchFtp() {
|
||||
const ids = [...batchAuswahl.value];
|
||||
if (!ids.length || batchBusy.value) return;
|
||||
batchBusy.value = 'ftp'; fehler.value = '';
|
||||
try {
|
||||
const res = await api.post('/skrift-orders/batch-build', { ids });
|
||||
const bundles = res.data?.bundles || [];
|
||||
const errs = bundles.filter((b) => b.error);
|
||||
const total = bundles.reduce((s, b) => s + (b.count || 0), 0);
|
||||
const files = bundles.filter((b) => b.file).map((b) => b.file).join(', ');
|
||||
if (errs.length) fehler.value = `FTP: ${errs.map((e) => e.error).join('; ')}`;
|
||||
else { flash(`✓ ${total} Übermittlung(en) per FTP hochgeladen${files ? ` → ${files}.{xlsx,pdf}` : ''}.`); batchAuswahlLeeren(); }
|
||||
await laden();
|
||||
} catch (e) {
|
||||
fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'FTP-Upload fehlgeschlagen.';
|
||||
} finally { batchBusy.value = ''; }
|
||||
}
|
||||
// Gruppen (Tag/Kunde): Aufklappen + Kopf-Auswahl.
|
||||
const istOffen = (k) => batchOffen.value[k] !== false;
|
||||
const toggleOffen = (k) => { batchOffen.value = { ...batchOffen.value, [k]: !istOffen(k) }; };
|
||||
const batchGruppeGewaehlt = (subs) => subs.length > 0 && subs.every((s) => batchGewaehlt(s.id));
|
||||
function batchGruppeToggle(subs) {
|
||||
const alle = batchGruppeGewaehlt(subs);
|
||||
const set = new Set(batchAuswahl.value);
|
||||
subs.forEach((s) => (alle ? set.delete(String(s.id)) : set.add(String(s.id))));
|
||||
batchAuswahl.value = [...set];
|
||||
}
|
||||
// Baum: Tag → Kunde → Übermittlungen (neueste Tage zuerst).
|
||||
const batchBaum = () => {
|
||||
const tage = new Map();
|
||||
for (const s of batch.value) {
|
||||
const tag = String(s.date_created || '').slice(0, 10) || '—';
|
||||
const cid = (s.api_client && s.api_client.id != null) ? s.api_client.id : (s.api_client ?? '—');
|
||||
const cname = (s.api_client && s.api_client.name) || `Zugang ${cid}`;
|
||||
if (!tage.has(tag)) tage.set(tag, new Map());
|
||||
const kunden = tage.get(tag);
|
||||
const ck = String(cid);
|
||||
if (!kunden.has(ck)) kunden.set(ck, { id: cid, name: cname, subs: [] });
|
||||
kunden.get(ck).subs.push(s);
|
||||
}
|
||||
return [...tage.entries()].sort((a, b) => String(b[0]).localeCompare(String(a[0])))
|
||||
.map(([tag, kunden]) => {
|
||||
const ks = [...kunden.values()];
|
||||
return { tag, subsAll: ks.flatMap((k) => k.subs), kunden: ks };
|
||||
});
|
||||
};
|
||||
// Ausgewählte als versendet / nicht versendet markieren.
|
||||
async function batchVersendet(val) {
|
||||
const ids = [...batchAuswahl.value];
|
||||
@@ -1469,46 +1517,70 @@ const Board = defineComponent({
|
||||
return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile)));
|
||||
}
|
||||
|
||||
// ── Reiter „Übermittlungen" (Batch, nach Tag gruppiert, mit Auswahl) ──────
|
||||
// ── Reiter „Übermittlungen" (Batch, Tag → Kunde, aufklappbar, Auswahl) ────
|
||||
function batchInhalt() {
|
||||
const gruppen = batchGruppenTag();
|
||||
const baum = batchBaum();
|
||||
const auswahlN = batchAuswahl.value.length;
|
||||
// Aktionsleiste (nur bei Auswahl) – Übernehmen / Versendet / Löschen.
|
||||
// Aktionsleiste (nur bei Auswahl): FTP primär, Übernehmen sekundär.
|
||||
const wbtn = (label, on, primary) => h('button', { type: 'button', onClick: on, disabled: !!batchBusy.value,
|
||||
style: 'cursor:pointer;border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;'
|
||||
+ (primary ? `border:none;background:#fff;color:${ACCENT};` : 'border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;') }, label);
|
||||
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;');
|
||||
wbtn(batchBusy.value === 'ftp' ? 'Lädt hoch …' : '📤 Per FTP hochladen', batchFtp, true),
|
||||
wbtn(batchBusy.value === 'uebernehmen' ? 'Übernimmt …' : '➡ In Produktion übernehmen', batchUebernehmen, false),
|
||||
wbtn('✓ Versendet', () => batchVersendet(true), false),
|
||||
wbtn('✗ Nicht versendet', () => batchVersendet(false), false),
|
||||
wbtn('🗑 Löschen', batchLoeschen, false),
|
||||
h('button', { type: 'button', onClick: batchAuswahlLeeren, style: 'cursor:pointer;border:none;background:transparent;color:#fff;font:inherit;font-size:13px;text-decoration:underline;' }, 'Auswahl aufheben'),
|
||||
]) : subdued('Übermittlungen auswählen (Strg/Shift für mehrere, oder Gruppen-Haken), dann per FTP hochladen / übernehmen / markieren / löschen.', 'margin-bottom:12px;');
|
||||
|
||||
if (!gruppen.length) return [leiste, subdued('Keine offenen Übermittlungen.')];
|
||||
if (!baum.length) return [leiste, subdued('Keine offenen Übermittlungen.')];
|
||||
|
||||
const pfeil = (offen) => h('span', { style: 'display:inline-block;width:14px;font-size:12px;color:var(--foreground-subdued);' }, offen ? '▾' : '▸');
|
||||
const gruppenHaken = (subs) => h('input', { type: 'checkbox', checked: batchGruppeGewaehlt(subs), onClick: (e) => { e.stopPropagation(); batchGruppeToggle(subs); }, style: 'width:16px;height:16px;flex-shrink:0;' });
|
||||
|
||||
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};` : '') }, [
|
||||
return h('div', { key: 'bs' + s.id, style: CARD + 'margin:6px 0 6px 26px;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,
|
||||
]),
|
||||
]);
|
||||
};
|
||||
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)),
|
||||
]));
|
||||
|
||||
const bloecke = baum.map((tg) => {
|
||||
const tagKey = 'd:' + tg.tag;
|
||||
const tagOffen = istOffen(tagKey);
|
||||
const tagKopf = h('div', { style: 'display:flex;align-items:center;gap:10px;margin-bottom:8px;' }, [
|
||||
gruppenHaken(tg.subsAll),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:700;font-size:15px;color:' + ACCENT + ';', onClick: () => toggleOffen(tagKey) }, [
|
||||
pfeil(tagOffen), h('span', {}, `📅 ${tg.tag}`), subdued(`· ${tg.subsAll.length} Übermittlung(en)`),
|
||||
]),
|
||||
]);
|
||||
const kundenBloecke = tagOffen ? tg.kunden.map((k) => {
|
||||
const kKey = `c:${tg.tag}:${k.id}`;
|
||||
const kOffen = istOffen(kKey);
|
||||
const kKopf = h('div', { style: 'display:flex;align-items:center;gap:10px;margin:10px 0 4px 20px;' }, [
|
||||
gruppenHaken(k.subs),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:600;', onClick: () => toggleOffen(kKey) }, [
|
||||
pfeil(kOffen), h('span', {}, `👤 ${k.name}`), subdued(`· ${k.subs.length}`),
|
||||
]),
|
||||
]);
|
||||
return h('div', {}, [kKopf, kOffen ? h('div', {}, k.subs.map(zeile)) : null]);
|
||||
}) : null;
|
||||
return h('div', { style: SECTION }, [tagKopf, kundenBloecke]);
|
||||
});
|
||||
return [leiste, ...bloecke];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user