Bestandspflege: Material je Standort, Auto-Abbuchung beim Druck
Datenmodell (bootstrap):
- Collections standorte (seed Skrift/Signascript), inventory (Papier/Kuvert/
Briefmarke je Standort, initiale Zeilen) und inventory_log.
- directus_users.produktionsstandort (m2o standorte; USER_FIELDS-Relation wird
jetzt angelegt); orders.frankiert + porto_klasse; jobs.inventory_done.
Neue Hook-Extension skrift-inventory:
- Bucht beim Job-Status 'printed' automatisch ab (am Kunden-Standort, +10 %):
Brief->Papier (phys. Format), Kuvert->Kuvert + Briefmarke (nur wenn frankiert;
Inland Standard / Ausland International, bzw. porto_klasse). Idempotent.
produktion: neuer Tab 'Bestand' - je Standort, Unterschreitung rot
('nachbestellen'), manuelles Zubuchen + Mindestbestand/Bestand editierbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,8 @@ const USER_FIELDS = [
|
||||
// Veraltet (frühere Mehrfach-Adressliste) – bleibt für Alt-Daten erhalten.
|
||||
f('addresses', 'json', { interface: 'list', hidden: true, note: 'Veraltet – ersetzt durch billing_address/shipping_address.', options: { fields: ADDRESS_SUBFIELDS } }),
|
||||
f('notes', 'text', { interface: 'input-multiline', note: 'Interne Notizen (für den Kunden nicht sichtbar).' }),
|
||||
// Produktionsstandort dieses Kunden (Bestandsabbuchung). Leer = Skrift (Standard).
|
||||
m2o('produktionsstandort', 'standorte', { note: 'Wo die Aufträge dieses Kunden produziert werden. Leer = Skrift.' }),
|
||||
f('sevdesk_contact_id', 'string', { interface: 'input', hidden: true, note: 'Verknüpfter sevDesk-Kontakt (für Rechnungen).' }),
|
||||
// Zusätzliche Login-Mailadressen: Wer sich mit einer dieser Mails per Code
|
||||
// anmeldet, landet im SELBEN Konto (voller Zugriff). Liste von E-Mails.
|
||||
@@ -311,6 +313,10 @@ const collections = [
|
||||
// Rechnungs-/Lieferadresse als Snapshot (für Bestätigung, Rechnung, Versand).
|
||||
f('billing', 'json', { interface: 'input-code', note: 'Rechnungsadresse (Snapshot bei Bestellung).' }),
|
||||
f('shipping_address', 'json', { interface: 'input-code', note: 'Abweichende Lieferadresse (falls vorhanden).' }),
|
||||
// Bestandsabbuchung Briefmarke: von uns frankiert? Leer = automatisch
|
||||
// (Einzelversand = ja, Sammelversand = nein). Bei Sammelversand umstellbar.
|
||||
f('frankiert', 'boolean', { interface: 'boolean', note: 'Von uns frankiert → Briefmarke wird abgebucht. Leer = automatisch (Einzel ja / Sammel nein).' }),
|
||||
dropdown('porto_klasse', ['auto', 'standard', 'grossbrief', 'maxibrief'], { default: 'auto', note: 'Markentyp-Override. auto = Inland 0,95 € / Ausland 1,25 €.' }),
|
||||
f('entries_count', 'integer', { interface: 'input', default: 0 }),
|
||||
f('count_inland', 'integer', { interface: 'input', default: 0, note: 'Empfängerzeilen Deutschland.' }),
|
||||
f('count_ausland', 'integer', { interface: 'input', default: 0, note: 'Empfängerzeilen Ausland.' }),
|
||||
@@ -467,6 +473,8 @@ const collections = [
|
||||
f('numbers', 'string', { interface: 'input', note: 'Zu druckende Briefnummern (1-basiert). Leer = alle.' }),
|
||||
f('spaltenweise', 'boolean', { interface: 'boolean', default: false, note: 'Rollenschonende Zeichenreihenfolge (spaltenweise, für A6 quer). Der Agent lädt das SVG dann umsortiert.' }),
|
||||
f('error', 'text', { interface: 'input-multiline', note: 'Fehlermeldung vom Agent (falls fehlgeschlagen).' }),
|
||||
// Guard: Bestand wurde für diesen Job bereits abgebucht (Idempotenz des Inventar-Hooks).
|
||||
f('inventory_done', 'boolean', { interface: 'boolean', default: false, hidden: true, note: 'Material bereits abgebucht (Bestands-Hook).' }),
|
||||
{ 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: {} },
|
||||
] },
|
||||
@@ -722,6 +730,32 @@ const collections = [
|
||||
f('bundled_at', 'timestamp', { interface: 'datetime', readonly: true }),
|
||||
{ field: 'date_created', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-created'] }, schema: {} },
|
||||
] },
|
||||
|
||||
// ── Bestandspflege (zwei Produktionsstandorte) ─────────────────────────────
|
||||
{ collection: 'standorte', meta: { icon: 'factory', note: 'Produktionsstandorte (für Bestand + Kundenzuordnung).' }, fields: [
|
||||
f('name', 'string', { interface: 'input', required: true, unique: true }),
|
||||
f('aktiv', 'boolean', { interface: 'boolean', default: true }),
|
||||
f('sort', 'integer', { interface: 'input' }),
|
||||
] },
|
||||
{ collection: 'inventory', meta: { icon: 'inventory', note: 'Materialbestand je Standort (Papier/Kuvert/Briefmarke, je Format/Typ).' }, fields: [
|
||||
m2o('standort', 'standorte', { onDelete: 'CASCADE', note: 'Produktionsstandort.' }),
|
||||
dropdown('kategorie', ['papier', 'kuvert', 'briefmarke'], { note: 'Materialart.' }),
|
||||
f('format', 'string', { interface: 'input', note: 'Schlüssel zum Abgleich: Papier/Kuvert = Format-Key (a4,a5p,a6l,dinlang,c6,c4 …); Briefmarke = standard/international/grossbrief …' }),
|
||||
f('bezeichnung', 'string', { interface: 'input', note: 'Lesbare Bezeichnung (z. B. „A4 Papier", „Standard 0,95 €").' }),
|
||||
f('bestand', 'integer', { interface: 'input', default: 0, note: 'Aktueller Bestand.' }),
|
||||
f('mindestbestand', 'integer', { interface: 'input', default: 0, note: 'Schwelle → bei Unterschreitung „nachbestellen".' }),
|
||||
f('einheit', 'string', { interface: 'input', default: 'Stück' }),
|
||||
f('notiz', 'string', { interface: 'input' }),
|
||||
{ field: 'date_updated', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-updated'] }, schema: {} },
|
||||
] },
|
||||
{ collection: 'inventory_log', meta: { icon: 'receipt_long', hidden: true, note: 'Bestandsbuchungen (Abgänge beim Druck, Auffüllungen, Korrekturen).' }, fields: [
|
||||
m2o('inventory', 'inventory', { onDelete: 'CASCADE', note: 'Betroffene Bestandszeile.' }),
|
||||
f('delta', 'integer', { interface: 'input', note: 'Veränderung (negativ = Abgang, positiv = Zugang).' }),
|
||||
dropdown('grund', ['druck', 'auffuellung', 'korrektur'], { note: 'Buchungsgrund.' }),
|
||||
m2o('order', 'orders', { onDelete: 'SET NULL', note: 'Auslösender Auftrag (bei Abgang).' }),
|
||||
f('notiz', 'string', { interface: 'input' }),
|
||||
{ field: 'date_created', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-created'] }, schema: {} },
|
||||
] },
|
||||
];
|
||||
|
||||
// ── Schema anlegen ──────────────────────────────────────────────────────────
|
||||
@@ -752,13 +786,16 @@ async function applySchema() {
|
||||
// Profilfelder direkt am Login-Konto (Kunde = directus_users).
|
||||
console.log('› Kundenfelder an directus_users');
|
||||
for (const fld of USER_FIELDS) {
|
||||
const { __related, __onDelete, ...clean } = fld;
|
||||
if (!existingFields.has(`directus_users.${fld.field}`)) {
|
||||
await api('POST', '/fields/directus_users', fld);
|
||||
await api('POST', '/fields/directus_users', clean);
|
||||
log('field +', `directus_users.${fld.field}`);
|
||||
} else {
|
||||
// Vorhandenes Feld: Oberfläche/Optionen aktualisieren (z. B. Adressliste).
|
||||
try { await api('PATCH', `/fields/directus_users/${fld.field}`, { meta: fld.meta }); } catch { /* egal */ }
|
||||
try { await api('PATCH', `/fields/directus_users/${fld.field}`, { meta: clean.meta }); } catch { /* egal */ }
|
||||
}
|
||||
// m2o-Beziehung (z. B. produktionsstandort → standorte) nachziehen.
|
||||
if (__related) relationsToCreate.push({ collection: 'directus_users', field: fld.field, related: __related, onDelete: __onDelete });
|
||||
}
|
||||
// Ungenutzte Standardfelder ausblenden. Direkt patchen (System-Felder existieren
|
||||
// immer); ein Fehler wird geloggt statt still verschluckt – so sieht man, falls
|
||||
@@ -1063,6 +1100,30 @@ async function applySeed() {
|
||||
else { await api('POST', '/items/render_formats', rf); log('seed + render_formats.' + rf.key); }
|
||||
}
|
||||
|
||||
// ── Standorte + initialer Materialbestand ──────────────────────────────────
|
||||
const standortId = {};
|
||||
for (const s of [{ name: 'Skrift', sort: 1, aktiv: true }, { name: 'Signascript', sort: 2, aktiv: true }]) {
|
||||
const da = await api('GET', `/items/standorte?filter[name][_eq]=${encodeURIComponent(s.name)}&limit=1&fields=id`);
|
||||
if (da && da.length) standortId[s.name] = da[0].id;
|
||||
else { const neu = await api('POST', '/items/standorte', s); standortId[s.name] = neu?.id; log('seed + standorte.' + s.name); }
|
||||
}
|
||||
// Materialzeilen je Standort (Bestand/Mindestbestand füllt der Betreiber im Board).
|
||||
const papierNamen = { a4: 'A4', a5: 'A5', a6: 'A6', dinlang: 'DIN lang' };
|
||||
const material = [
|
||||
...Object.entries(papierNamen).map(([k, n]) => ({ kategorie: 'papier', format: k, bezeichnung: `${n} Papier`, einheit: 'Blatt' })),
|
||||
...[['dinlang', 'DIN lang'], ['c6', 'C6'], ['c4', 'C4']].map(([k, n]) => ({ kategorie: 'kuvert', format: k, bezeichnung: `Kuvert ${n}`, einheit: 'Stück' })),
|
||||
{ kategorie: 'briefmarke', format: 'standard', bezeichnung: 'Standard 0,95 €', einheit: 'Stück' },
|
||||
{ kategorie: 'briefmarke', format: 'international', bezeichnung: 'International 1,25 €', einheit: 'Stück' },
|
||||
];
|
||||
for (const sid of Object.values(standortId)) {
|
||||
if (!sid) continue;
|
||||
for (const m of material) {
|
||||
const da = await api('GET', `/items/inventory?filter[standort][_eq]=${sid}&filter[kategorie][_eq]=${m.kategorie}&filter[format][_eq]=${encodeURIComponent(m.format)}&limit=1&fields=id`);
|
||||
if (da && da.length) continue;
|
||||
await api('POST', '/items/inventory', { standort: sid, bestand: 0, mindestbestand: 0, ...m });
|
||||
}
|
||||
}
|
||||
|
||||
// email_settings (Singleton) – Signatur nur setzen, wenn leer.
|
||||
const es = await api('GET', '/items/email_settings');
|
||||
if (!es || !es.signature) {
|
||||
@@ -1102,7 +1163,8 @@ async function applySeed() {
|
||||
const BIZ = ['products', 'formats', 'motifs', 'price_items', 'pricing_settings',
|
||||
'vouchers', 'voucher_redemptions', 'orders', 'order_entries', 'order_addons', 'jobs', 'status_history',
|
||||
'offer_requests', 'signatures', 'customer_discounts', 'customer_products', 'contacts',
|
||||
'email_templates', 'email_settings', 'muster_settings', 'muster_texts', 'machines', 'format_templates', 'render_formats', 'test_prints'];
|
||||
'email_templates', 'email_settings', 'muster_settings', 'muster_texts', 'machines', 'format_templates', 'render_formats', 'test_prints',
|
||||
'standorte', 'inventory', 'inventory_log'];
|
||||
// Kunde = Login-Konto: Aufträge hängen direkt am Nutzer.
|
||||
const OWN_ORDER = { customer: { _eq: '$CURRENT_USER' } };
|
||||
const OWN_VIA_ORDER = { order: { customer: { _eq: '$CURRENT_USER' } } };
|
||||
|
||||
Reference in New Issue
Block a user