Modulare Startseite: Kartenraster, eigene Anordnung je Benutzer, Auswertungen
Die Uebersicht war fest verdrahtet und fuer alle gleich. Jetzt besteht sie aus Karten in einem 12-Spalten-Raster, die sich ziehen, in der Groesse aendern und einrasten lassen (react-grid-layout). "Anordnen" schaltet das frei, gespeichert wird ausdruecklich - Probieren bleibt folgenlos. Anordnung: - Neue Tabelle dashboard_layouts; user_id = NULL ist die Admin-Vorgabe. - Jeder Benutzer hat seine eigene Anordnung. Admins speichern eine Vorgabe fuer alle und koennen sie ueber die Einstellung dashboard_enforced verbindlich machen; dann lehnt das Backend das Speichern eigener Anordnungen ab. - "Auf Vorgabe zuruecksetzen" verwirft die persoenliche Anordnung. 15 Karten (Verzeichnis web/src/dashboard/cards.jsx, neue Karte = ein Eintrag): Schnellzugriff, Status, Artikelzahl, Artikeleinheiten, Bald ablaufend, Abgelaufen, Einkaufsliste, letzte Bewegungen sowie sechs Auswertungen (Ablauf-Ring, Kategorien-Ring, Kategorien nach Zustand, Bestandsverlauf, Verlauf eines Artikels, Ein-/Auslagerungen). Diagramme als eigene SVG-Komponenten statt Diagramm-Paket: - Zustandsfarben (ok/bald/abgelaufen) sind Statusangaben aus den Tokens und stehen nie ohne Beschriftung; Kategorien nutzen eine gepruefte Farbreihe in fester Ordnung, ab sieben Kategorien wird zu "Andere" gebuendelt. - Eine Werteachse, duenne Marken, zurueckhaltendes Gitter, Fadenkreuz mit Kurzinfo. Hell und Dunkel haben eigene Farbstufen. Mengen durchgehend in Artikeleinheiten (Glaeser, Packungen, Stueck): Gramm und Stueck lassen sich nicht addieren, Gebinde schon. Der bisher dreifach vorhandene Helfer liegt jetzt einmal in services/conversion.py::article_unit. Der Verlauf wird rueckwaerts vom heutigen Bestand aus den Bewegungen rekonstruiert. Bekannte Ungenauigkeit, im Code und in der Roadmap vermerkt: Die Umrechnung nutzt die heutige Packungsgroesse. Geprueft: "npm run build" laeuft durch. Die neuen pytest-Tests (backend/tests/test_dashboard.py) konnten hier nicht laufen - auf diesem Rechner ist kein Python installiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
94
web/src/dashboard/palette.js
Normal file
94
web/src/dashboard/palette.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Farben für die Diagramme.
|
||||
*
|
||||
* Zwei getrennte Aufgaben, zwei getrennte Paletten – das ist keine Kosmetik:
|
||||
*
|
||||
* 1. ZUSTAND (ok / bald ablaufend / abgelaufen) ist eine *Statusangabe*. Dafür
|
||||
* gelten die Tokens der Anwendung (--ok, --warn, --danger). Statusfarben
|
||||
* werden nie als Serienfarben zweckentfremdet und stehen nie allein: In der
|
||||
* Legende steht immer auch der Text.
|
||||
*
|
||||
* 2. KATEGORIEN sind *Identität*. Dafür eine feste Reihenfolge geprüfter Farben.
|
||||
* Die Reihenfolge ist Teil der Sicherheit gegen Farbfehlsichtigkeit – sie
|
||||
* wird nicht umsortiert und nicht im Kreis wiederholt. Über sechs Kategorien
|
||||
* hinaus wird zu „Andere“ gebündelt, statt weitere Farben zu erfinden.
|
||||
*
|
||||
* Dunkel ist kein Umdrehen der Hellwerte, sondern eigene Stufen für den dunklen
|
||||
* Untergrund.
|
||||
*/
|
||||
|
||||
// Feste Reihenfolge. Höchstens SERIES_MAX Einträge werden eingefärbt.
|
||||
export const CATEGORY_COLORS_LIGHT = [
|
||||
"#2a78d6", // blau
|
||||
"#eb6834", // orange
|
||||
"#1baf7a", // aqua
|
||||
"#eda100", // gelb
|
||||
"#e87ba4", // magenta
|
||||
"#008300", // grün
|
||||
];
|
||||
export const CATEGORY_COLORS_DARK = [
|
||||
"#3987e5",
|
||||
"#d95926",
|
||||
"#199e70",
|
||||
"#c98500",
|
||||
"#d55181",
|
||||
"#008300",
|
||||
];
|
||||
|
||||
/** Ab hier wird gebündelt – „Andere“ bekommt einen neutralen Grauton. */
|
||||
export const SERIES_MAX = 6;
|
||||
export const OTHER_COLOR_LIGHT = "#8a8f98";
|
||||
export const OTHER_COLOR_DARK = "#6f7783";
|
||||
|
||||
/** Erkennt das aktive Farbschema (Systemeinstellung + eigener Umschalter). */
|
||||
export function istDunkel() {
|
||||
if (typeof document === "undefined") return false;
|
||||
const gesetzt = document.documentElement.getAttribute("data-theme");
|
||||
if (gesetzt === "dark") return true;
|
||||
if (gesetzt === "light") return false;
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
||||
}
|
||||
|
||||
export function kategorieFarben(dunkel) {
|
||||
return dunkel ? CATEGORY_COLORS_DARK : CATEGORY_COLORS_LIGHT;
|
||||
}
|
||||
|
||||
export function andereFarbe(dunkel) {
|
||||
return dunkel ? OTHER_COLOR_DARK : OTHER_COLOR_LIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bündelt eine sortierte Liste auf SERIES_MAX Einträge plus „Andere“ und
|
||||
* vergibt die Farben in fester Reihenfolge.
|
||||
* Erwartet Objekte mit { name, value }.
|
||||
*/
|
||||
export function mitFarben(eintraege, dunkel) {
|
||||
const farben = kategorieFarben(dunkel);
|
||||
const sortiert = [...eintraege].sort((a, b) => b.value - a.value);
|
||||
const vorne = sortiert.slice(0, SERIES_MAX).map((e, i) => ({ ...e, color: farben[i] }));
|
||||
const rest = sortiert.slice(SERIES_MAX);
|
||||
if (rest.length === 0) return vorne;
|
||||
return [
|
||||
...vorne,
|
||||
{
|
||||
name: `Andere (${rest.length})`,
|
||||
value: rest.reduce((s, e) => s + e.value, 0),
|
||||
color: andereFarbe(dunkel),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Zustandsfarben kommen aus den CSS-Tokens, damit Hell/Dunkel automatisch passt. */
|
||||
export const STATUS_SERIES = [
|
||||
{ key: "ok", name: "In Ordnung", cssVar: "--ok" },
|
||||
{ key: "soon", name: "Bald ablaufend", cssVar: "--warn" },
|
||||
{ key: "expired", name: "Abgelaufen", cssVar: "--danger" },
|
||||
{ key: "no_date", name: "Ohne MHD", cssVar: "--muted" },
|
||||
];
|
||||
|
||||
/** Liest einen CSS-Tokenwert aus – die Diagramme zeichnen in echtem SVG. */
|
||||
export function tokenFarbe(name, fallback = "#888") {
|
||||
if (typeof document === "undefined") return fallback;
|
||||
const wert = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return wert?.trim() || fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user