Files
skrift-directus/extensions/directus-extension-skrift-statistik/dist/index.js
s4luorth 4aa3fd04e6 Statistik-Modul, Kontakt-Bulk, Notiz-Fix, Ausland-Erkennung, Weiteres-Kontakt
- Neues Modul „Statistik": versendete Briefe je Kunde/Monat, getrennt Inland/
  Ausland, Zeilen aufklappbar (Auftragsliste), mit Suche. Raus aus dem Board.
- Produktion: Auto-Refresh pausiert, während ein Feld bearbeitet wird (Notizen
  gingen sonst beim 15-s-Reload/Statuswechsel verloren). „Neu generieren“/
  „In manuellen Druck“ aus dem Empfänger-Dialog entfernt (liegen unter Weiteres).
  Unter Weiteres jetzt auch Kontakt/Kunde verknüpfen (per E-Mail) und mit × lösen
  – ohne Kundenmail.
- Ausland-Erkennung (zielInland) mit Wortgrenzen (\b) + unbekannter Ländertext →
  Inland: „Schweizer Straße“, „Neusass“, „Polenz“ werden nicht mehr als Ausland
  geflaggt (orders + produktion).
- Kontakte-Modul: Mehrfachauswahl → Status ändern / löschen.
- contacts-Hook: Abwahl von „In Kontakte speichern“ löscht die Kontakte wieder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-11 14:16:07 +02:00

