This commit is contained in:
oliver committed 2026-10-12 10:09:44 -03:00
1 parent 1ea3de74f2
commit 9db080faea
8 files changed
+2064 -55

No files matched your search

+495 -27
View File
@@ -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);