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:
Scarriffle
2026-07-30 14:29:09 +02:00
parent f3bab15362
commit 3e10e254c8
3 changed files with 191 additions and 62 deletions

View File

@@ -6,7 +6,8 @@ import { useAuth } from "../auth";
import Icon from "../components/Icon";
import { useConfirm } from "../confirm";
import { useToast } from "../toast";
import { KARTEN, KARTEN_IDS, ZEITRAEUME } from "../dashboard/cards";
import { KARTEN, KARTEN_IDS } from "../dashboard/cards";
import KartenConfig from "../dashboard/KartenConfig";
import Vorschau from "../dashboard/Vorschau";
const Raster = WidthProvider(GridLayout);
@@ -54,6 +55,8 @@ export default function Dashboard() {
const [fehler, setFehler] = useState(null);
const [menuOffen, setMenuOffen] = useState(false);
const [produkte, setProdukte] = useState([]);
const [locations, setLocations] = useState([]);
const [konfigKarte, setKonfigKarte] = useState(null); // Kennung der Karte im Konfig-Dialog
const aktivesId = routeId ? Number(routeId) : dashboards[0]?.id;
const aktives = dashboards.find((d) => d.id === aktivesId) ?? dashboards[0];
@@ -80,14 +83,15 @@ export default function Dashboard() {
useEffect(() => { laden(); }, []);
// Artikelliste nur einmal für die Einstellung der artikelbezogenen Karten.
// Artikel- und Lagerortliste einmal für die Karten-Konfiguration laden.
useEffect(() => {
api.listProducts().then(setProdukte).catch(() => {});
api.listLocations().then(setLocations).catch(() => {});
}, []);
// Beim Wechsel des Dashboards einen offenen Entwurf verwerfen sonst
// landete er beim Speichern auf dem falschen.
useEffect(() => { setEntwurf(null); setMenuOffen(false); }, [aktivesId]);
useEffect(() => { setEntwurf(null); setMenuOffen(false); setKonfigKarte(null); }, [aktivesId]);
/**
* Unbekannte Karten aussortieren z.B. nach einem Update, das eine entfernt.
@@ -149,6 +153,7 @@ export default function Dashboard() {
const gespeichert = await api.updateDashboard(aktives.id, { layout: schlank(entwurf) });
setDashboards((alt) => alt.map((d) => (d.id === gespeichert.id ? gespeichert : d)));
setEntwurf(null);
setKonfigKarte(null);
toast("Anordnung gespeichert.");
} catch (err) {
setFehler(err.message);
@@ -271,26 +276,10 @@ export default function Dashboard() {
});
}
/**
* Zeitraum einer Verlaufskarte ändern. Außerhalb des Bearbeitungsmodus wird
* die Wahl still gespeichert sonst wäre sie nach dem nächsten Laden wieder
* weg, ohne dass es einen sichtbaren Speichern-Knopf gäbe.
*/
async function setzeZeitraum(kennung, tage) {
const geaendert = aktuell.map((k) => (k.i === kennung ? { ...k, tage } : k));
if (bearbeiten) {
setEntwurf(geaendert);
return;
}
setDashboards((alt) => alt.map((d) => (
d.id === aktives.id ? { ...d, layout: geaendert } : d
)));
if (erzwungen || !aktives?.id) return;
try {
await api.updateDashboard(aktives.id, { layout: geaendert });
} catch {
/* Nicht speichern zu können ist hier kein Grund, die Ansicht zu stören. */
}
/** Zeitraum einer Karte ändern. Passiert nur noch im Konfig-Dialog, also im
* Bearbeitungsmodus gespeichert wird mit „Speichern" wie jede andere Änderung. */
function setzeZeitraum(kennung, tage) {
setEntwurf((alt) => (alt ?? []).map((k) => (k.i === kennung ? { ...k, tage } : k)));
}
const eigenes = herkunft === "user" && aktives?.id;
@@ -348,7 +337,8 @@ export default function Dashboard() {
)}
</div>
<button className="btn primary" onClick={speichern}>Speichern</button>
<button className="btn ghost" onClick={() => { setEntwurf(null); setMenuOffen(false); }}>
<button className="btn ghost"
onClick={() => { setEntwurf(null); setMenuOffen(false); setKonfigKarte(null); }}>
Abbrechen
</button>
</>
@@ -387,33 +377,24 @@ export default function Dashboard() {
if (!karte) return null;
const Inhalt = karte.komponente;
const artikel = produkte.find((p) => p.id === eintrag.props?.product_id);
const ort = eintrag.props?.location_id
? locations.find((l) => l.id === eintrag.props.location_id)
: null;
const zusatz = artikel?.name || ort?.name; // im Titel gezeigt
return (
<section key={eintrag.i} className="card dash-card">
<div className="dash-card-head">
<Icon name={karte.icon} size={16} />
<h2>{artikel ? `${karte.titel}: ${artikel.name}` : karte.titel}</h2>
{/* Zeitraum gehört in den Kopf: So bleibt die volle Kartenhöhe
dem Diagramm. onMouseDown stoppt das Ziehen der Karte. */}
{karte.zeitraum && (
<select className="zeitraum" value={eintrag.tage ?? karte.zeitraum}
<h2>{zusatz ? `${karte.titel}: ${zusatz}` : karte.titel}</h2>
{/* Einstellungen sammelt der Konfig-Dialog (wie in HomeAssistant),
statt einzelner Dropdowns im Kopf. Nur beim Anordnen sichtbar
und nur, wenn die Karte überhaupt Optionen hat. */}
{bearbeiten && karte.config && (
<button className="btn-icon" title="Karte einstellen"
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => setzeZeitraum(eintrag.i, Number(e.target.value))}>
{ZEITRAEUME.map((z) => (
<option key={z.tage} value={z.tage}>{z.label}</option>
))}
</select>
)}
{/* Artikelbezogene Karten werden beim Anordnen eingestellt -
so kann dieselbe Art mehrfach auf verschiedene Artikel zeigen. */}
{bearbeiten && karte.artikel && (
<select className="zeitraum" value={eintrag.props?.product_id ?? ""}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => setzeProps(eintrag.i, {
product_id: e.target.value ? Number(e.target.value) : undefined,
})}>
<option value=""> Artikel wählen </option>
{produkte.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
onClick={() => setKonfigKarte(eintrag.i)}>
<Icon name="settings" size={15} />
</button>
)}
{bearbeiten && (
<button className="btn-icon danger" title="Karte entfernen"
@@ -456,6 +437,24 @@ export default function Dashboard() {
</>
)}
</div>
{konfigKarte && (() => {
const inst = aktuell.find((k) => k.i === konfigKarte);
if (!inst) return null;
const karte = KARTEN[inst.type];
return (
<KartenConfig
karte={karte}
props={inst.props || {}}
tage={inst.tage ?? karte.zeitraum}
produkte={produkte}
locations={locations}
onProps={(partial) => setzeProps(konfigKarte, partial)}
onTage={(n) => setzeZeitraum(konfigKarte, n)}
onClose={() => setKonfigKarte(null)}
/>
);
})()}
</div>
);
}