diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index 16cb761..a4bbf04 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -310,6 +310,7 @@ const collections = [ dropdown('payment_status', ['offen', 'bezahlt', 'storniert'], { default: 'offen' }), f('payment_ref', 'string', { interface: 'input' }), f('artifact_path', 'string', { interface: 'input', note: 'Verweis auf Backend-Output.' }), + f('internal_note', 'text', { interface: 'input-multiline', note: 'Interne Notiz zum Auftrag (Produktion).' }), { field: 'date_created', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-created'] }, schema: {} }, { field: 'date_updated', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-updated'] }, schema: {} }, // Produktionsansicht: durchsuchbare Empfängertabelle (Briefnummer + volle @@ -521,6 +522,13 @@ const collections = [ // Kundenspezifische Angebote: frei benannte Aufschläge (pro Stück) + Namens-Zusatz, // die der eingeloggte Kunde im Konfigurator auswählt. Kein Festpreis (Phase 2). + // Notizen je zusammengefasster Charge (Schlüssel = Kunde|Produkt|Versandtag|Eingang). + { collection: 'charge_notes', meta: { icon: 'sticky_note_2', note: 'Notizen je Charge (Produktions-Board).', hidden: true }, fields: [ + f('charge_key', 'string', { interface: 'input', unique: true, required: true, note: 'Charge-Schlüssel (kunde|produkt|versandtag|eingang).' }), + f('note', 'text', { interface: 'input-multiline' }), + { field: 'date_updated', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-updated'] }, schema: {} }, + ] }, + { collection: 'customer_offers', meta: { icon: 'local_offer', note: 'Kundenangebote: Aufschläge/Name-Zusatz je Kunde+Produkt (Kunde wählt im Konfigurator).' }, fields: [ m2o('customer', 'directus_users', { onDelete: 'CASCADE' }), m2o('product', 'products', { onDelete: 'CASCADE' }), diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index db610f1..4cbf341 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -104,6 +104,15 @@ function chip(text, farbe) { function subdued(text, extra) { return h('div', { style: `color:var(--foreground-subdued);font-size:13px;${extra || ''}` }, text); } +// Editierbares Notizfeld (speichert beim Verlassen/Blur). +function notizFeld(wert, onSave, ph) { + return h('textarea', { + rows: 2, placeholder: ph || 'Notiz …', + style: 'width:100%;margin:6px 0 2px;border:1px solid var(--border-subdued);border-radius:6px;padding:6px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);resize:vertical;', + value: wert || '', + onChange: (e) => onSave(e.target.value), + }, wert || ''); +} // Zielland-Erkennung je Empfänger (wie im Backend): Deutschland, wenn Land DE/leer, // „Deutschland" im Freitext, „D-" vor der PLZ oder nur PLZ ohne Auslandshinweis. function zielInland(e) { @@ -187,7 +196,7 @@ const Board = defineComponent({ + 'envelope_labeling,shipping_type,shipping_day,keep_contacts,entries_count,count_inland,count_ausland,brief_gedruckt,kuvert_gedruckt,' + 'date_updated,date_created,format.key,format.name,product.name,product.input_mode,' + 'source,api_client.id,api_client.name,' - + 'source_file,' + + 'source_file,internal_note,' + 'customer.first_name,customer.last_name,customer.email,customer.phone'; async function ladeOrders() { try { @@ -260,6 +269,32 @@ const Board = defineComponent({ } catch { /* Anzeige läuft dann ohne „gesendet" weiter */ } } + // Notizen je Charge (charge_key → Notiz) + Speichern von Auftrags-/Charge-Notizen. + const chargeNotizen = ref({}); + async function ladeChargeNotizen() { + try { + const r = await api.get('/items/charge_notes', { params: { limit: -1, fields: 'charge_key,note' } }); + const m = {}; + for (const x of r.data?.data || []) m[x.charge_key] = x.note || ''; + chargeNotizen.value = m; + } catch { /* Notizen optional */ } + } + async function setOrderNotiz(order, v) { + const val = String(v || ''); + if (val === (order.internal_note || '')) return; + try { await api.patch('/items/orders/' + order.id, { internal_note: val }); order.internal_note = val; } + catch { fehler.value = 'Notiz konnte nicht gespeichert werden.'; } + } + async function setChargeNotiz(key, v) { + const val = String(v || ''); + if (val === (chargeNotizen.value[key] || '')) return; + try { + const ex = (await api.get('/items/charge_notes', { params: { filter: { charge_key: { _eq: key } }, limit: 1, fields: 'id' } })).data?.data?.[0]; + if (ex) await api.patch('/items/charge_notes/' + ex.id, { note: val }); + else await api.post('/items/charge_notes', { charge_key: key, note: val }); + chargeNotizen.value = { ...chargeNotizen.value, [key]: val }; + } catch { fehler.value = 'Charge-Notiz konnte nicht gespeichert werden.'; } + } async function laden(leise = false) { // leise = Hintergrund-Aktualisierung: kein Spinner, kein Fehlerbanner. if (!leise) { loading.value = true; fehler.value = ''; } @@ -284,7 +319,7 @@ const Board = defineComponent({ muster.value = ms.data?.data || { text_du: '', text_sie: '' }; // Empfänger-Index nur beim vollen Laden neu bauen (nicht bei der 15-s- // Hintergrund-Aktualisierung) – im Hintergrund, blockiert das Board nicht. - if (!leise) { ladeEmpfIndex(); ladeSendIndex(); } + if (!leise) { ladeEmpfIndex(); ladeSendIndex(); ladeChargeNotizen(); } } catch (e) { if (!leise) fehler.value = e?.response?.data?.errors?.[0]?.message || e?.message || 'Laden fehlgeschlagen.'; } finally { if (!leise) loading.value = false; } @@ -885,7 +920,7 @@ const Board = defineComponent({ send.brief.length ? chip('🖨 Brief gesendet: ' + send.brief.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null, send.umschlag.length ? chip('✉ Kuvert gesendet: ' + send.umschlag.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null, ]) : null; - return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, meta, badges, actionButtons(o)]); + return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, meta, badges, notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'), actionButtons(o)]); } function requestCard(req) { const istMuster = req.type === 'muster'; @@ -929,7 +964,8 @@ const Board = defineComponent({ c.faellig ? chip('⏰ fällig bis ' + dmY(c.faellig), faelligFarbe) : null, ]); const body = auf ? h('div', { style: 'margin-top:12px;display:flex;flex-direction:column;gap:2px;' }, c.orders.map((o) => orderCard(o, opts))) : null; - return h('div', { key: 'c' + c.key, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, body]); + const chargeNotiz = notizFeld(chargeNotizen.value[c.key], (v) => setChargeNotiz(c.key, v), 'Notiz zur Charge …'); + return h('div', { key: 'c' + c.key, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, chargeNotiz, body]); } // Aufträge einer Ansicht rendern: bei aktiver Suche als EINZELaufträge (damit