Files
skrift-directus/extensions/directus-extension-skrift-statistik/dist/index.js
s4luorth a043a624d4 Board: Charge-Abstand + Aufgaben-Umschalter unter Weiteres; Kontakte/Statistik-Farben
- Board Charge-Karte: gleichmaessige Abstaende (Flex-Spalte), Notiz mit Label,
  aufgeklappte Teil-Auftraege klar abgetrennt + Luft (kein Aneinanderkleben)
- Board "Weiteres": Versandart, Frankierung, Kuvertierung, Brief/Kuvert gedruckt
  direkt umschaltbar (setOrderFeld); frankiert+versanddatum in Order-Abfrage ergaenzt
- Kontakte + Statistik: gleiche kontraststarke feste Farben + sichtbare Rahmen nachgezogen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-01 13:03:28 +02:00

162 lines
8.1 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 #d2dae6;border-radius:10px;padding:16px 18px;background:#ffffff;box-shadow:0 1px 3px rgba(0,0,0,.08);';
const S_INPUT = 'border:1px solid #b4c0d2;border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;background:#ffffff;color:#1b2430;outline:none;width:100%;';
const subdued = (t, extra) => h('div', { style: `color:#566070;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 || '#eef2f8'};color:#1b2430;` }, 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 #d2dae6;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#566070;white-space:nowrap;` }, t);
const td = (t, extra) => h('td', { style: `padding:9px 12px;border-bottom:1px solid #d2dae6;${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:#eef2f8;' }, [
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:#566070;'),
td(o.product?.name || '—', 'color:#566070;'),
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 #b4c0d2;background:#eef2f8;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}`, '#e3ecfa'),
chip(`🌍 Ausland: ${s.ausland}`, '#fbeccb'),
chip(`Σ Gesamt: ${s.gesamt} Briefe`, '#dff2e2'),
]);
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:#566070;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:#fbe3e0;color:#c0392b;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:#eef2f8;' }, [seite]),
});
};
},
});
export default {
id: 'skrift-statistik',
name: 'Statistik',
icon: 'bar_chart',
routes: [{ path: '', component: Board }],
};