not bad
This commit is contained in:
1 parent
1ea3de74f2
commit
9db080faea
8 files changed
+2064
-55
No files matched your search
@@ -6,6 +6,7 @@ const PALETTE = ["#4f46e5", "#0ea5e9", "#f59e0b", "#10b981", "#ec4899", "#8b5cf6
|
||||
|
||||
const els = {
|
||||
month: document.getElementById("month-select"),
|
||||
quarter: document.getElementById("quarter-select"),
|
||||
year: document.getElementById("year-select"),
|
||||
refresh: document.getElementById("refresh-btn"),
|
||||
download: document.getElementById("download-btn"),
|
||||
@@ -23,6 +24,37 @@ const els = {
|
||||
vatTable: document.querySelector("#vat-table tbody"),
|
||||
vatPeriod: document.getElementById("vat-period"),
|
||||
vatState: document.getElementById("vat-state"),
|
||||
// ODOO · Rechnungen pro Monat
|
||||
odooKpis: document.getElementById("odoo-kpis"),
|
||||
odooChart: document.getElementById("odoo-chart"),
|
||||
odooLegend: document.getElementById("odoo-legend"),
|
||||
odooPeriod: document.getElementById("odoo-period"),
|
||||
odooState: document.getElementById("odoo-state"),
|
||||
odooPivotPreset: document.getElementById("odoo-pivot-preset"),
|
||||
odooFilterTag: document.getElementById("odoo-filter-tag"),
|
||||
odooFilterBestellnummer: document.getElementById("odoo-filter-bestellnummer"),
|
||||
odooFilterLieferland: document.getElementById("odoo-filter-lieferland"),
|
||||
odooFilterNummer: document.getElementById("odoo-filter-rechnungsnummer"),
|
||||
// Gesamt · Alle Rechnungen pro Monat (Pivot, Experiment)
|
||||
pivotKpis: document.getElementById("pivot-kpis"),
|
||||
pivotPeriod: document.getElementById("pivot-period"),
|
||||
pivotState: document.getElementById("pivot-state"),
|
||||
pivotPreset: document.getElementById("pivot-preset"),
|
||||
pivotFilterTag: document.getElementById("pivot-filter-tag"),
|
||||
pivotFilterBestellnummer: document.getElementById("pivot-filter-bestellnummer"),
|
||||
pivotFilterLieferland: document.getElementById("pivot-filter-lieferland"),
|
||||
pivotFilterNummer: document.getElementById("pivot-filter-rechnungsnummer"),
|
||||
pivotFilterQuelle: document.getElementById("pivot-filter-quelle"),
|
||||
// Schweiz · Quartal Schweiz
|
||||
chKpis: document.getElementById("ch-kpis"),
|
||||
chPeriod: document.getElementById("ch-period"),
|
||||
chState: document.getElementById("ch-state"),
|
||||
chPreset: document.getElementById("ch-pivot-preset"),
|
||||
chFilterTag: document.getElementById("ch-filter-tag"),
|
||||
chFilterBestellnummer: document.getElementById("ch-filter-bestellnummer"),
|
||||
chFilterLieferland: document.getElementById("ch-filter-lieferland"),
|
||||
chFilterNummer: document.getElementById("ch-filter-rechnungsnummer"),
|
||||
chFilterQuelle: document.getElementById("ch-filter-quelle"),
|
||||
// Auth
|
||||
authOverlay: document.getElementById("auth-overlay"),
|
||||
authForm: document.getElementById("auth-form"),
|
||||
@@ -33,8 +65,10 @@ const els = {
|
||||
};
|
||||
|
||||
// State for the currently displayed dashboard and its last loaded data.
|
||||
// Only the dataset of the active dashboard is held — nothing is prefetched.
|
||||
let currentView = "amazon-abgangsland";
|
||||
let lastData = null; // { abgangsland, vat }
|
||||
let lastData = null; // { view, data } — data is the normalized dataset of `view`
|
||||
let loadToken = 0; // guards against stale responses from superseded requests
|
||||
|
||||
/* ---------- Auth (HTTP Basic) ---------- */
|
||||
|
||||
@@ -121,13 +155,17 @@ function buildPeriodSelectors() {
|
||||
}
|
||||
|
||||
els.month.value = String(defMonth);
|
||||
els.quarter.value = String(Math.floor(defMonth / 3) + 1);
|
||||
els.year.value = String(defYear);
|
||||
}
|
||||
|
||||
function selectedPeriod() {
|
||||
const meta = VIEWS[currentView];
|
||||
if (meta && meta.quarter) {
|
||||
return `${els.year.value}-Q${els.quarter.value}`;
|
||||
}
|
||||
const month = MONTHS[Number(els.month.value)];
|
||||
const year = els.year.value;
|
||||
return `${year}-${month}`;
|
||||
return `${els.year.value}-${month}`;
|
||||
}
|
||||
|
||||
/* ---------- Formatting ---------- */
|
||||
@@ -160,6 +198,9 @@ function formatQuantity(value) {
|
||||
/* ---------- Data ---------- */
|
||||
|
||||
function extractArray(payload, key) {
|
||||
// The webhook may return the requested dataset either directly as an array
|
||||
// or wrapped under its query key.
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
if (Array.isArray(payload[key])) return payload[key];
|
||||
const match = Object.keys(payload).find((k) => k.toLowerCase() === key.toLowerCase());
|
||||
@@ -217,6 +258,152 @@ function normalizeVat(rows) {
|
||||
return { countries, totals };
|
||||
}
|
||||
|
||||
// Amounts may arrive either as plain numbers (SQL numeric) or as German
|
||||
// formatted strings ("90.717,93 €") — accept both.
|
||||
function parseAmount(value) {
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
||||
return parseEuro(value);
|
||||
}
|
||||
|
||||
function computeRechnungenTotals(invoices) {
|
||||
return invoices.reduce(
|
||||
(acc, i) => {
|
||||
acc.gesamtBrutto += i.gesamtBrutto;
|
||||
acc.gesamtNetto += i.gesamtNetto;
|
||||
acc.brutto += i.brutto;
|
||||
acc.bruttoErm += i.bruttoErm;
|
||||
acc.netto += i.netto;
|
||||
acc.nettoErm += i.nettoErm;
|
||||
acc.versandBrutto += i.versandBrutto;
|
||||
acc.versandNetto += i.versandNetto;
|
||||
return acc;
|
||||
},
|
||||
{ gesamtBrutto: 0, gesamtNetto: 0, brutto: 0, bruttoErm: 0, netto: 0, nettoErm: 0, versandBrutto: 0, versandNetto: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRechnungen(rows) {
|
||||
const invoices = rows.map((row) => ({
|
||||
datum: row["Rechnungsdatum"] ?? "",
|
||||
nummer: row["Rechnungsnummer"] ?? "",
|
||||
land: row["Rechnungsland"] ?? "",
|
||||
landCode: row["Rechnungsland Ländercode"] ?? "",
|
||||
zahlart: row["Zahlart"] ?? "",
|
||||
zahlungsreferenz: row["Zahlungsreference der Bestellung"] ?? "",
|
||||
vatId: row["USt. Id / VAT Id des Käufers (in Bestellung)"] ?? "",
|
||||
gesamtBrutto: parseAmount(row["Gesamtpreis brutto"]),
|
||||
gesamtNetto: parseAmount(row["Gesamtpreis netto"]),
|
||||
brutto: parseAmount(row["Brutto"]),
|
||||
bruttoErm: parseAmount(row["Brutto erm"]),
|
||||
ustSatz: parseAmount(row["USt. Satz"]),
|
||||
ustSatzErm: parseAmount(row["Ust. Satz erm"]),
|
||||
netto: parseAmount(row["Netto Betrag"]),
|
||||
nettoErm: parseAmount(row["Netto Betrag erm"]),
|
||||
versandBrutto: parseAmount(row["Versandkosten brutto"]),
|
||||
versandNetto: parseAmount(row["Versandkosten netto"]),
|
||||
auftragsdatum: row["Auftragsdatum"] ?? "",
|
||||
bestellnummer: row["Bestellnummer"] ?? "",
|
||||
zahldatum: row["Zahldatum"] ?? "",
|
||||
lieferName: row["Lieferadresse Name"] ?? "",
|
||||
tag: row["Tag"] ?? "",
|
||||
lieferland: row["Lieferland"] ?? "",
|
||||
quelle: row["Quelle"] ?? "",
|
||||
}));
|
||||
|
||||
return { invoices, totals: computeRechnungenTotals(invoices) };
|
||||
}
|
||||
|
||||
/* ---------- ODOO Rechnungen pro Monat: filters ---------- */
|
||||
|
||||
let rechnungenFilter = { tag: "", bestellnummer: "", lieferland: "", nummer: "", quelle: "" };
|
||||
|
||||
function matchesFilter(value, query) {
|
||||
if (!query) return true;
|
||||
return String(value).toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
function applyRechnungenFilters(data) {
|
||||
const f = rechnungenFilter;
|
||||
const invoices = data.invoices.filter((i) =>
|
||||
matchesFilter(i.tag, f.tag) &&
|
||||
matchesFilter(i.bestellnummer, f.bestellnummer) &&
|
||||
matchesFilter(i.lieferland, f.lieferland) &&
|
||||
matchesFilter(i.nummer, f.nummer) &&
|
||||
matchesFilter(i.quelle, f.quelle)
|
||||
);
|
||||
return { invoices, totals: computeRechnungenTotals(invoices) };
|
||||
}
|
||||
|
||||
function rechnungenFilterInputs() {
|
||||
if (currentView === "gesamt-rechnungen-pivot") {
|
||||
return [els.pivotFilterTag, els.pivotFilterBestellnummer, els.pivotFilterLieferland, els.pivotFilterNummer, els.pivotFilterQuelle];
|
||||
}
|
||||
if (currentView === "schweiz-quartal") {
|
||||
return [els.chFilterTag, els.chFilterBestellnummer, els.chFilterLieferland, els.chFilterNummer, els.chFilterQuelle];
|
||||
}
|
||||
if (currentView === "odoo-rechnungen") {
|
||||
return [els.odooFilterTag, els.odooFilterBestellnummer, els.odooFilterLieferland, els.odooFilterNummer];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Every filter input across the invoice dashboards. Attached once at startup,
|
||||
// because the views are only toggled — the inputs stay in the DOM.
|
||||
function allRechnungenFilterInputs() {
|
||||
return [
|
||||
els.odooFilterTag, els.odooFilterBestellnummer, els.odooFilterLieferland, els.odooFilterNummer,
|
||||
els.pivotFilterTag, els.pivotFilterBestellnummer, els.pivotFilterLieferland, els.pivotFilterNummer, els.pivotFilterQuelle,
|
||||
els.chFilterTag, els.chFilterBestellnummer, els.chFilterLieferland, els.chFilterNummer, els.chFilterQuelle,
|
||||
];
|
||||
}
|
||||
|
||||
function resetRechnungenFilters() {
|
||||
rechnungenFilter = { tag: "", bestellnummer: "", lieferland: "", nummer: "", quelle: "" };
|
||||
rechnungenFilterInputs().forEach((input) => { if (input) input.value = ""; });
|
||||
}
|
||||
|
||||
function onRechnungenFilter() {
|
||||
const [tag, bestellnummer, lieferland, nummer, quelle] = rechnungenFilterInputs();
|
||||
rechnungenFilter = {
|
||||
tag: (tag?.value || "").trim(),
|
||||
bestellnummer: (bestellnummer?.value || "").trim(),
|
||||
lieferland: (lieferland?.value || "").trim(),
|
||||
nummer: (nummer?.value || "").trim(),
|
||||
quelle: (quelle?.value || "").trim(),
|
||||
};
|
||||
if (lastData && lastData.view === currentView) {
|
||||
renderActiveView();
|
||||
}
|
||||
}
|
||||
|
||||
function setupRechnungenFilters() {
|
||||
allRechnungenFilterInputs().forEach((input) => { if (input) input.addEventListener("input", onRechnungenFilter); });
|
||||
}
|
||||
|
||||
function setupRechnungenPivot() {
|
||||
wirePivotPreset(els.odooPivotPreset, "odoo-rechnungen");
|
||||
wirePivotPreset(els.pivotPreset, "gesamt-rechnungen-pivot");
|
||||
wirePivotPreset(els.chPreset, "schweiz-quartal");
|
||||
}
|
||||
|
||||
function wirePivotPreset(select, view) {
|
||||
if (!select) return;
|
||||
select.addEventListener("change", () => {
|
||||
const presets = PIVOT_PRESETS[view];
|
||||
const preset = presets[select.value] || presets.standard;
|
||||
pivotOptions[view] = {
|
||||
rows: [...preset.rows],
|
||||
cols: [...preset.cols],
|
||||
vals: [...preset.vals],
|
||||
aggregatorName: preset.aggregatorName,
|
||||
rendererName: preset.rendererName,
|
||||
};
|
||||
// A preset also loads the default (empty) filters.
|
||||
resetRechnungenFilters();
|
||||
renderActiveView();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Rendering: Umsatz je Abgangsland ---------- */
|
||||
|
||||
function colorFor(bereich, map) {
|
||||
@@ -428,6 +615,171 @@ function renderVat(data) {
|
||||
renderVatTable(data);
|
||||
}
|
||||
|
||||
/* ---------- Rendering: ODOO Rechnungen pro Monat ---------- */
|
||||
|
||||
function renderRechnungenKpis(data) {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
const { invoices, totals } = data;
|
||||
const ust = totals.gesamtBrutto - totals.gesamtNetto;
|
||||
|
||||
v.kpis.innerHTML = `
|
||||
<div class="kpi primary">
|
||||
<div class="kpi-label">Gesamt brutto</div>
|
||||
<div class="kpi-value">${formatEuro(totals.gesamtBrutto)}</div>
|
||||
<div class="kpi-sub">alle Rechnungen im Zeitraum</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Rechnungen</div>
|
||||
<div class="kpi-value">${invoices.length}</div>
|
||||
<div class="kpi-sub">Datensätze</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Gesamt netto</div>
|
||||
<div class="kpi-value">${formatEuro(totals.gesamtNetto)}</div>
|
||||
<div class="kpi-sub">ohne USt</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">USt (brutto − netto)</div>
|
||||
<div class="kpi-value">${formatEuro(ust)}</div>
|
||||
<div class="kpi-sub">enthaltene Umsatzsteuer</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderRechnungenChart(data) {
|
||||
const v = viewEls();
|
||||
if (!v || !v.chart) return;
|
||||
|
||||
const byCountry = new Map();
|
||||
data.invoices.forEach((inv) => {
|
||||
const key = inv.landCode || inv.land || "–";
|
||||
byCountry.set(key, (byCountry.get(key) || 0) + inv.gesamtBrutto);
|
||||
});
|
||||
|
||||
const entries = [...byCountry.entries()].sort((a, b) => b[1] - a[1]);
|
||||
const grand = entries.reduce((sum, [, value]) => sum + value, 0);
|
||||
const max = entries.reduce((m, [, value]) => Math.max(m, value), 0) || 1;
|
||||
|
||||
v.chart.innerHTML = "";
|
||||
v.legend.innerHTML = "";
|
||||
|
||||
if (!entries.length) {
|
||||
v.chart.innerHTML = '<p class="muted">Keine Daten für diesen Zeitraum.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach(([code, value]) => {
|
||||
const pct = (value / max) * 100;
|
||||
const share = grand ? (value / grand) * 100 : 0;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "bar-row";
|
||||
row.innerHTML = `
|
||||
<div class="bar-country">${code}</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill" style="background:var(--accent)"></div>
|
||||
</div>
|
||||
<div class="bar-value">${formatEuro(value)} · ${formatPercent(share)}</div>`;
|
||||
v.chart.appendChild(row);
|
||||
|
||||
const fill = row.querySelector(".bar-fill");
|
||||
fill.getBoundingClientRect();
|
||||
fill.style.width = Math.max(pct, 6) + "%";
|
||||
fill.classList.add("ready");
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Rechnungen pivot (experiment) ---------- */
|
||||
|
||||
// Preset pivot layouts per view, selectable from each view's "Vorlage" dropdown.
|
||||
const PIVOT_PRESETS = {
|
||||
"gesamt-rechnungen-pivot": {
|
||||
standard: { rows: ["Quelle"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
rechnungsland: { rows: ["Quelle"], cols: ["Rechnungsland"], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
"odoo-rechnungen": {
|
||||
standard: { rows: ["Rechnungsland"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
zahlart: { rows: ["Zahlart"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
"schweiz-quartal": {
|
||||
standard: { rows: ["Quelle"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
rechnungsland: { rows: ["Quelle"], cols: ["Rechnungsland"], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
};
|
||||
|
||||
// Current pivot layout per view (kept so a filter re-render doesn't reset it).
|
||||
const pivotOptions = {
|
||||
"gesamt-rechnungen-pivot": { ...PIVOT_PRESETS["gesamt-rechnungen-pivot"].standard },
|
||||
"odoo-rechnungen": { ...PIVOT_PRESETS["odoo-rechnungen"].standard },
|
||||
"schweiz-quartal": { ...PIVOT_PRESETS["schweiz-quartal"].standard },
|
||||
};
|
||||
|
||||
function renderRechnungenPivot(data) {
|
||||
const v = viewEls();
|
||||
if (!v || !v.pivot) return;
|
||||
|
||||
renderRechnungenKpis(data);
|
||||
if (v.chart) renderRechnungenChart(data);
|
||||
|
||||
// Recreate the container so pivotUI cannot reuse state from a previous
|
||||
// render — clearing innerHTML alone is not enough, so a preset change would
|
||||
// not take effect.
|
||||
const prev = v.pivot;
|
||||
const el = prev.cloneNode(false);
|
||||
prev.parentNode.replaceChild(el, prev);
|
||||
|
||||
if (!window.jQuery || !jQuery.fn.pivotUI) {
|
||||
el.innerHTML = '<p class="muted">PivotTable.js ist nicht geladen (offline?).</p>';
|
||||
return;
|
||||
}
|
||||
if (!data.invoices.length) {
|
||||
el.innerHTML = '<p class="muted">Keine Daten für diesen Zeitraum.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Flaches Datenmodell für die Pivot-Tabelle. Alle Attribute sind im
|
||||
// pivotUI-Bedienfeld frei kombinierbar (Zeilen/Spalten/Werte/Aggregator).
|
||||
// Hinweis: "Brutto" ist nur der Normalsatz-Anteil; "Gesamtpreis brutto" ist
|
||||
// Brutto + Brutto erm (= die Summe, die die KPI "Gesamt brutto" anzeigt).
|
||||
const rows = data.invoices.map((i) => ({
|
||||
Quelle: i.quelle || "(leer)",
|
||||
Rechnungsland: i.land || "(leer)",
|
||||
Zahlart: i.zahlart || "(leer)",
|
||||
Bestellnummer: i.bestellnummer || "(leer)",
|
||||
Tag: i.tag || "(leer)",
|
||||
Lieferland: i.lieferland || "(leer)",
|
||||
"Gesamtpreis brutto": i.gesamtBrutto,
|
||||
Brutto: i.brutto,
|
||||
"Brutto erm": i.bruttoErm,
|
||||
"Gesamtpreis netto": i.gesamtNetto,
|
||||
Netto: i.netto,
|
||||
"Netto erm": i.nettoErm,
|
||||
"Versandkosten brutto": i.versandBrutto,
|
||||
"Versandkosten netto": i.versandNetto,
|
||||
}));
|
||||
|
||||
const opts = {
|
||||
...pivotOptions[currentView],
|
||||
unusedAttrsVertical: true,
|
||||
onRefresh: (config) => {
|
||||
pivotOptions[currentView] = {
|
||||
rows: config.rows,
|
||||
cols: config.cols,
|
||||
vals: config.vals,
|
||||
aggregatorName: config.aggregatorName,
|
||||
rendererName: config.rendererName,
|
||||
};
|
||||
},
|
||||
};
|
||||
// Only offer the plain table renderer (no heatmaps/charts).
|
||||
const tableRenderer = jQuery.pivotUtilities && jQuery.pivotUtilities.renderers
|
||||
? jQuery.pivotUtilities.renderers["Table"]
|
||||
: null;
|
||||
if (tableRenderer) opts.renderers = { Table: tableRenderer };
|
||||
|
||||
jQuery(el).pivotUI(rows, opts);
|
||||
}
|
||||
|
||||
/* ---------- Active view helpers ---------- */
|
||||
|
||||
function viewEls() {
|
||||
@@ -437,15 +789,26 @@ function viewEls() {
|
||||
if (currentView === "amazon-vat-summary") {
|
||||
return { kpis: els.vatKpis, chart: els.vatChart, legend: els.vatLegend, table: els.vatTable, state: els.vatState };
|
||||
}
|
||||
if (currentView === "odoo-rechnungen") {
|
||||
return { kpis: els.odooKpis, chart: els.odooChart, legend: els.odooLegend, state: els.odooState, pivot: document.getElementById("odoo-pivot") };
|
||||
}
|
||||
if (currentView === "gesamt-rechnungen-pivot") {
|
||||
return { kpis: els.pivotKpis, state: els.pivotState, pivot: document.getElementById("pivot") };
|
||||
}
|
||||
if (currentView === "schweiz-quartal") {
|
||||
return { kpis: els.chKpis, state: els.chState, pivot: document.getElementById("ch-pivot") };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderActiveView() {
|
||||
if (!lastData) return;
|
||||
if (!lastData || lastData.view !== currentView) return;
|
||||
if (currentView === "amazon-abgangsland") {
|
||||
renderAbgangsland(lastData.abgangsland);
|
||||
renderAbgangsland(lastData.data);
|
||||
} else if (currentView === "amazon-vat-summary") {
|
||||
renderVat(lastData.vat);
|
||||
renderVat(lastData.data);
|
||||
} else if (currentView === "odoo-rechnungen" || currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal") {
|
||||
renderRechnungenPivot(applyRechnungenFilters(lastData.data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,9 +829,10 @@ function showSkeleton() {
|
||||
if (!v) return;
|
||||
v.state.classList.add("hidden");
|
||||
v.kpis.innerHTML = skeletonKpis();
|
||||
v.chart.innerHTML = skeletonBars();
|
||||
v.legend.innerHTML = "";
|
||||
v.table.innerHTML = "";
|
||||
if (v.chart) v.chart.innerHTML = skeletonBars();
|
||||
if (v.legend) v.legend.innerHTML = "";
|
||||
if (v.table) v.table.innerHTML = "";
|
||||
if (v.pivot) v.pivot.innerHTML = "";
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
@@ -482,24 +846,52 @@ function clearViews() {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
v.kpis.innerHTML = "";
|
||||
v.chart.innerHTML = "";
|
||||
v.legend.innerHTML = "";
|
||||
v.table.innerHTML = "";
|
||||
if (v.chart) v.chart.innerHTML = "";
|
||||
if (v.legend) v.legend.innerHTML = "";
|
||||
if (v.table) v.table.innerHTML = "";
|
||||
if (v.pivot) v.pivot.innerHTML = "";
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const meta = VIEWS[currentView];
|
||||
const query = meta && meta.query;
|
||||
|
||||
// The datasets are huge: only the dataset of the displayed dashboard is
|
||||
// requested, and only when that dashboard is shown or reloaded.
|
||||
if (!query) {
|
||||
lastData = null;
|
||||
clearViews();
|
||||
updateDownloadState();
|
||||
return;
|
||||
}
|
||||
|
||||
resetRechnungenFilters();
|
||||
|
||||
const period = selectedPeriod();
|
||||
els.tablePeriod.textContent = period;
|
||||
els.vatPeriod.textContent = period;
|
||||
els.odooPeriod.textContent = period;
|
||||
els.pivotPeriod.textContent = period;
|
||||
els.chPeriod.textContent = period;
|
||||
els.refresh.classList.add("spinning");
|
||||
showSkeleton();
|
||||
|
||||
const token = ++loadToken;
|
||||
|
||||
try {
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = { Accept: "application/json", "Content-Type": "application/json" };
|
||||
const authorization = authHeader();
|
||||
if (authorization) headers.Authorization = authorization;
|
||||
|
||||
const response = await fetch(`${API_URL}?month=${encodeURIComponent(period)}`, { headers });
|
||||
const body = meta.quarter ? { query, quarter: period } : { query, month: period };
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// A newer request (view/period switch) superseded this one — ignore it.
|
||||
if (token !== loadToken) return;
|
||||
|
||||
// The webhook is protected with Basic Auth: ask for credentials when needed.
|
||||
if (response.status === 401) {
|
||||
@@ -515,10 +907,7 @@ async function load() {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const payload = await response.json();
|
||||
lastData = {
|
||||
abgangsland: normalizeAbgangsland(extractArray(payload, "Summe je Abgangsland")),
|
||||
vat: normalizeVat(extractArray(payload, "VAT Summary")),
|
||||
};
|
||||
lastData = { view: currentView, data: meta.normalize(extractArray(payload, query)) };
|
||||
|
||||
renderActiveView();
|
||||
|
||||
@@ -528,6 +917,7 @@ async function load() {
|
||||
v.state.textContent = "";
|
||||
}
|
||||
} catch (error) {
|
||||
if (token !== loadToken) return;
|
||||
lastData = null;
|
||||
clearViews();
|
||||
showError(`Daten konnten nicht geladen werden: ${error.message}`);
|
||||
@@ -578,19 +968,63 @@ function buildVatCsv(data) {
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function buildRechnungenCsv(data) {
|
||||
const showQuelle = currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal";
|
||||
const header = [
|
||||
"Rechnungsdatum", "Rechnungsnummer", "Rechnungsland", "Rechnungsland Ländercode",
|
||||
"Zahlart", "Zahlungsreference der Bestellung",
|
||||
"USt. Id / VAT Id des Käufers (in Bestellung)", "Gesamtpreis brutto", "Gesamtpreis netto",
|
||||
"Brutto", "Brutto erm", "USt. Satz", "Ust. Satz erm", "Netto Betrag", "Netto Betrag erm",
|
||||
"Versandkosten brutto", "Versandkosten netto", "Auftragsdatum", "Bestellnummer", "Zahldatum",
|
||||
"Lieferadresse Name", "Tag", "Lieferland",
|
||||
];
|
||||
if (showQuelle) header.push("Quelle");
|
||||
const lines = [header.map(csvEscape).join(";")];
|
||||
|
||||
data.invoices.forEach((i) => {
|
||||
const cols = [
|
||||
i.datum, i.nummer, i.land, i.landCode, i.zahlart, i.zahlungsreferenz, i.vatId,
|
||||
formatEuro(i.gesamtBrutto), formatEuro(i.gesamtNetto),
|
||||
formatEuro(i.brutto), formatEuro(i.bruttoErm), formatQuantity(i.ustSatz), formatQuantity(i.ustSatzErm),
|
||||
formatEuro(i.netto), formatEuro(i.nettoErm),
|
||||
formatEuro(i.versandBrutto), formatEuro(i.versandNetto),
|
||||
i.auftragsdatum, i.bestellnummer, i.zahldatum, i.lieferName, i.tag, i.lieferland,
|
||||
];
|
||||
if (showQuelle) cols.push(i.quelle);
|
||||
lines.push(cols.map(csvEscape).join(";"));
|
||||
});
|
||||
|
||||
const t = data.totals;
|
||||
const totalCols = [
|
||||
"Gesamt", String(data.invoices.length), "", "", "", "", "",
|
||||
formatEuro(t.gesamtBrutto), formatEuro(t.gesamtNetto),
|
||||
formatEuro(t.brutto), formatEuro(t.bruttoErm), "", "",
|
||||
formatEuro(t.netto), formatEuro(t.nettoErm),
|
||||
formatEuro(t.versandBrutto), formatEuro(t.versandNetto),
|
||||
"", "", "", "", "", "",
|
||||
];
|
||||
if (showQuelle) totalCols.push("");
|
||||
lines.push(totalCols.map(csvEscape).join(";"));
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function downloadCsv() {
|
||||
if (!lastData) return;
|
||||
if (!lastData || lastData.view !== currentView) return;
|
||||
|
||||
const period = selectedPeriod();
|
||||
let csv;
|
||||
let filename;
|
||||
|
||||
if (currentView === "amazon-abgangsland") {
|
||||
csv = buildAbgangslandCsv(lastData.abgangsland);
|
||||
csv = buildAbgangslandCsv(lastData.data);
|
||||
filename = `umsatz-je-abgangsland_${period}.csv`;
|
||||
} else if (currentView === "amazon-vat-summary") {
|
||||
csv = buildVatCsv(lastData.vat);
|
||||
csv = buildVatCsv(lastData.data);
|
||||
filename = `vat-summary_${period}.csv`;
|
||||
} else if (currentView === "odoo-rechnungen" || currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal") {
|
||||
csv = buildRechnungenCsv(applyRechnungenFilters(lastData.data));
|
||||
filename = `${currentView}_${period}.csv`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -609,8 +1043,13 @@ function downloadCsv() {
|
||||
}
|
||||
|
||||
function updateDownloadState() {
|
||||
const supported = currentView === "amazon-abgangsland" || currentView === "amazon-vat-summary";
|
||||
els.download.disabled = !(lastData && supported);
|
||||
const supported =
|
||||
currentView === "amazon-abgangsland" ||
|
||||
currentView === "amazon-vat-summary" ||
|
||||
currentView === "odoo-rechnungen" ||
|
||||
currentView === "gesamt-rechnungen-pivot" ||
|
||||
currentView === "schweiz-quartal";
|
||||
els.download.disabled = !(lastData && lastData.view === currentView && supported);
|
||||
}
|
||||
|
||||
/* ---------- Navigation ---------- */
|
||||
@@ -619,14 +1058,34 @@ const VIEWS = {
|
||||
"amazon-abgangsland": {
|
||||
title: "Umsatz je Abgangsland",
|
||||
subtitle: "Amazon · Brutto (inkl. USt)",
|
||||
// Webhook query key and row normalizer for this dashboard's dataset.
|
||||
query: "Summe je Abgangsland",
|
||||
normalize: normalizeAbgangsland,
|
||||
},
|
||||
"amazon-vat-summary": {
|
||||
title: "VAT Summary",
|
||||
subtitle: "Amazon · USt-Übersicht je Land",
|
||||
query: "VAT Summary",
|
||||
normalize: normalizeVat,
|
||||
},
|
||||
"odoo-dashboard": {
|
||||
title: "ODOO Dashboard",
|
||||
subtitle: "ODOO",
|
||||
"odoo-rechnungen": {
|
||||
title: "ODOO Rechnungen pro Monat",
|
||||
subtitle: "ODOO · Rechnungen je Monat",
|
||||
query: "ODOO Rechnungen pro Monat",
|
||||
normalize: normalizeRechnungen,
|
||||
},
|
||||
"gesamt-rechnungen-pivot": {
|
||||
title: "Alle Rechnungen pro Monat",
|
||||
subtitle: "Gesamt · Pivot",
|
||||
query: "Alle Rechnungen pro Monat",
|
||||
normalize: normalizeRechnungen,
|
||||
},
|
||||
"schweiz-quartal": {
|
||||
title: "Quartal Schweiz",
|
||||
subtitle: "Schweiz · AMAZON + ODOO",
|
||||
query: "Quartal Schweiz",
|
||||
normalize: normalizeRechnungen,
|
||||
quarter: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -664,7 +1123,12 @@ function showView(name) {
|
||||
document.getElementById("view-subtitle").textContent = meta.subtitle;
|
||||
|
||||
currentView = name;
|
||||
renderActiveView();
|
||||
// Quarter views use the quarter selector instead of the month selector.
|
||||
const isQuarter = !!meta.quarter;
|
||||
els.month.classList.toggle("hidden", isQuarter);
|
||||
els.quarter.classList.toggle("hidden", !isQuarter);
|
||||
// Query only this dashboard's dataset — nothing is prefetched.
|
||||
load();
|
||||
updateDownloadState();
|
||||
}
|
||||
|
||||
@@ -692,7 +1156,11 @@ function setupAuth() {
|
||||
buildPeriodSelectors();
|
||||
setupMenu();
|
||||
setupAuth();
|
||||
setupRechnungenFilters();
|
||||
setupRechnungenPivot();
|
||||
load();
|
||||
els.month.addEventListener("change", load);
|
||||
els.quarter.addEventListener("change", load);
|
||||
els.year.addEventListener("change", load);
|
||||
els.refresh.addEventListener("click", load);
|
||||
els.download.addEventListener("click", downloadCsv);
|
||||
|
||||
Reference in new issue
Block a user