Web: Dashboard-Karten wie HomeAssistant konfigurieren + Einkaufsliste je Lagerort
Karten-Optionen (Artikel, Lagerort, Zeitraum) sassen als einzelne Dropdowns im Kartenkopf. Jetzt: deklaratives config-Schema je Kartenart und ein Zahnrad-Dialog (KartenConfig) im Anordnen-Modus - der Kopf zeigt sonst nur noch Titel/Papierkorb. Felder: product, location, zeitraum (erweiterbar: select, boolean). Werte bleiben in props/tage, keine Migration. Einkaufsliste-Karte laedt zusaetzlich shoppingByLocation und zeigt Gesamt + Bedarfe je Lagerort. Ueber die Karten-Konfig waehlbarer Lagerort: leer = alle Orte, gesetzt = Gesamt + nur dieser Ort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f3bab15362
commit
3e10e254c8
+53
-16
@@ -306,43 +306,74 @@ function KarteAblauf({ modus = "alle" }) {
|
||||
);
|
||||
}
|
||||
|
||||
function KarteEinkaufsliste() {
|
||||
/**
|
||||
* Einkaufsliste mit Gesamt-Bedarf UND Bedarf je Lagerort. Über die Karten-Konfig
|
||||
* lässt sich ein Lagerort festlegen: leer = alle Orte, sonst nur dieser.
|
||||
*/
|
||||
function KarteEinkaufsliste({ props: karteProps }) {
|
||||
const ortId = karteProps?.location_id || "";
|
||||
const { daten, fehler } = useDaten(async () => {
|
||||
const [produkte, gruppen] = await Promise.all([api.shoppingList(), api.groupShoppingList()]);
|
||||
return { produkte, gruppen };
|
||||
const [produkte, gruppen, orte] = await Promise.all([
|
||||
api.shoppingList(), api.groupShoppingList(), api.shoppingByLocation(),
|
||||
]);
|
||||
return { produkte, gruppen, orte };
|
||||
});
|
||||
|
||||
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.produkte.length && !daten.gruppen.length) {
|
||||
return <div className="empty">Alle Mindestbestände erreicht.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
const orte = ortId ? daten.orte.filter((o) => o.location_id === ortId) : daten.orte;
|
||||
const gesamtLeer = !daten.produkte.length && !daten.gruppen.length;
|
||||
const orteLeer = orte.every((o) => !o.products.length && !o.groups.length);
|
||||
if (gesamtLeer && orteLeer) return <div className="empty">Alle Mindestbestände erreicht.</div>;
|
||||
|
||||
// Ein Abschnitt (Gesamt oder ein Ort). Die Produkt-Formatierung kommt herein,
|
||||
// weil Gesamt-Produkte Gebinde/Basiseinheit tragen, je-Ort-Bedarfe aber schon
|
||||
// eine fertige Einheit (unit_label).
|
||||
const abschnitt = (gruppen, produkte, prodWert) => (
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Bedarf</th><th className="num">Bestand</th><th className="num">Fehlt</th></tr>
|
||||
</thead>
|
||||
<thead><tr><th>Bedarf</th><th className="num">Bestand</th><th className="num">Fehlt</th></tr></thead>
|
||||
<tbody>
|
||||
{daten.gruppen.map((it) => (
|
||||
{gruppen.map((it) => (
|
||||
<tr key={`g${it.group_id}`}>
|
||||
<td data-label="Bedarf"><span className="badge accent">Gruppe</span> {it.name}</td>
|
||||
<td data-label="Bestand" className="num">{fmt(it.stock)} {it.unit_name}</td>
|
||||
<td data-label="Fehlt" className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
||||
</tr>
|
||||
))}
|
||||
{daten.produkte.map((it) => (
|
||||
{produkte.map((it) => (
|
||||
<tr key={`p${it.product_id}`}>
|
||||
<td data-label="Bedarf">{it.name}</td>
|
||||
<td data-label="Bestand" className="num">{amountText(it.stock, it.package_size, it.base_unit)}</td>
|
||||
<td data-label="Fehlt" className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
|
||||
<td data-label="Bestand" className="num">{prodWert(it, "stock")}</td>
|
||||
<td data-label="Fehlt" className="num strong">{prodWert(it, "deficit")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
const gesamtProd = (it, feld) => amountText(it[feld], it.package_size, it.base_unit);
|
||||
const ortProd = (it, feld) => `${fmt(it[feld])} ${it.unit_label}`;
|
||||
|
||||
return (
|
||||
<div className="card-stack">
|
||||
{!gesamtLeer && (
|
||||
<div>
|
||||
<div className="muted small" style={{ marginBottom: "var(--sp-1)", fontWeight: 600 }}>Gesamt</div>
|
||||
{abschnitt(daten.gruppen, daten.produkte, gesamtProd)}
|
||||
</div>
|
||||
)}
|
||||
{orte.map((o) => ((o.products.length || o.groups.length) ? (
|
||||
<div key={o.location_id}>
|
||||
<div className="muted small" style={{ marginBottom: "var(--sp-1)", fontWeight: 600 }}>
|
||||
<Icon name="location" size={13} /> {o.location_name}
|
||||
</div>
|
||||
{abschnitt(o.groups, o.products, ortProd)}
|
||||
</div>
|
||||
) : null))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BEWEGUNG_LABEL = { in: "Eingelagert", out: "Ausgelagert", adjust: "Korrektur" };
|
||||
@@ -575,7 +606,7 @@ export const KARTEN = {
|
||||
titel: "Artikel im Blick", icon: "package", vorschau: "kennzahl",
|
||||
beschreibung: "Bestand eines festen Artikels – mehrfach verwendbar",
|
||||
standard: { w: 3, h: 3 }, min: { w: 2, h: 3 },
|
||||
artikel: true,
|
||||
config: [{ key: "product_id", type: "product", label: "Artikel" }],
|
||||
komponente: KarteArtikel,
|
||||
},
|
||||
status: {
|
||||
@@ -618,6 +649,7 @@ export const KARTEN = {
|
||||
titel: "Einkaufsliste", icon: "cart", vorschau: "tabelle",
|
||||
beschreibung: "Produkte und Gruppen unter Mindestbestand",
|
||||
standard: { w: 6, h: 6 }, min: { w: 4, h: 4 },
|
||||
config: [{ key: "location_id", type: "location", label: "Lagerort", hint: "leer = alle Orte" }],
|
||||
komponente: KarteEinkaufsliste,
|
||||
},
|
||||
movements: {
|
||||
@@ -648,19 +680,24 @@ export const KARTEN = {
|
||||
titel: "Bestandsverlauf", icon: "history", vorschau: "linie",
|
||||
beschreibung: "Artikeleinheiten im Lager über 90 Tage",
|
||||
zeitraum: 90, standard: { w: 8, h: 6 }, min: { w: 4, h: 5 },
|
||||
config: [{ type: "zeitraum", label: "Zeitraum" }],
|
||||
komponente: KarteBestandsverlauf,
|
||||
},
|
||||
"product-timeline": {
|
||||
titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie",
|
||||
beschreibung: "Ein fester Artikel über die Zeit – mehrfach verwendbar",
|
||||
zeitraum: 90, standard: { w: 8, h: 7 }, min: { w: 4, h: 6 },
|
||||
artikel: true,
|
||||
config: [
|
||||
{ key: "product_id", type: "product", label: "Artikel" },
|
||||
{ type: "zeitraum", label: "Zeitraum" },
|
||||
],
|
||||
komponente: KarteArtikelverlauf,
|
||||
},
|
||||
"activity-timeline": {
|
||||
titel: "Ein- und Auslagerungen", icon: "history", vorschau: "wasserfall",
|
||||
beschreibung: "Brücke: Bestandslinie mit grünen und roten Balken je Bewegung",
|
||||
zeitraum: 30, standard: { w: 8, h: 6 }, min: { w: 5, h: 5 },
|
||||
config: [{ type: "zeitraum", label: "Zeitraum" }],
|
||||
komponente: KarteBewegungsverlauf,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user