From aa589143742d94899b85f9ab9bcdcc2e47ef5bec Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 23 Jul 2026 10:09:05 +0200 Subject: [PATCH] Kartengroessen korrigiert, Vorschau im Menue, Zeitraum-Auswahl bei Verlaeufen Zu kleine Karten (Konstruktionsfehler): Bei Schnellzugriff, Status und den Kennzahlen war die Mindesthoehe gleich der Standardhoehe - und beide zu klein. Bei 96 px Zeilenhoehe blieben nach Kartenkopf und Innenabstand nur 23 px fuer den Inhalt, eine Kennzahl braucht allein rund 45 px. Deshalb erschien schon in der Grundeinstellung ein Rollbalken, und kleiner ging es nicht. - Zeilenhoehe von 96 auf 40 px: die Hoehe laesst sich jetzt fein abstufen. - Jede Karte hat eine Mindesthoehe, in die ihr Inhalt nachweislich passt (Rechnung steht als Kommentar am Verzeichnis). - Gespeicherte Anordnungen aus der alten Fassung werden beim Laden auf die Mindestgroesse angehoben, statt zu flach zu bleiben. Vorschau im Menue "Karte hinzufuegen": Der Name allein verriet nicht, was eine Karte zeigt. Jede Karte hat jetzt eine kleine Skizze (Tabelle, Ring, Balken, Linie, Kennzahl) und eine Beschreibung. Bewusst gezeichnete Skizzen statt echter Vorschau - sonst muesste das Menue fuer jede Karte Daten laden. Zeitraum bei den Verlaufskarten: Bestandsverlauf, Artikelverlauf und Ein-/Auslagerungen haben eine Auswahl 7 Tage bis 1 Jahr. Ringdiagramme: Der Ring klebte oben in der Karte, darunter blieb viel Leerraum. Er fuellt jetzt die Karte und sitzt mittig. Geprueft: "npm run build" laeuft durch. Co-Authored-By: Claude Opus 4.8 --- backend/app/routers/dashboard.py | 16 ++-- web/src/dashboard/Vorschau.jsx | 116 ++++++++++++++++++++++ web/src/dashboard/cards.jsx | 160 ++++++++++++++++++++++--------- web/src/pages/Dashboard.jsx | 37 +++++-- web/src/styles.css | 32 ++++++- 5 files changed, 298 insertions(+), 63 deletions(-) create mode 100644 web/src/dashboard/Vorschau.jsx diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index da89069..e0c9fcd 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -49,14 +49,16 @@ router = APIRouter(prefix="/dashboard", tags=["dashboard"]) ENFORCED_KEY = "dashboard_enforced" # Eingebaute Anordnung – wird benutzt, solange weder eigene noch Vorgabe existiert. +# Höhen zählen Rasterzeilen à 40 px; die Oberfläche hebt zu flache Karten +# zusätzlich auf ihre Mindesthöhe an. BUILTIN_LAYOUT: list[dict] = [ - {"i": "actions", "x": 0, "y": 0, "w": 4, "h": 1}, - {"i": "status", "x": 4, "y": 0, "w": 8, "h": 1}, - {"i": "expiring", "x": 0, "y": 1, "w": 6, "h": 3}, - {"i": "shopping", "x": 6, "y": 1, "w": 6, "h": 3}, - {"i": "expiry-donut", "x": 0, "y": 4, "w": 4, "h": 3}, - {"i": "category-donut", "x": 4, "y": 4, "w": 4, "h": 3}, - {"i": "stock-timeline", "x": 8, "y": 4, "w": 4, "h": 3}, + {"i": "actions", "x": 0, "y": 0, "w": 4, "h": 3}, + {"i": "status", "x": 4, "y": 0, "w": 8, "h": 3}, + {"i": "expiring", "x": 0, "y": 3, "w": 6, "h": 6}, + {"i": "shopping", "x": 6, "y": 3, "w": 6, "h": 6}, + {"i": "expiry-donut", "x": 0, "y": 9, "w": 4, "h": 6}, + {"i": "category-donut", "x": 4, "y": 9, "w": 4, "h": 6}, + {"i": "stock-timeline", "x": 8, "y": 9, "w": 4, "h": 6}, ] diff --git a/web/src/dashboard/Vorschau.jsx b/web/src/dashboard/Vorschau.jsx new file mode 100644 index 0000000..900169d --- /dev/null +++ b/web/src/dashboard/Vorschau.jsx @@ -0,0 +1,116 @@ +/** + * Kleine Skizzen für das Menü „Karte hinzufügen“. + * + * Bewusst abstrakte Zeichnungen statt einer echten Vorschau: Eine Live-Vorschau + * müsste für jede Karte Daten laden, nur um ein Menü zu zeigen. Die Skizze sagt + * in einem Blick, ob es eine Tabelle, ein Ring oder eine Linie wird – genau das, + * was der Name allein nicht verrät. + */ +const BREITE = 72; +const HOEHE = 44; + +function Rahmen({ children }) { + return ( + + ); +} + +const SKIZZEN = { + aktion: ( + + + + + ), + + kennzahl: ( + + + + + ), + + kennzahlen: ( + + {[8, 28, 48].map((x, i) => ( + + + + + ))} + + ), + + tabelle: ( + + + {[18, 25, 32].map((y) => ( + + + + + ))} + + ), + + ring: ( + + {/* Kreisumfang bei r=11: ~69 – daraus die Abschnitte */} + + + + + + {[14, 21, 28].map((y, i) => ( + + ))} + + ), + + balken: ( + + {[12, 21, 30].map((y, i) => ( + + + + + + ))} + + ), + + linie: ( + + + + + ), + + linien: ( + + + + + + ), +}; + +export default function Vorschau({ art }) { + return SKIZZEN[art] ?? SKIZZEN.tabelle; +} diff --git a/web/src/dashboard/cards.jsx b/web/src/dashboard/cards.jsx index 029510d..9de426c 100644 --- a/web/src/dashboard/cards.jsx +++ b/web/src/dashboard/cards.jsx @@ -272,30 +272,56 @@ function KarteKategorienSaeulen() { ); } -function KarteBestandsverlauf({ tage = 90 }) { +/** Zeitraum-Auswahl für die Verlaufskarten. */ +const ZEITRAEUME = [ + { tage: 7, label: "7 Tage" }, + { tage: 30, label: "30 Tage" }, + { tage: 90, label: "90 Tage" }, + { tage: 180, label: "6 Monate" }, + { tage: 365, label: "1 Jahr" }, +]; + +function ZeitraumWahl({ wert, setzen }) { + return ( + + ); +} + +function KarteBestandsverlauf() { + const [tage, setTage] = useState(90); const { daten, fehler } = useDaten(() => api.dashboardTimeline(tage), [tage]); - if (fehler) return
{fehler}
; - if (!daten) return
Lädt…
; return ( - kurzesDatum(p.date))} - serien={[{ - name: "Artikeleinheiten", - color: tokenFarbe("--accent", "#3f51b5"), - werte: daten.map((p) => p.article_units), - }]} - einheit="Einheiten" - /> +
+ + {fehler ? ( +
{fehler}
+ ) : !daten ? ( +
Lädt…
+ ) : ( + kurzesDatum(p.date))} + serien={[{ + name: "Artikeleinheiten", + color: tokenFarbe("--accent", "#3f51b5"), + werte: daten.map((p) => p.article_units), + }]} + einheit="Einheiten" + /> + )} +
); } function KarteArtikelverlauf() { const [produkte, setProdukte] = useState([]); const [gewaehlt, setGewaehlt] = useState(""); + const [tage, setTage] = useState(90); const { daten, fehler } = useDaten( - () => (gewaehlt ? api.dashboardTimeline(90, gewaehlt) : Promise.resolve([])), - [gewaehlt], + () => (gewaehlt ? api.dashboardTimeline(tage, gewaehlt) : Promise.resolve([])), + [gewaehlt, tage], ); useEffect(() => { @@ -308,11 +334,13 @@ function KarteArtikelverlauf() { return (
- +
+ + +
{fehler ? (
{fehler}
) : ( @@ -330,79 +358,123 @@ function KarteArtikelverlauf() { ); } -function KarteBewegungsverlauf({ tage = 30 }) { +function KarteBewegungsverlauf() { + const [tage, setTage] = useState(30); const { daten, fehler } = useDaten(() => api.dashboardActivity(tage), [tage]); - if (fehler) return
{fehler}
; - if (!daten) return
Lädt…
; return ( - kurzesDatum(p.date))} - serien={[ - { name: "Eingelagert", color: tokenFarbe("--ok", "#2e7d55"), werte: daten.map((p) => p.checked_in) }, - { name: "Ausgelagert", color: tokenFarbe("--warn", "#b06f14"), werte: daten.map((p) => p.checked_out) }, - ]} - einheit="Vorgänge" - /> +
+ + {fehler ? ( +
{fehler}
+ ) : !daten ? ( +
Lädt…
+ ) : ( + kurzesDatum(p.date))} + serien={[ + { name: "Eingelagert", color: tokenFarbe("--ok", "#2e7d55"), werte: daten.map((p) => p.checked_in) }, + { name: "Ausgelagert", color: tokenFarbe("--warn", "#b06f14"), werte: daten.map((p) => p.checked_out) }, + ]} + einheit="Vorgänge" + /> + )} +
); } // ---------------------------------------------------------------- Verzeichnis +/** + * Höhen sind Rasterzeilen à 40 px (plus 16 px Abstand je weiterer Zeile). + * Die Mindesthöhe ist bewusst so gewählt, dass der Inhalt ohne Rollbalken + * hineinpasst – vorher war sie gleich der Standardhöhe und beide zu klein. + * + * nutzbare Höhe = h * 40 + (h - 1) * 16 − 41 (Kopf) − 32 (Innenabstand) + * h=3 → 79 px h=4 → 119 px h=6 → 199 px h=7 → 239 px + */ export const KARTEN = { actions: { - titel: "Schnellzugriff", icon: "checkin", standard: { w: 4, h: 1 }, min: { w: 3, h: 1 }, + titel: "Schnellzugriff", icon: "checkin", vorschau: "aktion", + beschreibung: "Knöpfe zum Ein- und Auslagern", + standard: { w: 4, h: 3 }, min: { w: 3, h: 3 }, komponente: KarteAktionen, }, status: { - titel: "Status", icon: "dashboard", standard: { w: 8, h: 1 }, min: { w: 4, h: 1 }, + titel: "Status", icon: "dashboard", vorschau: "kennzahlen", + beschreibung: "Bald ablaufend, abgelaufen, nachzukaufen", + standard: { w: 8, h: 3 }, min: { w: 4, h: 3 }, komponente: KarteStatus, }, "kpi-products": { - titel: "Artikel mit Bestand", icon: "package", standard: { w: 3, h: 1 }, min: { w: 2, h: 1 }, + titel: "Artikel mit Bestand", icon: "package", vorschau: "kennzahl", + beschreibung: "Anzahl verschiedener Artikel im Lager", + standard: { w: 3, h: 3 }, min: { w: 2, h: 3 }, komponente: KarteArtikelzahl, }, "kpi-units": { - titel: "Artikeleinheiten", icon: "box", standard: { w: 3, h: 1 }, min: { w: 2, h: 1 }, + titel: "Artikeleinheiten", icon: "box", vorschau: "kennzahl", + beschreibung: "Summe aller Gebinde im Lager", + standard: { w: 3, h: 3 }, min: { w: 2, h: 3 }, komponente: KarteEinheiten, }, expiring: { - titel: "Bald ablaufend", icon: "clock", standard: { w: 6, h: 3 }, min: { w: 4, h: 2 }, + titel: "Bald ablaufend", icon: "clock", vorschau: "tabelle", + beschreibung: "Chargen innerhalb der Warnfrist", + standard: { w: 6, h: 6 }, min: { w: 4, h: 4 }, komponente: () => , }, expired: { - titel: "Abgelaufen", icon: "alert", standard: { w: 6, h: 2 }, min: { w: 4, h: 2 }, + titel: "Abgelaufen", icon: "alert", vorschau: "tabelle", + beschreibung: "Nur überfällige Chargen", + standard: { w: 6, h: 5 }, min: { w: 4, h: 4 }, komponente: () => , }, shopping: { - titel: "Einkaufsliste", icon: "cart", standard: { w: 6, h: 3 }, min: { w: 4, h: 2 }, + titel: "Einkaufsliste", icon: "cart", vorschau: "tabelle", + beschreibung: "Produkte und Gruppen unter Mindestbestand", + standard: { w: 6, h: 6 }, min: { w: 4, h: 4 }, komponente: KarteEinkaufsliste, }, movements: { - titel: "Letzte Bewegungen", icon: "history", standard: { w: 6, h: 3 }, min: { w: 4, h: 2 }, + titel: "Letzte Bewegungen", icon: "history", vorschau: "tabelle", + beschreibung: "Wer hat zuletzt was ein- oder ausgelagert", + standard: { w: 6, h: 6 }, min: { w: 4, h: 4 }, komponente: KarteBewegungen, }, "expiry-donut": { - titel: "Ablauf-Anteile", icon: "clock", standard: { w: 4, h: 3 }, min: { w: 3, h: 3 }, + titel: "Ablauf-Anteile", icon: "clock", vorschau: "ring", + beschreibung: "Ring: in Ordnung, bald ablaufend, abgelaufen", + standard: { w: 4, h: 6 }, min: { w: 3, h: 5 }, komponente: KarteAblaufRing, }, "category-donut": { - titel: "Kategorien-Anteile", icon: "tag", standard: { w: 4, h: 3 }, min: { w: 3, h: 3 }, + titel: "Kategorien-Anteile", icon: "tag", vorschau: "ring", + beschreibung: "Ring: welche Kategorie wie viel ausmacht", + standard: { w: 4, h: 6 }, min: { w: 3, h: 5 }, komponente: KarteKategorienRing, }, "category-bars": { - titel: "Kategorien nach Zustand", icon: "tag", standard: { w: 8, h: 3 }, min: { w: 4, h: 2 }, + titel: "Kategorien nach Zustand", icon: "tag", vorschau: "balken", + beschreibung: "Balken je Kategorie, nach Ablauf aufgeteilt", + standard: { w: 8, h: 6 }, min: { w: 4, h: 5 }, komponente: KarteKategorienSaeulen, }, "stock-timeline": { - titel: "Bestandsverlauf", icon: "history", standard: { w: 8, h: 3 }, min: { w: 4, h: 2 }, + titel: "Bestandsverlauf", icon: "history", vorschau: "linie", + beschreibung: "Artikeleinheiten im Lager über 90 Tage", + standard: { w: 8, h: 6 }, min: { w: 4, h: 5 }, komponente: KarteBestandsverlauf, }, "product-timeline": { - titel: "Verlauf eines Artikels", icon: "package", standard: { w: 8, h: 3 }, min: { w: 4, h: 3 }, + titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie", + beschreibung: "Ein wählbarer Artikel über 90 Tage", + standard: { w: 8, h: 7 }, min: { w: 4, h: 6 }, komponente: KarteArtikelverlauf, }, "activity-timeline": { - titel: "Ein- und Auslagerungen", icon: "history", standard: { w: 8, h: 3 }, min: { w: 4, h: 2 }, + titel: "Ein- und Auslagerungen", icon: "history", vorschau: "linien", + beschreibung: "Vorgänge je Tag über 30 Tage", + standard: { w: 8, h: 6 }, min: { w: 4, h: 5 }, komponente: KarteBewegungsverlauf, }, }; diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index c60ad46..2eda4a0 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -5,11 +5,15 @@ import { useAuth } from "../auth"; import Icon from "../components/Icon"; import { useConfirm } from "../confirm"; import { KARTEN, KARTEN_IDS } from "../dashboard/cards"; +import Vorschau from "../dashboard/Vorschau"; const Raster = WidthProvider(GridLayout); const SPALTEN = 12; -const ZEILENHOEHE = 96; +// Feine Zeilen: Damit lässt sich die Höhe in kleinen Schritten anpassen. Mit den +// vorherigen 96 px war schon die kleinste Stufe entweder zu klein (Rollbalken) +// oder unnötig hoch. +const ZEILENHOEHE = 40; /** * Startseite als frei zusammenstellbares Raster. @@ -48,9 +52,20 @@ export default function Dashboard() { useEffect(() => { laden(); }, []); - /** Unbekannte Karten aussortieren – z.B. nach einem Update, das eine entfernt. */ + /** + * Unbekannte Karten aussortieren – z.B. nach einem Update, das eine entfernt. + * Zusätzlich zu kleine Karten auf ihre Mindestgröße heben: Gespeicherte + * Anordnungen aus einer früheren Fassung haben Höhen aus einem gröberen + * Raster und wären sonst zu flach für ihren Inhalt. + */ function bereinige(liste) { - return (liste || []).filter((k) => KARTEN[k.i]); + return (liste || []) + .filter((k) => KARTEN[k.i]) + .map((k) => ({ + ...k, + w: Math.max(k.w, KARTEN[k.i].min.w), + h: Math.max(k.h, KARTEN[k.i].min.h), + })); } const verfuegbar = useMemo( @@ -173,15 +188,17 @@ export default function Dashboard() { Karte hinzufügen {menuOffen && ( -
    +
    {verfuegbar.map((id) => ( -
  • - -
  • + ))} -
+
)} diff --git a/web/src/styles.css b/web/src/styles.css index b6af666..cab0329 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -495,7 +495,10 @@ td select { width: auto; min-width: 0; max-width: 100%; } } .dash-card-head h2 { margin: 0; flex: 1; font-size: 0.9rem; } .dash-card-head .icon { color: var(--muted); } -.dash-card-body { flex: 1; min-height: 0; overflow: auto; padding: var(--sp-4); } +.dash-card-body { + flex: 1; min-height: 0; overflow: auto; padding: var(--sp-4); + display: flex; flex-direction: column; +} /* Anfasser nur im Bearbeitungsmodus - sonst stoert er beim Lesen. */ .dash-grid .react-resizable-handle { display: none; } @@ -536,6 +539,25 @@ td select { width: auto; min-width: 0; max-width: 100%; } } .menu .link-btn:hover { background: var(--surface-2); text-decoration: none; } +/* Auswahl mit Skizze: Der Name allein verraet nicht, ob eine Karte eine + Tabelle, ein Ring oder eine Linie wird. */ +.karten-menu { min-width: 340px; max-height: 60vh; } +.karten-wahl { + display: flex; align-items: center; gap: var(--sp-3); + width: 100%; padding: var(--sp-2); border: none; background: none; + border-radius: var(--radius-sm); cursor: pointer; text-align: left; + font: inherit; color: var(--text); +} +.karten-wahl:hover { background: var(--surface-2); } +.karten-wahl .vorschau { width: 72px; height: 44px; flex: 0 0 auto; } +.karten-text { display: flex; flex-direction: column; min-width: 0; } +.karten-titel { font-weight: 600; font-size: 0.875rem; } + +/* Auswahlfelder in Karten (Zeitraum, Artikel) */ +.card-toolbar { display: flex; gap: var(--sp-2); flex-wrap: wrap; } +.card-toolbar select { margin-top: 0; width: auto; min-width: 0; flex: 1 1 140px; } +select.zeitraum { margin-top: 0; width: auto; min-width: 0; flex: 0 0 auto; align-self: flex-start; } + /* ---------- Startseite: Kartentypen ---------- */ .card-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; } .card-stack { display: flex; flex-direction: column; gap: var(--sp-3); height: 100%; } @@ -551,7 +573,13 @@ td select { width: auto; min-width: 0; max-width: 100%; } .kpi.danger .kpi-value { color: var(--danger); } /* ---------- Diagramme ---------- */ -.chart-donut { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; } +/* Der Ring fuellt die Karte und sitzt mittig – vorher klebte er oben und + darunter blieb eine grosse leere Flaeche stehen. */ +.chart-donut { + display: flex; align-items: center; justify-content: center; + gap: var(--sp-4); flex-wrap: wrap; + flex: 1; min-height: 0; +} .donut-svg { width: 160px; height: 160px; flex: 0 0 auto; } .donut-svg circle { transition: stroke-width 120ms ease; cursor: default; } .donut-value { font-size: 20px; font-weight: 680; fill: var(--text); }