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:
93
web/src/dashboard/KartenConfig.jsx
Normal file
93
web/src/dashboard/KartenConfig.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import Icon from "../components/Icon";
|
||||||
|
import { ZEITRAEUME } from "./cards";
|
||||||
|
import { locationOptions } from "../locationPath";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfig-Dialog einer Karte – wie in HomeAssistant: ein eigener Dialog mit den
|
||||||
|
* Optionen der Karte, statt Dropdowns im Kartenkopf. Welche Felder erscheinen,
|
||||||
|
* bestimmt `karte.config`. Die Werte landen in den Karten-Props (bzw. `tage`
|
||||||
|
* beim Zeitraum) und werden mit dem Layout gespeichert.
|
||||||
|
*/
|
||||||
|
export default function KartenConfig({
|
||||||
|
karte, props: karteProps = {}, tage,
|
||||||
|
produkte = [], locations = [], onProps, onTage, onClose,
|
||||||
|
}) {
|
||||||
|
const felder = karte?.config || [];
|
||||||
|
|
||||||
|
function feld(f) {
|
||||||
|
if (f.type === "product") {
|
||||||
|
return (
|
||||||
|
<label key={f.key}>
|
||||||
|
{f.label}
|
||||||
|
<select value={karteProps[f.key] ?? ""}
|
||||||
|
onChange={(e) => onProps({ [f.key]: 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>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (f.type === "location") {
|
||||||
|
return (
|
||||||
|
<label key={f.key}>
|
||||||
|
{f.label}{f.hint ? <span className="muted small"> ({f.hint})</span> : null}
|
||||||
|
<select value={karteProps[f.key] ?? ""}
|
||||||
|
onChange={(e) => onProps({ [f.key]: e.target.value || undefined })}>
|
||||||
|
<option value="">– alle Orte –</option>
|
||||||
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (f.type === "zeitraum") {
|
||||||
|
return (
|
||||||
|
<label key="zeitraum">
|
||||||
|
{f.label}
|
||||||
|
<select value={tage ?? karte.zeitraum} onChange={(e) => onTage(Number(e.target.value))}>
|
||||||
|
{ZEITRAEUME.map((z) => <option key={z.tage} value={z.tage}>{z.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (f.type === "select") {
|
||||||
|
return (
|
||||||
|
<label key={f.key}>
|
||||||
|
{f.label}
|
||||||
|
<select value={karteProps[f.key] ?? f.default ?? ""}
|
||||||
|
onChange={(e) => onProps({ [f.key]: e.target.value })}>
|
||||||
|
{(f.optionen || []).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (f.type === "boolean") {
|
||||||
|
return (
|
||||||
|
<label key={f.key} className="check-inline">
|
||||||
|
<input type="checkbox" checked={karteProps[f.key] ?? f.default ?? false}
|
||||||
|
onChange={(e) => onProps({ [f.key]: e.target.checked })} />
|
||||||
|
<span>{f.label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
||||||
|
<div className="modal" role="dialog" aria-modal="true">
|
||||||
|
<div className="modal-head">
|
||||||
|
<Icon name="settings" size={18} />
|
||||||
|
<h2>{karte?.titel} einstellen</h2>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body" style={{ display: "grid", gap: "var(--sp-3)" }}>
|
||||||
|
{felder.length === 0
|
||||||
|
? <p className="muted">Diese Karte hat keine Einstellungen.</p>
|
||||||
|
: felder.map(feld)}
|
||||||
|
</div>
|
||||||
|
<div className="btn-pair">
|
||||||
|
<button type="button" className="btn primary" onClick={onClose}>Fertig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { daten, fehler } = useDaten(async () => {
|
||||||
const [produkte, gruppen] = await Promise.all([api.shoppingList(), api.groupShoppingList()]);
|
const [produkte, gruppen, orte] = await Promise.all([
|
||||||
return { produkte, gruppen };
|
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 (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 <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">
|
<div className="table-wrap">
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead><tr><th>Bedarf</th><th className="num">Bestand</th><th className="num">Fehlt</th></tr></thead>
|
||||||
<tr><th>Bedarf</th><th className="num">Bestand</th><th className="num">Fehlt</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{daten.gruppen.map((it) => (
|
{gruppen.map((it) => (
|
||||||
<tr key={`g${it.group_id}`}>
|
<tr key={`g${it.group_id}`}>
|
||||||
<td data-label="Bedarf"><span className="badge accent">Gruppe</span> {it.name}</td>
|
<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="Bestand" className="num">{fmt(it.stock)} {it.unit_name}</td>
|
||||||
<td data-label="Fehlt" className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
<td data-label="Fehlt" className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{daten.produkte.map((it) => (
|
{produkte.map((it) => (
|
||||||
<tr key={`p${it.product_id}`}>
|
<tr key={`p${it.product_id}`}>
|
||||||
<td data-label="Bedarf">{it.name}</td>
|
<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="Bestand" className="num">{prodWert(it, "stock")}</td>
|
||||||
<td data-label="Fehlt" className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
|
<td data-label="Fehlt" className="num strong">{prodWert(it, "deficit")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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" };
|
const BEWEGUNG_LABEL = { in: "Eingelagert", out: "Ausgelagert", adjust: "Korrektur" };
|
||||||
@@ -575,7 +606,7 @@ export const KARTEN = {
|
|||||||
titel: "Artikel im Blick", icon: "package", vorschau: "kennzahl",
|
titel: "Artikel im Blick", icon: "package", vorschau: "kennzahl",
|
||||||
beschreibung: "Bestand eines festen Artikels – mehrfach verwendbar",
|
beschreibung: "Bestand eines festen Artikels – mehrfach verwendbar",
|
||||||
standard: { w: 3, h: 3 }, min: { w: 2, h: 3 },
|
standard: { w: 3, h: 3 }, min: { w: 2, h: 3 },
|
||||||
artikel: true,
|
config: [{ key: "product_id", type: "product", label: "Artikel" }],
|
||||||
komponente: KarteArtikel,
|
komponente: KarteArtikel,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
@@ -618,6 +649,7 @@ export const KARTEN = {
|
|||||||
titel: "Einkaufsliste", icon: "cart", vorschau: "tabelle",
|
titel: "Einkaufsliste", icon: "cart", vorschau: "tabelle",
|
||||||
beschreibung: "Produkte und Gruppen unter Mindestbestand",
|
beschreibung: "Produkte und Gruppen unter Mindestbestand",
|
||||||
standard: { w: 6, h: 6 }, min: { w: 4, h: 4 },
|
standard: { w: 6, h: 6 }, min: { w: 4, h: 4 },
|
||||||
|
config: [{ key: "location_id", type: "location", label: "Lagerort", hint: "leer = alle Orte" }],
|
||||||
komponente: KarteEinkaufsliste,
|
komponente: KarteEinkaufsliste,
|
||||||
},
|
},
|
||||||
movements: {
|
movements: {
|
||||||
@@ -648,19 +680,24 @@ export const KARTEN = {
|
|||||||
titel: "Bestandsverlauf", icon: "history", vorschau: "linie",
|
titel: "Bestandsverlauf", icon: "history", vorschau: "linie",
|
||||||
beschreibung: "Artikeleinheiten im Lager über 90 Tage",
|
beschreibung: "Artikeleinheiten im Lager über 90 Tage",
|
||||||
zeitraum: 90, standard: { w: 8, h: 6 }, min: { w: 4, h: 5 },
|
zeitraum: 90, standard: { w: 8, h: 6 }, min: { w: 4, h: 5 },
|
||||||
|
config: [{ type: "zeitraum", label: "Zeitraum" }],
|
||||||
komponente: KarteBestandsverlauf,
|
komponente: KarteBestandsverlauf,
|
||||||
},
|
},
|
||||||
"product-timeline": {
|
"product-timeline": {
|
||||||
titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie",
|
titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie",
|
||||||
beschreibung: "Ein fester Artikel über die Zeit – mehrfach verwendbar",
|
beschreibung: "Ein fester Artikel über die Zeit – mehrfach verwendbar",
|
||||||
zeitraum: 90, standard: { w: 8, h: 7 }, min: { w: 4, h: 6 },
|
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,
|
komponente: KarteArtikelverlauf,
|
||||||
},
|
},
|
||||||
"activity-timeline": {
|
"activity-timeline": {
|
||||||
titel: "Ein- und Auslagerungen", icon: "history", vorschau: "wasserfall",
|
titel: "Ein- und Auslagerungen", icon: "history", vorschau: "wasserfall",
|
||||||
beschreibung: "Brücke: Bestandslinie mit grünen und roten Balken je Bewegung",
|
beschreibung: "Brücke: Bestandslinie mit grünen und roten Balken je Bewegung",
|
||||||
zeitraum: 30, standard: { w: 8, h: 6 }, min: { w: 5, h: 5 },
|
zeitraum: 30, standard: { w: 8, h: 6 }, min: { w: 5, h: 5 },
|
||||||
|
config: [{ type: "zeitraum", label: "Zeitraum" }],
|
||||||
komponente: KarteBewegungsverlauf,
|
komponente: KarteBewegungsverlauf,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { useAuth } from "../auth";
|
|||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import { useToast } from "../toast";
|
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";
|
import Vorschau from "../dashboard/Vorschau";
|
||||||
|
|
||||||
const Raster = WidthProvider(GridLayout);
|
const Raster = WidthProvider(GridLayout);
|
||||||
@@ -54,6 +55,8 @@ export default function Dashboard() {
|
|||||||
const [fehler, setFehler] = useState(null);
|
const [fehler, setFehler] = useState(null);
|
||||||
const [menuOffen, setMenuOffen] = useState(false);
|
const [menuOffen, setMenuOffen] = useState(false);
|
||||||
const [produkte, setProdukte] = useState([]);
|
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 aktivesId = routeId ? Number(routeId) : dashboards[0]?.id;
|
||||||
const aktives = dashboards.find((d) => d.id === aktivesId) ?? dashboards[0];
|
const aktives = dashboards.find((d) => d.id === aktivesId) ?? dashboards[0];
|
||||||
@@ -80,14 +83,15 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
useEffect(() => { laden(); }, []);
|
useEffect(() => { laden(); }, []);
|
||||||
|
|
||||||
// Artikelliste nur einmal für die Einstellung der artikelbezogenen Karten.
|
// Artikel- und Lagerortliste einmal für die Karten-Konfiguration laden.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.listProducts().then(setProdukte).catch(() => {});
|
api.listProducts().then(setProdukte).catch(() => {});
|
||||||
|
api.listLocations().then(setLocations).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Beim Wechsel des Dashboards einen offenen Entwurf verwerfen – sonst
|
// Beim Wechsel des Dashboards einen offenen Entwurf verwerfen – sonst
|
||||||
// landete er beim Speichern auf dem falschen.
|
// 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.
|
* 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) });
|
const gespeichert = await api.updateDashboard(aktives.id, { layout: schlank(entwurf) });
|
||||||
setDashboards((alt) => alt.map((d) => (d.id === gespeichert.id ? gespeichert : d)));
|
setDashboards((alt) => alt.map((d) => (d.id === gespeichert.id ? gespeichert : d)));
|
||||||
setEntwurf(null);
|
setEntwurf(null);
|
||||||
|
setKonfigKarte(null);
|
||||||
toast("Anordnung gespeichert.");
|
toast("Anordnung gespeichert.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
@@ -271,26 +276,10 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Zeitraum einer Karte ändern. Passiert nur noch im Konfig-Dialog, also im
|
||||||
* Zeitraum einer Verlaufskarte ändern. Außerhalb des Bearbeitungsmodus wird
|
* Bearbeitungsmodus – gespeichert wird mit „Speichern" wie jede andere Änderung. */
|
||||||
* die Wahl still gespeichert – sonst wäre sie nach dem nächsten Laden wieder
|
function setzeZeitraum(kennung, tage) {
|
||||||
* weg, ohne dass es einen sichtbaren Speichern-Knopf gäbe.
|
setEntwurf((alt) => (alt ?? []).map((k) => (k.i === kennung ? { ...k, tage } : k)));
|
||||||
*/
|
|
||||||
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. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const eigenes = herkunft === "user" && aktives?.id;
|
const eigenes = herkunft === "user" && aktives?.id;
|
||||||
@@ -348,7 +337,8 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary" onClick={speichern}>Speichern</button>
|
<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
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@@ -387,33 +377,24 @@ export default function Dashboard() {
|
|||||||
if (!karte) return null;
|
if (!karte) return null;
|
||||||
const Inhalt = karte.komponente;
|
const Inhalt = karte.komponente;
|
||||||
const artikel = produkte.find((p) => p.id === eintrag.props?.product_id);
|
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 (
|
return (
|
||||||
<section key={eintrag.i} className="card dash-card">
|
<section key={eintrag.i} className="card dash-card">
|
||||||
<div className="dash-card-head">
|
<div className="dash-card-head">
|
||||||
<Icon name={karte.icon} size={16} />
|
<Icon name={karte.icon} size={16} />
|
||||||
<h2>{artikel ? `${karte.titel}: ${artikel.name}` : karte.titel}</h2>
|
<h2>{zusatz ? `${karte.titel}: ${zusatz}` : karte.titel}</h2>
|
||||||
{/* Zeitraum gehört in den Kopf: So bleibt die volle Kartenhöhe
|
{/* Einstellungen sammelt der Konfig-Dialog (wie in HomeAssistant),
|
||||||
dem Diagramm. onMouseDown stoppt das Ziehen der Karte. */}
|
statt einzelner Dropdowns im Kopf. Nur beim Anordnen sichtbar
|
||||||
{karte.zeitraum && (
|
und nur, wenn die Karte überhaupt Optionen hat. */}
|
||||||
<select className="zeitraum" value={eintrag.tage ?? karte.zeitraum}
|
{bearbeiten && karte.config && (
|
||||||
|
<button className="btn-icon" title="Karte einstellen"
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setzeZeitraum(eintrag.i, Number(e.target.value))}>
|
onClick={() => setKonfigKarte(eintrag.i)}>
|
||||||
{ZEITRAEUME.map((z) => (
|
<Icon name="settings" size={15} />
|
||||||
<option key={z.tage} value={z.tage}>{z.label}</option>
|
</button>
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
)}
|
)}
|
||||||
{bearbeiten && (
|
{bearbeiten && (
|
||||||
<button className="btn-icon danger" title="Karte entfernen"
|
<button className="btn-icon danger" title="Karte entfernen"
|
||||||
@@ -456,6 +437,24 @@ export default function Dashboard() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user