- 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>
162 lines
8.1 KiB
JavaScript
162 lines
8.1 KiB
JavaScript
/**
|
||
* 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 }],
|
||
};
|