Versand-Seite: eine Zeile je FTP-Sendung (Buendel) statt je Auftrag

Die oeffentliche Versand-Seite listet jetzt die FTP-Sendungen (gebuendelte
batch_submissions, gruppiert nach bundle_name = FTP-Dateiname
<Zugang>_<Datum>_<HHMM>), nicht mehr einzelne Auftraege. "Versendet"
markiert das ganze Buendel (alle Uebermittlungen darin) als versendet;
danach verschwindet es aus der Liste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-28 10:54:27 +02:00
parent 870295e012
commit d3ce505389

View File

@@ -368,13 +368,13 @@ const handler = (router, { services, getSchema, logger }) => {
// „versendet" markiert (Status-API liefert dann „shipped"). Schutz = geheimer
// Pfad-Token (kein Konto). Ohne gesetzten Token ist die Seite aus.
const escH = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const VS_LABEL = { eingegangen: 'Eingegangen', im_druck: 'Im Druck', kuvertieren: 'Kuvertierung', versandfertig: 'Versandfertig' };
const versandSeite = ({ orders = [], message = '', error = '' } = {}) => {
const rows = orders.length ? orders.map((o) => `
// Eine Zeile je FTP-Sendung (Bündel), benannt wie die FTP-Datei (<Zugang>_<Datum>_<HHMM>).
const versandSeite = ({ bundles = [], message = '', error = '' } = {}) => {
const rows = bundles.length ? bundles.map((b) => `
<div class="row">
<div class="info"><b>${escH(o.order_number)}</b><span>${escH(VS_LABEL[o.production_status] || o.production_status || 'offen')} · ${Number(o.entries_count) || 1} Empf. · ${escH(String(o.date_created || '').slice(0, 10))}</span></div>
<form method="post"><input type="hidden" name="order_number" value="${escH(o.order_number)}"><button type="submit">Versendet ✓</button></form>
</div>`).join('') : '<p class="muted">Keine offenen Aufträge – alles versendet.</p>';
<div class="info"><b>${escH(b.name)}</b><span>${Number(b.count) || 0} Empfänger · ${escH(String(b.date || '').slice(0, 10))}</span></div>
<form method="post"><input type="hidden" name="bundle" value="${escH(b.name)}"><button type="submit">Versendet ✓</button></form>
</div>`).join('') : '<p class="muted">Keine offenen Sendungen – alles versendet.</p>';
return `<!doctype html><html lang="de"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Versand bestätigen</title>
<style>
@@ -385,54 +385,57 @@ const handler = (router, { services, getSchema, logger }) => {
.ok{background:#e7f6ec;border:1px solid #34a853;color:#1e7e34;border-radius:10px;padding:12px 14px;margin:0 0 10px}
.err{background:#fdeaea;border:1px solid #c62828;color:#b3261e;border-radius:10px;padding:12px 14px;margin:0 0 10px}
.row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;margin-top:8px}
.info{display:flex;flex-direction:column;min-width:0} .info b{font-size:16px} .info span{font-size:12px;color:#8a97a8}
.info{display:flex;flex-direction:column;min-width:0} .info b{font-size:16px;word-break:break-all} .info span{font-size:12px;color:#8a97a8}
form{margin:0} button{font-size:14px;font-weight:700;padding:10px 16px;border:none;border-radius:9px;background:#1E50A5;color:#fff;cursor:pointer;white-space:nowrap}
</style></head><body><div class="card">
<h1>Versand bestätigen</h1>
<p>Alle offenen Aufträge. Bitte pro Auftrag „Versendet" klicken, sobald er verschickt ist.</p>
<p>Jede Zeile ist eine FTP-Sendung. Bitte „Versendet" klicken, sobald die Sendung verschickt ist.</p>
${error ? `<div class="err">${escH(error)}</div>` : ''}
${message ? `<div class="ok">${escH(message)}</div>` : ''}
${rows}
</div></body></html>`;
};
// Offene Aufträge (noch nicht versendet/storniert) – nur Nummer/Status/Menge/Datum.
async function ladeOffeneAuftraege(svc) {
return svc('orders').readByQuery({
filter: { production_status: { _nin: ['versendet', 'storniert'] }, status: { _neq: 'entwurf' } },
sort: ['-date_created'], limit: 500,
fields: ['id', 'order_number', 'production_status', 'entries_count', 'date_created'],
// Offene FTP-Sendungen: gebündelte Übermittlungen, noch nicht versendet, je bundle_name gruppiert.
async function ladeBuendel(svc) {
const rows = await svc('batch_submissions').readByQuery({
filter: { status: { _eq: 'bundled' }, versendet: { _neq: true }, bundle_name: { _nnull: true } },
limit: -1, sort: ['-bundle_name'], fields: ['id', 'bundle_name', 'date_created'],
});
const m = new Map();
for (const r of rows || []) {
if (!m.has(r.bundle_name)) m.set(r.bundle_name, { name: r.bundle_name, count: 0, date: r.date_created });
m.get(r.bundle_name).count += 1;
}
return [...m.values()];
}
router.get('/v1/versand/:token', async (req, res) => {
if (!VERSAND_TOKEN || req.params.token !== VERSAND_TOKEN) return res.status(404).type('html').send('Not found.');
try {
const svc = svcFactory(await getSchema());
return res.type('html').send(versandSeite({ orders: (await ladeOffeneAuftraege(svc)) || [] }));
return res.type('html').send(versandSeite({ bundles: await ladeBuendel(svc) }));
} catch (e) {
logger.error(`[skrift-api] versand list: ${e.message}`);
return res.type('html').send(versandSeite({ error: 'Aufträge konnten nicht geladen werden.' }));
return res.type('html').send(versandSeite({ error: 'Sendungen konnten nicht geladen werden.' }));
}
});
router.post('/v1/versand/:token', async (req, res) => {
if (!VERSAND_TOKEN || req.params.token !== VERSAND_TOKEN) return res.status(404).type('html').send('Not found.');
const nr = String((req.body && (req.body.order_number ?? req.body.nummer)) || req.query.order_number || '').trim();
const bundle = String((req.body && (req.body.bundle ?? req.body.name)) || req.query.bundle || '').trim();
try {
const svc = svcFactory(await getSchema());
let message = '', error = '';
if (!nr) {
error = 'Bitte einen Auftrag wählen.';
if (!bundle) {
error = 'Bitte eine Sendung wählen.';
} else {
const rows = await svc('orders').readByQuery({ filter: { order_number: { _eq: nr } }, limit: 1, fields: ['id', 'order_number', 'production_status'] });
const order = rows?.[0];
if (!order) error = `Auftrag ${nr} wurde nicht gefunden.`;
else if (order.production_status === 'versendet') message = `Auftrag ${nr} war bereits als versendet markiert.`;
else if (order.production_status === 'storniert') error = `Auftrag ${nr} ist storniert.`;
else { await svc('orders').updateOne(order.id, { production_status: 'versendet', versandt_am: new Date().toISOString() }); message = `Auftrag ${nr} als versendet markiert.`; }
const rows = await svc('batch_submissions').readByQuery({ filter: { bundle_name: { _eq: bundle }, status: { _eq: 'bundled' } }, limit: -1, fields: ['id'] });
const ids = (rows || []).map((r) => r.id);
if (!ids.length) error = `Sendung ${bundle} nicht gefunden oder bereits versendet.`;
else { await svc('batch_submissions').updateMany(ids, { versendet: true }); message = `Sendung ${bundle} als versendet markiert (${ids.length}).`; }
}
return res.type('html').send(versandSeite({ orders: (await ladeOffeneAuftraege(svc)) || [], message, error }));
return res.type('html').send(versandSeite({ bundles: await ladeBuendel(svc), message, error }));
} catch (e) {
logger.error(`[skrift-api] versand: ${e.stack || e.message}`);
return res.type('html').send(versandSeite({ error: 'Konnte nicht gespeichert werden. Bitte erneut versuchen.' }));