Die Auswahl zeigte nur den Namen. In einem Vorrat mit mehreren
Herstellern standen damit zwei Zeilen "Pesto Rosso" untereinander, ohne
jeden Unterschied - man konnte nur raten und hinterher nachsehen, ob die
richtige Karte entstanden ist. Dazu war es eine blanke Liste ueber den
ganzen Katalog, hunderte Zeilen lang.
Jetzt steht in jeder Zeile "Name - Marke - Gebindegroesse", und darueber
ein Suchfeld, das wortweise filtert ("pesto mutti" findet eins von
vier). Der bereits gewaehlte Artikel bleibt in der Liste, auch wenn die
Suche ihn ausblendet - sonst zeigte das Feld nichts an und ein Klick
daneben haette die Wahl verworfen.
Der Kartentitel traegt die Marke jetzt ebenfalls, sonst hiessen zwei
Karten weiterhin gleich. Die Gebindegroesse bleibt dort weg, sie waere
im Titel zu lang; dafuer gibt es den Schalter an artikelLabel().
Die Beschriftung liegt in units.js, weil sie ueberall gebraucht wird, wo
Artikel zur Auswahl stehen - zwei Fassungen davon liefen sonst
auseinander.
Von Hand geprueft: zwei gleichnamige Pesto angelegt, Auswahl
unterscheidet sie, Suche kuerzt auf einen, Titel liest "Pesto Rosso -
Mutti". Suite bei 517.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
5.1 KiB
React
143 lines
5.1 KiB
React
import { useMemo, useState } from "react";
|
||
import Icon from "../components/Icon";
|
||
import { ZEITRAEUME } from "./cards";
|
||
import { locationOptions } from "../locationPath";
|
||
import { artikelLabel } from "../units";
|
||
|
||
/**
|
||
* 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 (
|
||
<ArtikelWahl key={f.key} label={f.label} produkte={produkte}
|
||
wert={karteProps[f.key] ?? ""}
|
||
onWahl={(id) => onProps({ [f.key]: id ?? undefined })} />
|
||
);
|
||
}
|
||
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}{f.hint ? <span className="muted small"> ({f.hint})</span> : null}
|
||
<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>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Artikel aussuchen – mit Suchfeld und sprechenden Zeilen.
|
||
*
|
||
* Eine blanke Auswahlliste ueber den ganzen Katalog ist unbrauchbar: Sie ist
|
||
* hunderte Zeilen lang, und weil nur der Name darin stand, standen "Pesto
|
||
* Rosso" und "Pesto Rosso" untereinander, ohne Unterschied. Jetzt steht Marke
|
||
* und Gebindegroesse dabei, und das Suchfeld daruber kuerzt die Liste auf das,
|
||
* was man meint.
|
||
*/
|
||
function ArtikelWahl({ label, produkte, wert, onWahl }) {
|
||
const [suche, setSuche] = useState("");
|
||
|
||
const gefiltert = useMemo(() => {
|
||
const begriffe = suche.toLowerCase().split(/\s+/).filter(Boolean);
|
||
if (begriffe.length === 0) return produkte;
|
||
return produkte.filter((p) => {
|
||
const text = artikelLabel(p).toLowerCase();
|
||
return begriffe.every((b) => text.includes(b));
|
||
});
|
||
}, [produkte, suche]);
|
||
|
||
// Der gewaehlte Artikel muss in der Liste bleiben, auch wenn die Suche ihn
|
||
// gerade ausblendet - sonst zeigt das Feld nichts an und ein Klick daneben
|
||
// wuerde die Wahl verwerfen.
|
||
const gewaehlt = produkte.find((p) => String(p.id) === String(wert));
|
||
const sichtbar = gewaehlt && !gefiltert.includes(gewaehlt)
|
||
? [gewaehlt, ...gefiltert]
|
||
: gefiltert;
|
||
|
||
return (
|
||
<div>
|
||
<label>
|
||
{label}
|
||
<input value={suche} placeholder="suchen … (Name oder Marke)"
|
||
onChange={(e) => setSuche(e.target.value)} />
|
||
</label>
|
||
<select value={wert} style={{ marginTop: "var(--sp-2)" }}
|
||
onChange={(e) => onWahl(e.target.value ? Number(e.target.value) : null)}>
|
||
<option value="">– Artikel wählen –</option>
|
||
{sichtbar.map((p) => (
|
||
<option key={p.id} value={p.id}>{artikelLabel(p)}</option>
|
||
))}
|
||
</select>
|
||
<p className="muted small mt-0">
|
||
{suche
|
||
? `${gefiltert.length} von ${produkte.length} Artikeln`
|
||
: `${produkte.length} Artikel`}
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|