Web: Skeleton-Loading (schimmernde Ladeplatzhalter)

Statt nacktem 'Lädt…'/'Wird geladen…' jetzt geformte, schimmernde
Platzhalter an den zentralen Stellen:
- Neuer Baustein components/Skeleton.jsx (Skeleton/SkeletonLines/CardSkeleton),
  Sweep-Animation, Farben aus CSS-Tokens (Dark-Mode automatisch), respektiert
  prefers-reduced-motion.
- DataTable: bei Erstladung Platzhalterzeilen in Spaltenform (deckt alle
  8 Tabellenseiten). Nachladen mit vorhandenen Zeilen laesst Daten stehen.
- Dashboard-Karten: Zustand-Wrapper + Inline-Diagramm-Lader zeigen
  KPI-/Listen-/Diagramm-Skelette passend zur Kartenform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-04 00:30:25 +02:00
parent da5934948e
commit e214bb76a9
4 changed files with 126 additions and 17 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api";
import Icon from "../components/Icon";
import { CardSkeleton } from "../components/Skeleton";
import { useSettings } from "../settings";
import {
amountText, articlePrimary, articleSecondary, fmt, relativeExpiry,
@@ -35,9 +36,9 @@ function useDaten(laden, abhaengig = []) {
return { daten, fehler };
}
function Zustand({ fehler, daten, children, leer = "Keine Daten." }) {
function Zustand({ fehler, daten, children, leer = "Keine Daten.", skelett = "liste" }) {
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (daten == null) return <div className="empty">Lädt</div>;
if (daten == null) return <CardSkeleton variant={skelett} />;
if (Array.isArray(daten) && daten.length === 0) return <div className="empty">{leer}</div>;
return children;
}
@@ -183,7 +184,7 @@ function KarteArtikel({ props: karteProps }) {
return <div className="empty">Kein Artikel gewählt über Anordnen einstellen.</div>;
}
return (
<Zustand fehler={fehler} daten={daten}>
<Zustand fehler={fehler} daten={daten} skelett="kpi">
{daten && (
<div className="card-stack">
<div className="kpi">
@@ -219,7 +220,7 @@ function Kennzahl({ wert, label, icon, ton }) {
function KarteStatus() {
const { daten, fehler } = useDaten(() => api.dashboardStats());
return (
<Zustand fehler={fehler} daten={daten}>
<Zustand fehler={fehler} daten={daten} skelett="kpi">
<div className="kpi-row">
<Kennzahl icon="clock" label="Bald ablaufend" wert={daten?.expiring_soon ?? 0}
ton={daten?.expiring_soon ? "warn" : ""} />
@@ -234,7 +235,7 @@ function KarteStatus() {
function KarteArtikelzahl() {
const { daten, fehler } = useDaten(() => api.dashboardStats());
return (
<Zustand fehler={fehler} daten={daten}>
<Zustand fehler={fehler} daten={daten} skelett="kpi">
<Kennzahl icon="package" label="Artikel mit Bestand" wert={daten?.products_in_stock ?? 0} />
</Zustand>
);
@@ -243,7 +244,7 @@ function KarteArtikelzahl() {
function KarteEinheiten() {
const { daten, fehler } = useDaten(() => api.dashboardStats());
return (
<Zustand fehler={fehler} daten={daten}>
<Zustand fehler={fehler} daten={daten} skelett="kpi">
<Kennzahl icon="box" label="Artikeleinheiten im Lager" wert={fmt(daten?.article_units ?? 0)} />
</Zustand>
);
@@ -321,7 +322,7 @@ function KarteEinkaufsliste({ props: karteProps }) {
});
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="liste" />;
const orte = ortId ? daten.orte.filter((o) => o.location_id === ortId) : daten.orte;
const gesamtLeer = !daten.produkte.length && !daten.gruppen.length;
@@ -433,7 +434,7 @@ function KarteAblaufRing({ props: karteProps }) {
// Ob „Ohne MHD" (Gegenstände/undatierte Chargen) mitgezählt wird Karten-Einstellung.
const mitOhneMhd = karteProps?.mit_ohne_mhd ?? true;
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="chart" />;
// Zustandsfarben, nie Serienfarben und immer mit Beschriftung.
const segmente = [
@@ -452,7 +453,7 @@ function KarteKategorienRing({ props: karteProps }) {
const tiefe = karteProps?.tiefe ? Number(karteProps.tiefe) : undefined;
const { daten, fehler } = useDaten(() => api.dashboardByCategory(tiefe), [tiefe]);
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="chart" />;
// Detailansicht: jede Kategorie einzeln statt der Bündelung zu „Andere“.
const segmente = mitFarben(
@@ -466,7 +467,7 @@ function KarteKategorienRing({ props: karteProps }) {
function KarteKategorienSaeulen() {
const { daten, fehler } = useDaten(() => api.dashboardByCategory());
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="chart" />;
const zeilen = daten.slice(0, 8).map((k) => ({
name: k.name,
@@ -513,7 +514,7 @@ function KarteBestandsverlauf({ tage = 90 }) {
const { daten, fehler } = useDaten(() => api.dashboardTimeline(tage), [tage]);
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="chart" />;
return (
<Line
@@ -572,7 +573,7 @@ function KarteBewegungsverlauf({ tage = 30 }) {
const { daten, fehler } = useDaten(() => api.dashboardFlow(tage), [tage]);
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
if (!daten) return <CardSkeleton variant="chart" />;
return (
<Wasserfall