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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 10:09:05 +02:00
parent 4246e0d98f
commit aa58914374
5 changed files with 298 additions and 63 deletions

View File

@@ -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 (
<select className="zeitraum" value={wert} onChange={(e) => setzen(Number(e.target.value))}>
{ZEITRAEUME.map((z) => <option key={z.tage} value={z.tage}>{z.label}</option>)}
</select>
);
}
function KarteBestandsverlauf() {
const [tage, setTage] = useState(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>;
return (
<Line
labels={daten.map((p) => kurzesDatum(p.date))}
serien={[{
name: "Artikeleinheiten",
color: tokenFarbe("--accent", "#3f51b5"),
werte: daten.map((p) => p.article_units),
}]}
einheit="Einheiten"
/>
<div className="card-stack">
<ZeitraumWahl wert={tage} setzen={setTage} />
{fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : !daten ? (
<div className="empty">Lädt</div>
) : (
<Line
labels={daten.map((p) => kurzesDatum(p.date))}
serien={[{
name: "Artikeleinheiten",
color: tokenFarbe("--accent", "#3f51b5"),
werte: daten.map((p) => p.article_units),
}]}
einheit="Einheiten"
/>
)}
</div>
);
}
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 (
<div className="card-stack">
<select value={gewaehlt} onChange={(e) => setGewaehlt(e.target.value)}
style={{ marginTop: 0, maxWidth: 280 }}>
<option value=""> Artikel wählen </option>
{produkte.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<div className="card-toolbar">
<select value={gewaehlt} onChange={(e) => setGewaehlt(e.target.value)}>
<option value=""> Artikel wählen </option>
{produkte.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<ZeitraumWahl wert={tage} setzen={setTage} />
</div>
{fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : (
@@ -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 <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
return (
<Line
labels={daten.map((p) => 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"
/>
<div className="card-stack">
<ZeitraumWahl wert={tage} setzen={setTage} />
{fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : !daten ? (
<div className="empty">Lädt</div>
) : (
<Line
labels={daten.map((p) => 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"
/>
)}
</div>
);
}
// ---------------------------------------------------------------- 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: () => <KarteAblauf />,
},
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: () => <KarteAblauf nurAbgelaufen />,
},
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,
},
};