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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user