B: Explizite lineare Druck-Warteschlange + Formatwechsel-Bestaetigung
Schema (bootstrap): - jobs.position (float) = explizite lineare Reihenfolge; machines.pending_format = vom Agent angefordertes naechstes Format - Service-Rechte: jobs.update +position, machines.update +pending_format Einreihen (skrift-orders): enqueue/enqueue-bulk vergeben position (ans Ende je Maschine) Board (produktion) Warteschlange: EINE lineare Liste je Maschine (nicht mehr nach Format gebuendelt), Hoch/Runter/Anfang/Ende-Umsortieren, Formatwechsel-Marker, laufende Jobs, Banner "Format einlegen -> bestaetigen" (setzt current_format, leert pending) Uebersicht (Mitarbeiter): Maschinen-Panel mit Bestaetigen/Starten + Vorschau der kommenden Format-Segmente; jobs nach position sortiert ACHTUNG Deploy: Bootstrap VOR Directus-Neustart UND vor Agent-Update (neue Felder/Rechte). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -404,6 +404,10 @@ const collections = [
|
||||
// format-Schlüssel und springt nie selbstständig auf ein anderes Format.
|
||||
// Leer = Maschine wartet, bis im Board ein Format als eingelegt gewählt wird.
|
||||
f('current_format', 'string', { interface: 'input', note: 'Aktuell eingelegtes Format (Agent druckt nur passende Jobs). Leer = wartet auf Formatwahl.' }),
|
||||
// Vom Agent angefordertes NÄCHSTES Format (lineare Warteschlange): der nächste
|
||||
// geplante Job braucht ein anderes Format → Agent pausiert und setzt dies, das
|
||||
// Board zeigt „Format einlegen → bestätigen" (setzt current_format, leert dies).
|
||||
f('pending_format', 'string', { interface: 'input', note: 'Angefordertes nächstes Format (wartet auf Bestätigung im Board).' }),
|
||||
f('notes', 'text', { interface: 'input-multiline' }),
|
||||
] },
|
||||
|
||||
@@ -471,6 +475,9 @@ const collections = [
|
||||
f('format_label', 'string', { interface: 'input', note: 'Lesbare Formatbezeichnung für die Warteschlangen-Ansicht.' }),
|
||||
dropdown('status', ['queued', 'printing', 'printed', 'failed', 'shipped'], { default: 'queued' }),
|
||||
f('priority', 'integer', { interface: 'input', default: 0, note: 'Höher = zuerst. Direktdruck bekommt hohe Priorität.' }),
|
||||
// Explizite lineare Reihenfolge der Plan-Warteschlange (klein = zuerst). Ein
|
||||
// Format darf mehrfach an verschiedenen Positionen stehen; im Board umsortierbar.
|
||||
f('position', 'float', { interface: 'input', default: 0, note: 'Lineare Position in der Warteschlange (klein = zuerst).' }),
|
||||
// Pro-Druck-Übersteuerung der Template-Maße (Breite/Höhe/X/Y/Scale/Rot).
|
||||
// Vom Druck-Popup gesetzt; leer = Template-Werte verwenden.
|
||||
f('params', 'json', { interface: 'input-code', note: 'Maß-Übersteuerung {width,height,xpos,ypos,scale,rotation} (leer = Template).' }),
|
||||
@@ -1296,9 +1303,9 @@ async function applyPermissions() {
|
||||
['status', 'artifact_path', 'payment_status', 'payment_ref', 'production_status', 'brief_gedruckt', 'kuvert_gedruckt']);
|
||||
await ensurePermission(svcPol.id, 'jobs', 'create');
|
||||
// Agent meldet Druckfortschritt zurück.
|
||||
await ensurePermission(svcPol.id, 'jobs', 'update', ['status', 'priority', 'error']);
|
||||
// Agent meldet Maschinenzustand (Heartbeat).
|
||||
await ensurePermission(svcPol.id, 'machines', 'update', ['status', 'last_seen']);
|
||||
await ensurePermission(svcPol.id, 'jobs', 'update', ['status', 'priority', 'error', 'position']);
|
||||
// Agent meldet Maschinenzustand (Heartbeat) + fordert Formatwechsel an.
|
||||
await ensurePermission(svcPol.id, 'machines', 'update', ['status', 'last_seen', 'pending_format']);
|
||||
await ensurePermission(svcPol.id, 'status_history', 'create');
|
||||
// Hochgeladene Druckdateien lesen (Backend führt PDFs für den Charge-Sammeldruck
|
||||
// zusammen und lädt sie dafür über /assets).
|
||||
|
||||
@@ -1249,6 +1249,15 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
const modus = (body.mode === 'direct' && !busy) ? 'direct' : 'queue';
|
||||
const prio = modus === 'direct' ? 100 : 0;
|
||||
|
||||
// Nächste freie Position in der linearen Warteschlange dieser Maschine
|
||||
// (neue Jobs hängen hinten an; im Board umsortierbar).
|
||||
const letzte = await svc('jobs').readByQuery({
|
||||
filter: { machine: { _eq: body.machine }, status: { _in: ['queued', 'printing'] } },
|
||||
sort: ['-position'], limit: 1, fields: ['position'],
|
||||
});
|
||||
let pos = (letzte && letzte.length && Number.isFinite(Number(letzte[0].position))) ? Number(letzte[0].position) : 0;
|
||||
const nextPos = () => (pos += 1);
|
||||
|
||||
// Reinigt eine Maß-Übersteuerung auf erlaubte Zahlenfelder (oder null).
|
||||
const clampParams = (p) => {
|
||||
if (!p || typeof p !== 'object') return null;
|
||||
@@ -1274,7 +1283,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
params: clampParams(sec.params), numbers: (typeof sec.numbers === 'string' && sec.numbers.trim()) ? sec.numbers.trim() : null,
|
||||
// Rollenschonende Zeichenreihenfolge (A6 quer) – nur fürs Schriftstück sinnvoll.
|
||||
spaltenweise: !!(sec.spaltenweise || body.spaltenweise),
|
||||
status: 'queued', priority: prio,
|
||||
status: 'queued', priority: prio, position: nextPos(),
|
||||
}));
|
||||
}
|
||||
if (!jobs.length) return res.status(400).json({ error: 'Kein Abschnitt zum Drucken gewählt.' });
|
||||
@@ -1284,14 +1293,14 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
const briefTpl = await tplByKey(LETTER_TPL[order.format?.key] || 'a4_single');
|
||||
jobs.push(await svc('jobs').createOne({
|
||||
order: order.id, machine: body.machine, type: 'brief', template: briefTpl,
|
||||
format: bf.key, format_label: bf.label, status: 'queued', priority: prio,
|
||||
format: bf.key, format_label: bf.label, status: 'queued', priority: prio, position: nextPos(),
|
||||
}));
|
||||
if (order.needs_envelope && order.envelope_labeling && order.envelope_labeling !== 'keine') {
|
||||
const ef = umschlagFormatOf(order);
|
||||
const envTpl = await tplByKey(ENVELOPE_TPL[order.envelope_format] || 'kuvert_a_v1');
|
||||
jobs.push(await svc('jobs').createOne({
|
||||
order: order.id, machine: body.machine, type: 'umschlag', template: envTpl,
|
||||
format: ef.key, format_label: ef.label, status: 'queued', priority: prio,
|
||||
format: ef.key, format_label: ef.label, status: 'queued', priority: prio, position: nextPos(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1345,6 +1354,22 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
return Object.keys(out).length ? out : null;
|
||||
};
|
||||
|
||||
// Lineare Position je Maschine: Startwert = höchste vorhandene Position der
|
||||
// Maschine, dann fortlaufend. So hängen die Sammel-Jobs hinten an und behalten
|
||||
// die (nach Auftragsnummer sortierte) Reihenfolge – im Board umsortierbar.
|
||||
const posByMachine = {};
|
||||
const nextPos = async (machine) => {
|
||||
const key = String(machine);
|
||||
if (posByMachine[key] === undefined) {
|
||||
const letzte = await svc('jobs').readByQuery({
|
||||
filter: { machine: { _eq: machine }, status: { _in: ['queued', 'printing'] } },
|
||||
sort: ['-position'], limit: 1, fields: ['position'],
|
||||
});
|
||||
posByMachine[key] = (letzte && letzte.length && Number.isFinite(Number(letzte[0].position))) ? Number(letzte[0].position) : 0;
|
||||
}
|
||||
return (posByMachine[key] += 1);
|
||||
};
|
||||
|
||||
const jobs = [];
|
||||
const uebersprungen = [];
|
||||
const mkJob = async (order, typ, fmt) => {
|
||||
@@ -1356,7 +1381,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
jobs.push(await svc('jobs').createOne({
|
||||
order: order.id, machine: cfg.machine, type: typ, template: cfg.template,
|
||||
format: fmt.key, format_label: cfg.label || fmt.label, params: clampParams(cfg.params),
|
||||
status: 'queued', priority: prioFor(cfg.machine),
|
||||
status: 'queued', priority: prioFor(cfg.machine), position: await nextPos(cfg.machine),
|
||||
}));
|
||||
};
|
||||
for (const order of orders) {
|
||||
|
||||
@@ -284,14 +284,14 @@ const Board = defineComponent({
|
||||
async function ladeJobs() {
|
||||
const filter = { status: { _in: ['queued', 'printing', 'failed'] } };
|
||||
try {
|
||||
return await api.get('/items/jobs', { params: { limit: -1, sort: '-priority,date_created', filter, fields: JOB_FELDER + ',format,format_label' } });
|
||||
return await api.get('/items/jobs', { params: { limit: -1, sort: '-priority,position,date_created', filter, fields: JOB_FELDER + ',format,format_label,position' } });
|
||||
} catch {
|
||||
return api.get('/items/jobs', { params: { limit: -1, sort: '-priority,date_created', filter, fields: JOB_FELDER } });
|
||||
}
|
||||
}
|
||||
async function ladeMachines() {
|
||||
try {
|
||||
return await api.get('/items/machines', { params: { limit: -1, fields: 'id,name,active,status,last_seen,current_format' } });
|
||||
return await api.get('/items/machines', { params: { limit: -1, fields: 'id,name,active,status,last_seen,current_format,pending_format' } });
|
||||
} catch {
|
||||
return api.get('/items/machines', { params: { limit: -1, fields: 'id,name,active,status,last_seen' } });
|
||||
}
|
||||
@@ -578,17 +578,6 @@ const Board = defineComponent({
|
||||
} catch (e) { fehler.value = e?.response?.data?.error || e?.message || 'Einreihen fehlgeschlagen.'; }
|
||||
}
|
||||
|
||||
const queuedByMachine = computed(() => {
|
||||
const groups = {};
|
||||
for (const m of aktiveMaschinen.value) groups[m.id] = { machine: m, jobs: [] };
|
||||
groups.__ohne = { machine: { name: 'Ohne Maschine' }, jobs: [] };
|
||||
for (const j of jobs.value) {
|
||||
if (j.status !== 'queued') continue;
|
||||
if (!trifftSuche([j.order?.order_number, j.output_ref, j.type].filter(Boolean).join(' '))) continue;
|
||||
(groups[j.machine?.id] || groups.__ohne).jobs.push(j);
|
||||
}
|
||||
return Object.values(groups).filter((g) => g.jobs.length);
|
||||
});
|
||||
const tabCount = (key) => {
|
||||
if (key === 'suche') return sucheTreffer.value.length;
|
||||
if (key === 'neu') return neueOrders.value.length + neueRequests.value.length;
|
||||
@@ -1292,6 +1281,31 @@ const Board = defineComponent({
|
||||
try { await api.delete(`/items/jobs/${job.id}`); flash('Job entfernt.'); await laden(); }
|
||||
catch (e) { fehler.value = e?.message || 'Job konnte nicht entfernt werden.'; }
|
||||
}
|
||||
// Lineare Reihenfolge: Positionen 1..n in Listenreihenfolge schreiben (robust,
|
||||
// auch wenn Altbestand alle Position 0 hatte).
|
||||
async function setzeReihenfolge(liste) {
|
||||
try {
|
||||
for (let i = 0; i < liste.length; i++) {
|
||||
if (Number(liste[i].position) !== i + 1) await api.patch(`/items/jobs/${liste[i].id}`, { position: i + 1 });
|
||||
}
|
||||
await laden();
|
||||
} catch (e) { fehler.value = e?.message || 'Umsortieren fehlgeschlagen.'; }
|
||||
}
|
||||
function listeUmstellen(liste, idx, ziel) {
|
||||
if (ziel < 0 || ziel >= liste.length) return liste;
|
||||
const arr = liste.slice(); const [x] = arr.splice(idx, 1); arr.splice(ziel, 0, x); return arr;
|
||||
}
|
||||
// Agent hat einen Formatwechsel angefordert (pending_format) → bestätigen: das
|
||||
// angeforderte Format wird „eingelegt", die Anforderung gelöscht, Agent druckt weiter.
|
||||
async function bestaetigeFormat(machine) {
|
||||
try {
|
||||
const f = (machine.pending_format || '').trim();
|
||||
await api.patch(`/items/machines/${machine.id}`, { current_format: f || null, pending_format: null });
|
||||
machine.current_format = f || null; machine.pending_format = null;
|
||||
flash(f ? `Format „${f}" eingelegt – Druck läuft weiter.` : 'Bestätigt.');
|
||||
await laden();
|
||||
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e?.message || 'Bestätigen fehlgeschlagen.'; }
|
||||
}
|
||||
async function setRequestStatus(req, st) {
|
||||
try { await api.patch(`/items/offer_requests/${req.id}`, { status: st }); flash('Anfrage aktualisiert.'); await laden(); }
|
||||
catch (e) { fehler.value = e?.message || 'Fehlgeschlagen.'; }
|
||||
@@ -1797,47 +1811,80 @@ const Board = defineComponent({
|
||||
return items.length ? items : subdued('Nichts Neues.');
|
||||
}
|
||||
if (tab.value === 'warteschlange') {
|
||||
const gruppen = queuedByMachine.value;
|
||||
if (!gruppen.length) return subdued('Warteschlange leer.');
|
||||
return gruppen.map((g) => {
|
||||
const m = g.machine;
|
||||
const queuedFor = (mid) => jobs.value.filter((j) => j.status === 'queued' && String(j.machine?.id) === String(mid));
|
||||
const printingFor = (mid) => jobs.value.filter((j) => j.status === 'printing' && String(j.machine?.id) === String(mid));
|
||||
const fmtLabel = (j) => j && (j.format_label || j.format || 'Ohne Format');
|
||||
const zeileStil = 'border:1px solid #d2dae6;border-radius:10px;padding:9px 11px;background:#ffffff;margin-bottom:6px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;';
|
||||
const ordnBtn = (ic, titel, onClick, disabled) => h('button', { type: 'button', disabled, title: titel, onClick,
|
||||
style: `cursor:${disabled ? 'not-allowed' : 'pointer'};border:1px solid #b4c0d2;background:#ffffff;border-radius:7px;width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;color:${disabled ? '#c7d0df' : '#1b2430'};flex-shrink:0;` }, icon(ic));
|
||||
|
||||
const maschinenPanel = (m) => {
|
||||
const qlist = queuedFor(m.id);
|
||||
const plist = printingFor(m.id);
|
||||
if (!qlist.length && !plist.length) return null;
|
||||
const aktiv = (m.current_format || '').trim();
|
||||
// Jobs je Maschine nach Format bündeln (jede Format-Queue eigenständig).
|
||||
const fmt = {};
|
||||
for (const j of g.jobs) {
|
||||
const key = j.format || '';
|
||||
(fmt[key] || (fmt[key] = { key, label: j.format_label || j.format || 'Ohne Format', jobs: [] })).jobs.push(j);
|
||||
}
|
||||
const formatBlock = (fg) => {
|
||||
const laeuft = !!aktiv && fg.key === aktiv;
|
||||
const kopf = h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [
|
||||
h('span', { style: `font-weight:700;font-size:14px;color:${ACCENT};` }, fg.label),
|
||||
chip(`${fg.jobs.length} Job(s)`),
|
||||
laeuft ? chip('▶ liegt ein – wird gedruckt', '#dff2e2')
|
||||
: (aktiv ? chip('⏸ wartet (anderes Format liegt ein)') : null),
|
||||
]),
|
||||
laeuft
|
||||
? btn('⏹ Stapel stoppen', () => setMachineFormat(m, null), 'danger')
|
||||
: btn('▶ Dieses Format liegt ein → starten', () => setMachineFormat(m, fg.key), 'primary'),
|
||||
const pend = (m.pending_format || '').trim();
|
||||
const labelVon = (key) => (qlist.find((j) => j.format === key)?.format_label || key);
|
||||
const erste = qlist[0];
|
||||
|
||||
const kopf = h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;' }, [icon('print'), m.name, m.status ? subdued('· ' + m.status) : null]),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;' }, [subdued('Eingelegt:'),
|
||||
h('span', { style: `font-weight:600;color:${aktiv ? ACCENT : '#566070'};` }, aktiv ? labelVon(aktiv) : 'keins')]),
|
||||
]);
|
||||
const cards = fg.jobs.map((j) => h('div', { key: 'j' + j.id, style: CARD + 'margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;' }, [
|
||||
h('div', {}, [
|
||||
h('div', { style: 'font-weight:600;' }, `#${j.order?.order_number || j.output_ref || j.id} · ${j.type === 'umschlag' ? '✉ Kuvert' : '📄 Brief'}`),
|
||||
subdued(`${j.numbers ? 'Nr. ' + j.numbers : 'alle'}${j.priority ? ' · Direkt' : ''}`),
|
||||
]),
|
||||
btn('Entfernen', () => cancelJob(j), 'danger'),
|
||||
|
||||
// Formatwechsel-Anforderung des Agents → bestätigen; sonst Start-Hinweis.
|
||||
const banner = pend
|
||||
? h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;background:#fbeccb;border:1px solid #f0d79a;border-radius:10px;padding:12px 14px;margin-bottom:12px;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;font-weight:600;color:#8a5a00;' }, [icon('swap_horiz'), `Formatwechsel: bitte „${labelVon(pend)}" einlegen`]),
|
||||
btn('Eingelegt → bestätigen', () => bestaetigeFormat(m), 'primary', 'check'),
|
||||
])
|
||||
: (!aktiv && erste)
|
||||
? h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;background:#eef2f8;border:1px solid #b4c0d2;border-radius:10px;padding:12px 14px;margin-bottom:12px;' }, [
|
||||
subdued('Kein Format eingelegt.'),
|
||||
btn(`Format „${fmtLabel(erste)}" einlegen → starten`, () => setMachineFormat(m, erste.format), 'primary', 'play_arrow'),
|
||||
])
|
||||
: null;
|
||||
|
||||
const laufende = plist.map((j) => h('div', { key: 'p' + j.id, style: zeileStil + 'background:#e3f4e6;border-color:#bfe2c6;' }, [
|
||||
icon('print'), h('span', { style: 'font-weight:600;color:#1b6b2e;' }, `wird gedruckt: #${j.order?.order_number || j.output_ref || j.id} · ${j.type === 'umschlag' ? 'Kuvert' : 'Brief'} (${fmtLabel(j)})`),
|
||||
]));
|
||||
return h('div', { style: SECTION }, [kopf, ...cards]);
|
||||
};
|
||||
return h('div', { style: 'margin-bottom:22px;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px;' }, [
|
||||
h('div', { style: 'font-weight:700;' }, `🖨 ${m.name}${m.status ? ' · ' + m.status : ''}`),
|
||||
aktiv ? subdued(`Eingelegtes Format: ${aktiv}`) : subdued('Kein Format eingelegt – bitte einen Stapel starten.'),
|
||||
|
||||
const zeilen = [];
|
||||
qlist.forEach((j, i) => {
|
||||
if (i > 0 && (qlist[i - 1].format || '') !== (j.format || '')) {
|
||||
zeilen.push(h('div', { key: 'sep' + j.id, style: 'display:flex;align-items:center;gap:6px;margin:8px 0 6px 6px;font-size:12.5px;font-weight:600;color:#8a5a00;' }, [icon('swap_horiz'), `Formatwechsel → ${fmtLabel(j)}`]));
|
||||
}
|
||||
const istAktiv = aktiv && j.format === aktiv;
|
||||
zeilen.push(h('div', { key: 'q' + j.id, style: zeileStil + (istAktiv ? 'border-color:#bcd0ef;background:#f2f6fd;' : '') }, [
|
||||
h('div', { style: 'width:26px;height:26px;border-radius:7px;background:#eef2f8;border:1px solid #b4c0d2;display:inline-flex;align-items:center;justify-content:center;font-size:12.5px;font-weight:700;color:#566070;flex-shrink:0;' }, String(i + 1)),
|
||||
h('div', { style: 'flex:1;min-width:140px;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [
|
||||
h('span', { style: `font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;background:#e3ecfa;color:${ACCENT};border:1px solid #bcd0ef;` }, fmtLabel(j)),
|
||||
h('span', { style: 'font-weight:600;' }, `#${j.order?.order_number || j.output_ref || j.id} · ${j.type === 'umschlag' ? 'Kuvert' : 'Brief'}`),
|
||||
j.priority ? chip('Direkt', '#fbeccb', '#8a5a00') : null,
|
||||
]),
|
||||
...Object.values(fmt).map(formatBlock),
|
||||
]);
|
||||
subdued(j.numbers ? 'Nr. ' + j.numbers : 'alle'),
|
||||
]),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:4px;flex-shrink:0;' }, [
|
||||
ordnBtn('keyboard_double_arrow_up', 'An den Anfang', () => setzeReihenfolge(listeUmstellen(qlist, i, 0)), i === 0),
|
||||
ordnBtn('keyboard_arrow_up', 'Nach oben', () => setzeReihenfolge(listeUmstellen(qlist, i, i - 1)), i === 0),
|
||||
ordnBtn('keyboard_arrow_down', 'Nach unten', () => setzeReihenfolge(listeUmstellen(qlist, i, i + 1)), i === qlist.length - 1),
|
||||
ordnBtn('keyboard_double_arrow_down', 'Ans Ende', () => setzeReihenfolge(listeUmstellen(qlist, i, qlist.length - 1)), i === qlist.length - 1),
|
||||
ordnBtn('close', 'Entfernen', () => cancelJob(j), false),
|
||||
]),
|
||||
]));
|
||||
});
|
||||
|
||||
return h('div', { key: 'wm' + m.id, style: SECTION + 'margin-bottom:18px;' }, [
|
||||
kopf, banner, ...laufende,
|
||||
qlist.length ? h('div', { style: 'font-size:11.5px;font-weight:600;color:#566070;text-transform:uppercase;letter-spacing:.03em;margin:4px 0 8px;' }, `Geplante Reihenfolge (${qlist.length})`) : null,
|
||||
...zeilen,
|
||||
].filter(Boolean));
|
||||
};
|
||||
|
||||
const panels = aktiveMaschinen.value.map(maschinenPanel).filter(Boolean);
|
||||
return panels.length ? panels : subdued('Warteschlange leer.');
|
||||
}
|
||||
if (tab.value === 'druck') {
|
||||
const list = ordersByStatus('im_druck');
|
||||
|
||||
@@ -94,8 +94,8 @@ const Board = {
|
||||
+ 'envelope_labeling,envelope_format,api_client.id,api_client.name,product.name,'
|
||||
+ 'customer.first_name,customer.last_name,customer.company',
|
||||
} }),
|
||||
api.get('/items/jobs', { params: { limit: -1, filter: { status: { _in: ['queued', 'printing'] } }, fields: 'id,type,status,format,format_label,order.id' } }).catch(() => ({ data: { data: [] } })),
|
||||
api.get('/items/machines', { params: { limit: -1, filter: { active: { _eq: true } }, fields: 'id,name,current_format,status,active' } }).catch(() => ({ data: { data: [] } })),
|
||||
api.get('/items/jobs', { params: { limit: -1, sort: '-priority,position,date_created', filter: { status: { _in: ['queued', 'printing'] } }, fields: 'id,type,status,format,format_label,position,machine.id,order.id,order.order_number' } }).catch(() => ({ data: { data: [] } })),
|
||||
api.get('/items/machines', { params: { limit: -1, filter: { active: { _eq: true } }, fields: 'id,name,current_format,pending_format,status,active' } }).catch(() => ({ data: { data: [] } })),
|
||||
api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,default_format,search_default' } }).catch(() => ({ data: { data: [] } })),
|
||||
]);
|
||||
const vorherAnzahl = orders.value.length;
|
||||
@@ -162,11 +162,6 @@ const Board = {
|
||||
|
||||
const orderById = computed(() => { const m = {}; for (const o of orders.value) m[String(o.id)] = o; return m; });
|
||||
const eingeplantSet = computed(() => new Set(jobs.value.map((j) => String(j.order?.id)).filter((x) => x && x !== 'null')));
|
||||
const queuedByFormat = computed(() => {
|
||||
const map = {};
|
||||
for (const j of jobs.value) { if (j.status !== 'queued') continue; const f = j.format || '?'; if (!map[f]) map[f] = { n: 0, label: j.format_label || FORMAT_LABEL[f] || f }; map[f].n += 1; }
|
||||
return map;
|
||||
});
|
||||
const gefiltert = computed(() => {
|
||||
const q = sucheQuery.value.trim().toLowerCase();
|
||||
let list = orders.value;
|
||||
@@ -191,8 +186,25 @@ const Board = {
|
||||
} catch (e) { err.value = 'Speichern fehlgeschlagen.'; }
|
||||
}
|
||||
async function formatStarten(machine, fmt) {
|
||||
try { await api.patch('/items/machines/' + machine.id, { current_format: fmt }); machine.current_format = fmt; flash(`${machine.name}: Format „${FORMAT_LABEL[fmt] || fmt}" eingelegt → Warteschlange läuft.`); }
|
||||
catch (e) { err.value = 'Format konnte nicht gestartet werden.'; }
|
||||
try {
|
||||
await api.patch('/items/machines/' + machine.id, { current_format: fmt, pending_format: null });
|
||||
machine.current_format = fmt; machine.pending_format = null;
|
||||
flash(`${machine.name}: Format „${FORMAT_LABEL[fmt] || fmt}" eingelegt → Warteschlange läuft.`);
|
||||
await laden(true);
|
||||
} catch (e) { err.value = 'Format konnte nicht gestartet werden.'; }
|
||||
}
|
||||
// Queued-Jobs einer Maschine in Plan-Reihenfolge (jobs.value ist bereits sortiert).
|
||||
const jobsFuerMaschine = (mid) => jobs.value.filter((j) => j.status === 'queued' && String(j.machine?.id) === String(mid));
|
||||
// Aufeinanderfolgende gleiche Formate zu Segmenten zusammenfassen.
|
||||
function formatSegmente(list) {
|
||||
const seg = [];
|
||||
for (const j of list) {
|
||||
const key = j.format || '?';
|
||||
const last = seg[seg.length - 1];
|
||||
if (last && last.key === key) last.n += 1;
|
||||
else seg.push({ key, label: j.format_label || FORMAT_LABEL[key] || key, n: 1 });
|
||||
}
|
||||
return seg;
|
||||
}
|
||||
|
||||
// ── Charge-Bildung (API-Aufträge gruppieren) ──────────────────────────────
|
||||
@@ -384,17 +396,41 @@ const Board = {
|
||||
]);
|
||||
|
||||
function maschinenPanel() {
|
||||
const qf = queuedByFormat.value; const formate = Object.keys(qf);
|
||||
const startBtn = (label, onClick, primär) => h('button', { type: 'button', onClick,
|
||||
style: `cursor:pointer;border-radius:8px;padding:9px 15px;font:inherit;font-size:14px;font-weight:600;display:inline-flex;align-items:center;gap:6px;border:1px solid;${primär ? 'border-color:#1E50A5;background:#1E50A5;color:#fff;' : 'border-color:#b4c0d2;background:#ffffff;color:#1b2430;'}` },
|
||||
[icon(primär ? 'check' : 'play_arrow'), label]);
|
||||
const maschinenZeile = (m) => {
|
||||
const q = jobsFuerMaschine(m.id);
|
||||
const aktiv = (m.current_format || '').trim();
|
||||
const pend = (m.pending_format || '').trim();
|
||||
const segs = formatSegmente(q);
|
||||
const labelVon = (k) => (q.find((j) => j.format === k)?.format_label || FORMAT_LABEL[k] || k);
|
||||
// Aktion: Formatwechsel bestätigen > erstes Format starten > läuft.
|
||||
let aktion;
|
||||
if (pend) aktion = startBtn(`Format „${labelVon(pend)}" eingelegt → bestätigen`, () => formatStarten(m, pend), true);
|
||||
else if (!aktiv && segs.length) aktion = startBtn(`Format „${segs[0].label}" einlegen → starten`, () => formatStarten(m, segs[0].key), true);
|
||||
else if (aktiv) aktion = h('span', { style: 'display:inline-flex;align-items:center;gap:5px;font-size:13px;color:#1b6b2e;font-weight:600;' }, [icon('play_arrow'), `läuft: ${labelVon(aktiv)}`]);
|
||||
else aktion = h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine Jobs in der Warteschlange.');
|
||||
// Vorschau der kommenden Format-Segmente.
|
||||
const plan = segs.length ? h('div', { style: 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:8px;' }, [
|
||||
h('span', { style: 'font-size:12px;color:#566070;' }, 'Als Nächstes:'),
|
||||
...segs.slice(0, 8).flatMap((s, i) => {
|
||||
const chip = h('span', { style: `font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;background:${s.key === aktiv ? '#e3ecfa' : '#eef2f8'};color:${s.key === aktiv ? '#1E50A5' : '#566070'};border:1px solid ${s.key === aktiv ? '#bcd0ef' : '#d2dae6'};` }, `${s.label} (${s.n})`);
|
||||
return i === 0 ? [chip] : [h('span', { style: 'color:#b4c0d2;', 'aria-hidden': 'true' }, '›'), chip];
|
||||
}),
|
||||
]) : null;
|
||||
return h('div', { key: 'm' + m.id, style: 'padding:12px 14px;border:1px solid #d2dae6;border-radius:10px;background:#ffffff;' + (pend ? 'border-color:#f0d79a;background:#fdf6e6;' : '') }, [
|
||||
h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;' }, [
|
||||
h('div', { style: 'font-weight:600;' }, [m.name, h('span', { style: 'color:#566070;font-weight:400;font-size:13px;' }, aktiv ? ` · eingelegt: ${labelVon(aktiv)}` : ' · kein Format eingelegt')]),
|
||||
aktion,
|
||||
]),
|
||||
plan,
|
||||
].filter(Boolean));
|
||||
};
|
||||
return h('div', { style: 'border:1px solid #b4c0d2;border-radius:12px;background:#ffffff;margin-bottom:18px;overflow:hidden;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:#566070;padding:10px 14px;border-bottom:1px solid #d2dae6;background:#eef2f8;' }, [icon('print'), 'Maschinen · Format einlegen und starten']),
|
||||
h('div', { style: 'padding:14px;display:flex;flex-direction:column;gap:14px;' },
|
||||
aktiveMaschinen.value.length ? aktiveMaschinen.value.map((m) => h('div', { key: 'm' + m.id, style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;' }, [
|
||||
h('div', { style: 'font-weight:500;min-width:150px;' }, [m.name, h('span', { style: 'color:#566070;font-weight:400;font-size:13px;' }, m.current_format ? ` · eingelegt: ${FORMAT_LABEL[m.current_format] || m.current_format}` : ' · kein Format eingelegt')]),
|
||||
...(formate.length ? formate.map((f) => h('button', { type: 'button', onClick: () => formatStarten(m, f),
|
||||
style: `cursor:pointer;border-radius:8px;padding:8px 14px;font:inherit;font-size:14px;font-weight:500;display:inline-flex;align-items:center;gap:6px;border:1px solid;${m.current_format === f ? 'border-color:#1E50A5;background:#1E50A5;color:#fff;' : 'border-color:#b4c0d2;background:#ffffff;color:#1b2430;'}`,
|
||||
}, [icon('play_arrow'), `${qf[f].label} (${qf[f].n})`]))
|
||||
: [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine Jobs in der Warteschlange.')]),
|
||||
])) : [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine aktive Maschine.')]),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:#566070;padding:10px 14px;border-bottom:1px solid #d2dae6;background:#eef2f8;' }, [icon('print'), 'Maschinen · Format einlegen und bestätigen']),
|
||||
h('div', { style: 'padding:14px;display:flex;flex-direction:column;gap:12px;' },
|
||||
aktiveMaschinen.value.length ? aktiveMaschinen.value.map(maschinenZeile) : [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine aktive Maschine.')]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user