Batch-Uebermittlung (Phase 3): Board-Reiter + Buendeln-Knopf + Schreibvorlage
Produktions-Board: neuer Reiter "Uebermittlungen" (getrennt vom Auftragsboard). Offene batch_submissions je Zugang gruppiert; Knopf "Jetzt buendeln & hochladen" pro Zugang loest die Backend-Buendelung (xlsx + Sammel-PDF + FTP) sofort aus. Text-Uebermittlungen bekommen einen "Schreibvorlage"-Knopf, der Text + 5-Zeilen-Adressblock (Firma / Vorname Nachname / Zeile1-3 / PLZ Stadt / Land, max 5) in den manuellen Druck uebernimmt. skrift-orders: Proxy-Route /batch-build -> Backend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1367,6 +1367,27 @@ const handler = (router, { services, getSchema, logger }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Batch-Bündelung sofort auslösen (Board-Knopf → Backend) ────────────────
|
||||||
|
// Proxyt an das Backend, das die offenen batch_submissions dieses Zugangs zu
|
||||||
|
// xlsx + Sammel-PDF bündelt und per FTP hochlädt. client_id = api_clients-ID.
|
||||||
|
router.post('/batch-build', async (req, res) => {
|
||||||
|
const acc = req.accountability;
|
||||||
|
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
|
||||||
|
try {
|
||||||
|
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 }),
|
||||||
|
});
|
||||||
|
const txt = await r.text();
|
||||||
|
let data = null; try { data = txt ? JSON.parse(txt) : null; } catch { /* egal */ }
|
||||||
|
if (!r.ok) return res.status(502).json({ error: 'Bündelung fehlgeschlagen.', detail: (data && data.message) || String(txt).slice(0, 300) });
|
||||||
|
return res.json(data || { ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ error: 'Backend nicht erreichbar.', detail: String(e.message || e).slice(0, 300) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Testdruck: beliebiges Template + Text generieren und einreihen ─────────
|
// ── Testdruck: beliebiges Template + Text generieren und einreihen ─────────
|
||||||
const TEST_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' };
|
const TEST_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' };
|
||||||
router.post('/test-print', async (req, res) => {
|
router.post('/test-print', async (req, res) => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const TABS = [
|
|||||||
{ key: 'kuvertierung', label: '✉ Kuvertierung' },
|
{ key: 'kuvertierung', label: '✉ Kuvertierung' },
|
||||||
{ key: 'versandfertig', label: '📦 Versandfertig' },
|
{ key: 'versandfertig', label: '📦 Versandfertig' },
|
||||||
{ key: 'versendet', label: '🚚 Versendet' },
|
{ key: 'versendet', label: '🚚 Versendet' },
|
||||||
|
{ key: 'batch', label: '📦 Übermittlungen' },
|
||||||
{ key: 'archiv', label: '🗄 Archiv' },
|
{ key: 'archiv', label: '🗄 Archiv' },
|
||||||
];
|
];
|
||||||
const STATUS_LABEL = {
|
const STATUS_LABEL = {
|
||||||
@@ -192,6 +193,10 @@ const Board = defineComponent({
|
|||||||
const muster = ref({ text_du: '', text_sie: '' });
|
const muster = ref({ text_du: '', text_sie: '' });
|
||||||
// Benannte Mustertexte (Collection muster_texts) – im Muster-Dialog wählbar.
|
// Benannte Mustertexte (Collection muster_texts) – im Muster-Dialog wählbar.
|
||||||
const musterListe = ref([]);
|
const musterListe = ref([]);
|
||||||
|
// Alternativer Übermittlungsweg (batch_submissions) – offene Datensätze,
|
||||||
|
// getrennt vom normalen Auftragsboard im Reiter „Übermittlungen".
|
||||||
|
const batch = ref([]);
|
||||||
|
const batchBusy = ref('');
|
||||||
|
|
||||||
// ── Modale ──
|
// ── 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 });
|
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 });
|
||||||
@@ -387,7 +392,7 @@ const Board = defineComponent({
|
|||||||
// leise = Hintergrund-Aktualisierung: kein Spinner, kein Fehlerbanner.
|
// leise = Hintergrund-Aktualisierung: kein Spinner, kein Fehlerbanner.
|
||||||
if (!leise) { loading.value = true; fehler.value = ''; }
|
if (!leise) { loading.value = true; fehler.value = ''; }
|
||||||
try {
|
try {
|
||||||
const [o, r, j, m, t, ms, mtx] = await Promise.all([
|
const [o, r, j, m, t, ms, mtx, bs] = await Promise.all([
|
||||||
ladeOrders(),
|
ladeOrders(),
|
||||||
api.get('/items/offer_requests', { params: {
|
api.get('/items/offer_requests', { params: {
|
||||||
limit: -1, sort: '-date_created',
|
limit: -1, sort: '-date_created',
|
||||||
@@ -399,6 +404,11 @@ const Board = defineComponent({
|
|||||||
api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format,search_default' } }).catch(() => api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format' } })),
|
api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format,search_default' } }).catch(() => 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_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: [] } })),
|
api.get('/items/muster_texts', { params: { limit: -1, filter: { active: { _eq: true } }, sort: 'sort', fields: 'id,label,text' } }).catch(() => ({ data: { data: [] } })),
|
||||||
|
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',
|
||||||
|
} }).catch(() => ({ data: { data: [] } })),
|
||||||
]);
|
]);
|
||||||
orders.value = o.data?.data || [];
|
orders.value = o.data?.data || [];
|
||||||
requests.value = r.data?.data || [];
|
requests.value = r.data?.data || [];
|
||||||
@@ -407,6 +417,7 @@ const Board = defineComponent({
|
|||||||
templates.value = t.data?.data || [];
|
templates.value = t.data?.data || [];
|
||||||
muster.value = ms.data?.data || { text_du: '', text_sie: '' };
|
muster.value = ms.data?.data || { text_du: '', text_sie: '' };
|
||||||
musterListe.value = mtx.data?.data || [];
|
musterListe.value = mtx.data?.data || [];
|
||||||
|
batch.value = bs.data?.data || [];
|
||||||
// Empfänger-Index nur beim vollen Laden neu bauen (nicht bei der 15-s-
|
// Empfänger-Index nur beim vollen Laden neu bauen (nicht bei der 15-s-
|
||||||
// Hintergrund-Aktualisierung) – im Hintergrund, blockiert das Board nicht.
|
// Hintergrund-Aktualisierung) – im Hintergrund, blockiert das Board nicht.
|
||||||
if (!leise) { ladeEmpfIndex(); ladeSendIndex(); ladeChargeNotizen(); }
|
if (!leise) { ladeEmpfIndex(); ladeSendIndex(); ladeChargeNotizen(); }
|
||||||
@@ -531,6 +542,7 @@ const Board = defineComponent({
|
|||||||
if (key === 'kuvertierung') return ordersByStatus('kuvertieren').length;
|
if (key === 'kuvertierung') return ordersByStatus('kuvertieren').length;
|
||||||
if (key === 'versandfertig') return ordersByStatus('versandfertig').length;
|
if (key === 'versandfertig') return ordersByStatus('versandfertig').length;
|
||||||
if (key === 'versendet') return versendetAktiv().length;
|
if (key === 'versendet') return versendetAktiv().length;
|
||||||
|
if (key === 'batch') return batch.value.length;
|
||||||
if (key === 'archiv') return archivListe().length;
|
if (key === 'archiv') return archivListe().length;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@@ -910,6 +922,54 @@ const Board = defineComponent({
|
|||||||
} catch (e) { flash('Übernahme fehlgeschlagen.'); }
|
} catch (e) { flash('Übernahme fehlgeschlagen.'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Batch-Übermittlungen (alternativer Weg) ──────────────────────────────
|
||||||
|
// Offene Datensätze je Zugang gruppieren.
|
||||||
|
const batchGruppen = () => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return [...m.values()];
|
||||||
|
};
|
||||||
|
// 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 = '';
|
||||||
|
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}` : ''}.`);
|
||||||
|
await laden();
|
||||||
|
} catch (e) {
|
||||||
|
fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Bündelung 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) {
|
||||||
|
const zeilen = [
|
||||||
|
sub.firma,
|
||||||
|
[sub.vorname, sub.nachname].filter(Boolean).join(' '),
|
||||||
|
sub.zeile1, sub.zeile2, sub.zeile3,
|
||||||
|
[sub.plz, sub.stadt].filter(Boolean).join(' '),
|
||||||
|
sub.land,
|
||||||
|
].map((z) => String(z || '').trim()).filter(Boolean).slice(0, 5);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('skrift_neuauftrag_uebernahme', JSON.stringify({
|
||||||
|
text: sub.text || '', adressModus: '5zeilen',
|
||||||
|
rows: [{ free_text: zeilen.join('\n'), placeholders: {} }],
|
||||||
|
}));
|
||||||
|
const base = window.location.pathname.split('/admin/')[0] + '/admin/';
|
||||||
|
window.location.href = base + 'skrift-neuauftrag';
|
||||||
|
} catch (e) { flash('Übernahme fehlgeschlagen.'); }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Empfänger-Drawer (mit Dokument-Vorschau je Zeile) ──
|
// ── Empfänger-Drawer (mit Dokument-Vorschau je Zeile) ──
|
||||||
// Zerlegt einen Dateinamen in Brief-Nummer und Seite:
|
// Zerlegt einen Dateinamen in Brief-Nummer und Seite:
|
||||||
// letter_001_p2.svg → { nummer:1, seite:2 }, letter_003.svg → { nummer:3, seite:1 }.
|
// letter_001_p2.svg → { nummer:1, seite:2 }, letter_003.svg → { nummer:3, seite:1 }.
|
||||||
@@ -1348,7 +1408,39 @@ const Board = defineComponent({
|
|||||||
return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile)));
|
return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Reiter „Übermittlungen" (Batch, getrennt vom Auftragsboard) ──────────
|
||||||
|
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)`),
|
||||||
|
]),
|
||||||
|
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)]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function tabInhalt() {
|
function tabInhalt() {
|
||||||
|
if (tab.value === 'batch') return batchInhalt();
|
||||||
if (tab.value === 'suche') return sucheInhalt();
|
if (tab.value === 'suche') return sucheInhalt();
|
||||||
if (tab.value === 'neu') {
|
if (tab.value === 'neu') {
|
||||||
const items = [
|
const items = [
|
||||||
|
|||||||
Reference in New Issue
Block a user