162 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Skrift – Modul „Statistik"
* ===========================================================================
* Versendete Briefe je Kunde und Monat, getrennt nach Inland (Deutschland) und
* Ausland (weltweit). Jede Kundenzeile ist aufklappbar und zeigt die einzelnen
* Aufträge. Oben Monatswahl + Suche (Kunde, E-Mail, Auftragsnummer).
*
* Zählt nur Aufträge mit production_status = 'versendet'. Inland/Ausland kommen
* aus count_inland / count_ausland des Auftrags (bei Anlage bestimmt).
*
* Von Hand als ESM (kein Build-Schritt) – Vue + SDK stellt Directus bereit.
*/
import { defineComponent, ref, computed, onMounted, h, resolveComponent } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const ACCENT = '#1E50A5';
const CARD = 'border:1px solid var(--border-subdued);border-radius:10px;padding:16px 18px;background:var(--background-page,#fff);box-shadow:0 1px 3px rgba(0,0,0,.08);';
const S_INPUT = 'border:1px solid var(--border-normal,#c9d3e0);border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;background:var(--background-page,#fff);color:var(--foreground-normal);outline:none;width:100%;';
const subdued = (t, extra) => h('div', { style: `color:var(--foreground-subdued);font-size:13px;${extra || ''}` }, t);
function chip(text, farbe) {
return h('span', { style: `display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;background:${farbe || 'var(--background-subdued,#eef2f7)'};color:var(--foreground-normal);` }, text);
}
const kundeName = (c) => [c?.first_name, c?.last_name].filter(Boolean).join(' ') || c?.email || '—';
const Board = defineComponent({
setup() {
const api = useApi();
const orders = ref([]);
const loading = ref(false);
const fehler = ref('');
const monat = ref(new Date().toISOString().slice(0, 7)); // YYYY-MM
const suche = ref('');
const offen = ref(new Set()); // aufgeklappte Kundenzeilen
async function laden() {
loading.value = true; fehler.value = '';
try {
const r = await api.get('/items/orders', {
params: {
limit: -1, sort: ['-versandt_am'],
filter: { production_status: { _eq: 'versendet' } },
fields: ['id', 'order_number', 'entries_count', 'count_inland', 'count_ausland',
'versandt_am', 'date_updated', 'date_created', 'product.name',
'customer.first_name', 'customer.last_name', 'customer.email'],
},
});
orders.value = r.data?.data || [];
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Fehler beim Laden.'; }
finally { loading.value = false; }
}
onMounted(laden);
const versandMonat = (o) => String(o.versandt_am || o.date_updated || o.date_created || '').slice(0, 7);
const inlandVon = (o) => Number(o.count_inland) || 0;
const auslandVon = (o) => Number(o.count_ausland) || 0;
const briefeVon = (o) => (inlandVon(o) + auslandVon(o)) || (Number(o.entries_count) || 0);
const statistik = computed(() => {
const q = suche.value.trim().toLowerCase();
const map = new Map();
let inland = 0, ausland = 0, gesamt = 0;
for (const o of orders.value) {
if (versandMonat(o) !== monat.value) continue;
const email = o.customer?.email || '';
const name = kundeName(o.customer);
const key = email || name || '—';
if (q) {
const hay = [name, email, o.order_number].filter(Boolean).join(' ').toLowerCase();
if (!hay.includes(q)) continue;
}
const cur = map.get(key) || { kunde: name, email, inland: 0, ausland: 0, briefe: 0, orders: [] };
cur.inland += inlandVon(o);
cur.ausland += auslandVon(o);
cur.briefe += briefeVon(o);
cur.orders.push(o);
map.set(key, cur);
inland += inlandVon(o); ausland += auslandVon(o); gesamt += briefeVon(o);
}
const zeilen = [...map.entries()].map(([key, v]) => ({ key, ...v }))
.sort((a, b) => b.briefe - a.briefe);
return { zeilen, inland, ausland, gesamt };
});
const toggle = (key) => { const s = new Set(offen.value); s.has(key) ? s.delete(key) : s.add(key); offen.value = s; };
const th = (t, extra) => h('th', { style: `text-align:${extra || 'left'};padding:9px 12px;border-bottom:2px solid var(--border-subdued);font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);white-space:nowrap;` }, t);
const td = (t, extra) => h('td', { style: `padding:9px 12px;border-bottom:1px solid var(--border-subdued);${extra || ''}` }, t);
function auftragsListe(z) {
const rows = z.orders.slice().sort((a, b) => String(b.versandt_am || '').localeCompare(String(a.versandt_am || '')));
return h('tr', {}, h('td', { colspan: 5, style: 'padding:0 12px 12px;background:var(--background-subdued,#f7f9fc);' }, [
h('table', { style: 'width:100%;border-collapse:collapse;font-size:13px;' }, [
h('tbody', {}, rows.map((o) => h('tr', {}, [
td(`#${o.order_number}`, 'font-weight:600;'),
td(String(o.versandt_am || o.date_updated || '').slice(0, 10) || '—', 'color:var(--foreground-subdued);'),
td(o.product?.name || '—', 'color:var(--foreground-subdued);'),
td(`🇩🇪 ${inlandVon(o)}`, 'text-align:right;'),
td(`🌍 ${auslandVon(o)}`, 'text-align:right;'),
]))),
]),
]));
}
return () => {
const s = statistik.value;
const kopf = h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px;' }, [
h('label', { style: 'font-weight:600;' }, 'Monat:'),
h('input', { type: 'month', value: monat.value, style: S_INPUT + 'width:auto;',
onInput: (e) => { monat.value = e.target.value || monat.value; } }),
h('input', { type: 'search', value: suche.value, placeholder: '🔎 Kunde, E-Mail, Auftragsnr. …',
style: S_INPUT + 'width:auto;flex:1;min-width:200px;', onInput: (e) => { suche.value = e.target.value; } }),
h('button', { type: 'button', onClick: laden,
style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-subdued);border-radius:7px;padding:8px 14px;font:inherit;font-size:13px;' }, '↻'),
]);
const summen = h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;' }, [
chip(`🇩🇪 Inland: ${s.inland}`, 'var(--primary-25,rgba(30,80,165,.14))'),
chip(`🌍 Ausland: ${s.ausland}`, 'var(--warning-25,rgba(230,160,0,.15))'),
chip(`Σ Gesamt: ${s.gesamt} Briefe`, 'var(--success-25,rgba(46,125,50,.15))'),
]);
const inhalt = loading.value ? subdued('Lädt …')
: !s.zeilen.length ? subdued('Keine versendeten Briefe in diesem Monat.')
: h('div', { style: CARD + 'overflow-x:auto;padding:0;' }, [
h('table', { style: 'width:100%;border-collapse:collapse;font-size:14px;' }, [
h('thead', {}, h('tr', {}, [th(''), th('Kunde'), th('Inland', 'right'), th('Ausland', 'right'), th('Gesamt', 'right')])),
h('tbody', {}, s.zeilen.flatMap((z) => {
const auf = offen.value.has(z.key);
const zeile = h('tr', {
onClick: () => toggle(z.key),
style: 'cursor:pointer;',
}, [
td(auf ? '▾' : '▸', 'color:var(--foreground-subdued);width:24px;'),
td(h('div', {}, [
h('div', { style: 'font-weight:600;' }, z.kunde),
z.email ? subdued(z.email, 'font-size:12px;') : null,
])),
td(String(z.inland), 'text-align:right;'),
td(String(z.ausland), 'text-align:right;'),
td(String(z.briefe), 'text-align:right;font-weight:700;color:' + ACCENT + ';'),
]);
return auf ? [zeile, auftragsListe(z)] : [zeile];
})),
]),
]);
const seite = h('div', { style: 'max-width:960px;margin:0 auto;padding:24px 20px 60px;' }, [
fehler.value ? h('div', { style: 'background:var(--danger-25,rgba(198,40,40,.12));color:var(--danger,#c62828);padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, fehler.value) : null,
kopf, summen, inhalt,
]);
return h(resolveComponent('private-view'), { title: 'Statistik' }, {
default: () => h('div', { style: 'min-height:100%;background:var(--background-subdued,#f1f4f9);' }, [seite]),
});
};
},
});
export default {
id: 'skrift-statistik',
name: 'Statistik',
icon: 'bar_chart',
routes: [{ path: '', component: Board }],
};