136 lines
5.8 KiB
Markdown
136 lines
5.8 KiB
Markdown
# BI Dashboard
|
||
|
||
A flat, server-free dashboard for the Brandize BI webhook
|
||
(`Summe je Abgangsland`). It shows the gross revenue (Brutto inkl. USt) per
|
||
departure country for a selected period, with KPI cards, a bar chart and a
|
||
detail table.
|
||
|
||
## Run
|
||
|
||
Just open `index.html` in a browser (double-click it). No server, no build step.
|
||
|
||
The page calls the webhook directly:
|
||
|
||
```
|
||
https://brandize.app.n8n.cloud/webhook/bi-dashboard
|
||
```
|
||
|
||
via a **POST** request with a JSON body:
|
||
|
||
```json
|
||
{ "query": "Summe je Abgangsland", "month": "2026-SEP" }
|
||
```
|
||
|
||
The webhook sends CORS headers (it reflects the request `Origin`, including
|
||
`null` for `file://`), so a direct browser call works.
|
||
|
||
## Navigation
|
||
|
||
The left sidebar groups reports by topic:
|
||
|
||
- **Amazon** → *Umsatz je Abgangsland* (Brutto inkl. USt je Abgangsland)
|
||
- **Amazon** → *VAT Summary* (USt-Übersicht je Land)
|
||
- **ODOO** → *Rechnungen pro Monat* (Pivot über Odoo-Rechnungen)
|
||
- **ODOO** → *FIFO Lager Bewertung* (Lagerwert FIFO)
|
||
- **ODOO** → *Produkt Materialkosten* (Materialkosten je Produkt)
|
||
- **Gesamt** → *Alle Rechnungen pro Monat* (PivotTable.js-Experiment)
|
||
- **Gesamt** → *Quartal Schweiz* (AMAZON + ODOO, Lieferung in die Schweiz)
|
||
|
||
All groups start collapsed, so no submenu is visible when the page opens.
|
||
Clicking a topic expands its submenu; clicking a submenu item switches the
|
||
view.
|
||
|
||
## Authentication
|
||
|
||
The webhooks are protected with HTTP Basic Auth. The dashboard asks for the
|
||
credentials when they are needed: the first request is sent without them, and
|
||
when the webhook answers `401`, a login prompt appears. The entered
|
||
credentials are sent as an `Authorization: Basic ...` header on every request
|
||
and kept in `sessionStorage` for the rest of the browser session (so a reload
|
||
does not prompt again). If the webhook rejects them (`401`), the prompt
|
||
reappears. Storage is best-effort: on `file://` it may be unavailable, in which
|
||
case credentials are kept in memory only.
|
||
|
||
## CSV export
|
||
|
||
The header has a **CSV** button that downloads the data of the currently
|
||
displayed dashboard, as a semicolon-delimited UTF-8 file (with BOM) including a
|
||
Gesamt row:
|
||
|
||
- *Umsatz je Abgangsland* → `umsatz-je-abgangsland_YYYY-MMM.csv`
|
||
- *VAT Summary* → `vat-summary_YYYY-MMM.csv`
|
||
- *Rechnungen pro Monat* → `odoo-rechnungen_YYYY-MMM.csv`
|
||
- *Alle Rechnungen pro Monat* → `gesamt-rechnungen-pivot_YYYY-MMM.csv`
|
||
- *Quartal Schweiz* → `schweiz-quartal_YYYY-QN.csv`
|
||
- *FIFO Lager Bewertung* → `fifo-lager_YYYY-MMM.csv`
|
||
- *Produkt Materialkosten* → `materialkosten_YYYY-MMM.csv`
|
||
|
||
The button is disabled while no dataset is loaded for the current dashboard.
|
||
|
||
## Filter
|
||
|
||
A shared filter bar sits above every dashboard (labelled **Filter** with a
|
||
funnel icon and the hint *gilt für alle Dashboards*). It holds the **Zeitraum**
|
||
month + year selector (e.g. `2026-SEP`) and the reload button, and stays visible
|
||
across all views. The previous month is preselected. The selected period is
|
||
sent to the webhook in the POST body as `month` (e.g. `2026-SEP`), matching the
|
||
`%(month)s` parameter used in `../sql/*.sql`.
|
||
|
||
The invoice dashboards (*ODOO Rechnungen pro Monat*, *Alle Rechnungen pro Monat*
|
||
and *Quartal Schweiz*) have an additional, dashboard-specific filter row. *ODOO
|
||
Rechnungen pro Monat* filters by **Tag**, **Bestellnummer**, **Lieferland** and
|
||
**Rechnungsnummer**; *Alle Rechnungen pro Monat* and *Quartal Schweiz* add a
|
||
**Quelle** field. Filtering re-renders the KPIs, chart and pivot live.
|
||
|
||
For *Quartal Schweiz* the shared **Zeitraum** control shows a **quarter**
|
||
selector (Q1–Q4) instead of the month selector; the year selector is shared.
|
||
|
||
|
||
## Pivot (Experiment)
|
||
|
||
The three invoice dashboards (*ODOO Rechnungen pro Monat*, *Alle Rechnungen pro
|
||
Monat* and *Quartal Schweiz*) render their data as an interactive
|
||
[PivotTable.js](https://pivottable.js.org/) pivot instead of a detail table.
|
||
The pivot's rows, columns, values and aggregator are freely configurable in the
|
||
pivotUI control panel; the renderer is fixed to *Table*. Each dashboard has its
|
||
own filter row and **Vorlage** dropdown with presets:
|
||
|
||
- *ODOO Rechnungen pro Monat*: *Standard* (**Rechnungsland** rows, no column
|
||
group, **Sum(Gesamtpreis brutto)**) and *Zahlart*.
|
||
- *Alle Rechnungen pro Monat* and *Quartal Schweiz*: *Standard* (**Quelle**
|
||
rows, no column group, **Sum(Gesamtpreis brutto)**) and *Rechnungsland
|
||
(Spalten)* (adds **Rechnungsland** as the column group).
|
||
|
||
*Quartal Schweiz* is filtered to invoices with a **delivery into Switzerland**.
|
||
|
||
Applying a preset also resets the filters. The chosen layout is kept while you
|
||
type in the filters.
|
||
|
||
It loads jQuery, jQuery UI and PivotTable.js from public CDNs, so — unlike the
|
||
rest of the app — it needs an **internet connection** to render; offline it shows
|
||
a short message instead. To vendor the libraries locally, download them next to
|
||
`index.html` and change the `<link>`/`<script>` URLs in the `<head>`.
|
||
|
||
## Data sources
|
||
|
||
Because the datasets are large, the dashboard requests **only the dataset of
|
||
the currently displayed dashboard** — one POST per dashboard, sent only when
|
||
that dashboard is opened or reloaded. Nothing is prefetched. The dataset is
|
||
selected with the `query` field:
|
||
|
||
- *Umsatz je Abgangsland* → `query: "Summe je Abgangsland"`
|
||
- *VAT Summary* → `query: "VAT Summary"`
|
||
- *Rechnungen pro Monat* → `query: "ODOO Rechnungen pro Monat"`
|
||
- *Alle Rechnungen pro Monat* → `query: "Alle Rechnungen pro Monat"`
|
||
- *Quartal Schweiz* → `query: "Quartal Schweiz"` (sends `quarter` instead of `month`)
|
||
- *FIFO Lager Bewertung* → `query: "FIFO Lager Bewertung"` (response is `[{ id, query, row_count, data: [...] }]`)
|
||
- *Produkt Materialkosten* → `query: "Produkt Materialkosten"` (one row per product; `remaining_qty` = remaining of that cost bracket, not the product total)
|
||
|
||
## Files
|
||
|
||
| File | Purpose |
|
||
| --- | --- |
|
||
| `index.html` | Dashboard markup |
|
||
| `styles.css` | Styling |
|
||
| `app.js` | Data loading, parsing and rendering |
|