From 4972feef0982b8c6f5b8c89f4b8258d0fc64513f Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 2 Oct 2026 15:23:48 +0200 Subject: [PATCH] Artikelauswahl im Dashboard: Marke dazu, Suche davor 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 --- web/src/dashboard/KartenConfig.jsx | 65 ++++++++++++++++++++++++++---- web/src/pages/Dashboard.jsx | 7 +++- web/src/units.js | 17 ++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/web/src/dashboard/KartenConfig.jsx b/web/src/dashboard/KartenConfig.jsx index c43c804..2f58099 100644 --- a/web/src/dashboard/KartenConfig.jsx +++ b/web/src/dashboard/KartenConfig.jsx @@ -1,6 +1,8 @@ +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 @@ -17,14 +19,9 @@ export default function KartenConfig({ function feld(f) { if (f.type === "product") { return ( - + onProps({ [f.key]: id ?? undefined })} /> ); } if (f.type === "location") { @@ -91,3 +88,55 @@ export default function KartenConfig({ ); } + +/** + * 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 ( +
+ + +

+ {suche + ? `${gefiltert.length} von ${produkte.length} Artikeln` + : `${produkte.length} Artikel`} +

+
+ ); +} diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index 33eb1c9..9501603 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -9,6 +9,7 @@ import { useToast } from "../toast"; import { KARTEN, KARTEN_IDS } from "../dashboard/cards"; import KartenConfig from "../dashboard/KartenConfig"; import Vorschau from "../dashboard/Vorschau"; +import { artikelLabel } from "../units"; const Raster = WidthProvider(GridLayout); @@ -380,7 +381,11 @@ export default function Dashboard() { 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 + // Im Titel steht die Marke mit, sonst heissen zwei Karten gleich, + // wenn zwei Hersteller denselben Artikelnamen fuehren. Die + // Gebindegroesse bliebe dort zu lang - die steht in der Auswahl. + const zusatz = (artikel ? artikelLabel(artikel, { groesse: false }) : "") + || ort?.name; return (
diff --git a/web/src/units.js b/web/src/units.js index 8f79567..fcac66f 100644 --- a/web/src/units.js +++ b/web/src/units.js @@ -114,6 +114,23 @@ export function stockLabelMitZweit(p) { return zweit ? `${stockLabel(p)} · ${zweit}` : stockLabel(p); } +/** + * Artikelname, wie man ihn in einer Auswahlliste braucht: "Pesto Rosso · + * Barilla · 190 g". + * + * Der blosse Name reicht nicht. Ein Vorrat enthaelt denselben Artikel von + * mehreren Herstellern und in mehreren Groessen; zwei Zeilen "Pesto Rosso" + * untereinander sind dann nicht zu unterscheiden, und man waehlt die falsche. + * Marke und Gebindegroesse sind genau die beiden Angaben, die sie trennen. + */ +export function artikelLabel(p, { groesse = true } = {}) { + if (!p) return ""; + const gebinde = groesse && p.package_size > 0 + ? `${fmt(p.package_size)} ${unitShort(p.base_unit)}` + : ""; + return [p.name, p.brand, gebinde].filter(Boolean).join(" · "); +} + // Auswahlmöglichkeiten für Menge beim Ein-/Auslagern eines Produkts. export function unitOptions(product) { const opts = [{ value: baseUnitToInput(product.base_unit), label: unitShort(product.base_unit) }];