From a917c5f64eea3ea8ea8b4e2b84cc35af9395319d Mon Sep 17 00:00:00 2001 From: Marco Morath Date: Mon, 10 Aug 2026 11:00:21 +0200 Subject: [PATCH] =?UTF-8?q?Direkte=20Eingabe=20der=20Warengruppe=20biem=20?= =?UTF-8?q?Erfassen=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +++++++------- web/html/app.css | 12 +++--- web/html/js/views/list-detail.js | 71 +++++++++++++++++++++++--------- web/html/sw.js | 2 +- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index cf830ab..5473edf 100644 --- a/README.md +++ b/README.md @@ -1319,26 +1319,26 @@ Der Zeilenabstand der Eingabemaske kommt aus einer einzigen Regel Namenszeile – sie steckt seit der Vorschlagsliste in einem `.name-wrap` – war eine solche Einzelregel wirkungslos geworden, ohne dass es auffiel. -### Markt direkt beim Eintragen +### Warengruppe und Markt direkt beim Eintragen -Unter den Detailfeldern steht eine Marktauswahl. Ohne sie müsste jeder Eintrag -nachbearbeitet werden – beim Füllen einer Liste sind das schnell zwanzig -Handgriffe. +Unter den Detailfeldern stehen zwei Auswahlfelder – erst die Warengruppe, dann +der Markt. Ohne sie müsste jeder Eintrag nachbearbeitet werden; beim Füllen +einer Liste sind das schnell zwanzig Handgriffe. -Zwei Eigenheiten: - -**Der gewählte Markt bleibt nach dem Hinzufügen stehen** und überlebt auch den +**Beide bleiben nach dem Hinzufügen stehen** und überleben den Ansichtswechsel (gespeichert je Liste in IndexedDB). Wer eine Liste füllt, -trägt meist mehrere Artikel für denselben Laden ein. +trägt meist mehrere Artikel für denselben Laden und oft auch für dasselbe +Regal ein. Eine getroffene Auswahl ist am grünen Rahmen erkennbar. -**Die leere Auswahl heißt „Vorgabe des Artikels", nicht „ohne Markt".** Das ist -der Unterschied: Bei leerer Auswahl greift der Server auf den im Artikelstamm -hinterlegten Standardmarkt zurück. Wer wirklich keinen Markt will, entfernt ihn -danach im „⋯"-Menü des Eintrags. +**Die leere Auswahl heißt „Vorgabe des Artikels", nicht „ohne Markt".** Der +Unterschied ist wesentlich: Bei leerer Auswahl greift der Server auf die im +Artikelstamm hinterlegte Vorgabe zurück. Wer wirklich keine Zuordnung will, +entfernt sie danach im „⋯"-Menü des Eintrags. -Die Warengruppe bleibt im Formular unsichtbar. Sie kommt aus der Vorgabe des -Artikels oder aus einem Vorschlag; ein zweites Auswahlfeld würde die Zeile -überladen, ohne oft gebraucht zu werden. +**Vorschläge belegen beide Felder vor** – mit dem Markt und der Warengruppe, +in denen der Artikel zuletzt stand. Tippst du den Namen danach frei um, +bleiben sie stehen: Sie sind sichtbar ausgewählt, und sie stillschweigend +zurückzusetzen wäre überraschender, als sie stehenzulassen. Sortiert nach Häufigkeit, dann nach Aktualität; höchstens acht Vorschläge. diff --git a/web/html/app.css b/web/html/app.css index 5a05948..7e4180b 100644 --- a/web/html/app.css +++ b/web/html/app.css @@ -1560,9 +1560,10 @@ body.modal-open .app-footer { display: none; } .app-footer { display: none !important; } } -/* --- Marktauswahl in der Eingabezeile --- */ +/* --- Auswahlfelder in der Eingabezeile (Warengruppe, Markt) --- */ -select.market-select { +select.market-select, +select.category-select { width: 100%; padding: .65rem .5rem; font: inherit; @@ -1572,6 +1573,7 @@ select.market-select { border-radius: var(--radius); } -/* Ist ein Markt gewählt, ist das die Voreinstellung für die nächsten - Artikel - das soll man sehen, ohne hinzusehen. */ -select.market-select:not([data-empty]) { border-color: var(--accent); } +/* Ist etwas gewählt, gilt es auch für die nächsten Artikel - das soll + man sehen, ohne hinzusehen. */ +select.market-select:not([data-empty]), +select.category-select:not([data-empty]) { border-color: var(--accent); } diff --git a/web/html/js/views/list-detail.js b/web/html/js/views/list-detail.js index ad55536..8eaa3c4 100644 --- a/web/html/js/views/list-detail.js +++ b/web/html/js/views/list-detail.js @@ -33,8 +33,10 @@ export async function listDetailView( let banner = null; /** article_id -> { best_market_ids, best_cents, spread_cents, prices } */ let priceHints = new Map(); - // Zuletzt im Formular gewählter Markt - überlebt den Ansichtswechsel. + // Zuletzt im Formular gewählter Markt und Warengruppe - überleben + // den Ansichtswechsel. let lastMarket = null; + let lastCategory = null; // ------------------------------------------------------------------- // Laden @@ -64,6 +66,8 @@ export async function listDetailView( // cacheGet liefert den gespeicherten Wert unmittelbar; ein noch // nie gesetzter Schlüssel ergibt undefined. lastMarket = await db.cacheGet(`lastmarket:${listId}`).catch(() => null) || null; + lastCategory = + await db.cacheGet(`lastcategory:${listId}`).catch(() => null) || null; markets = (await db.cacheGet(`markets:${listId}`)) || []; categories = (await db.cacheGet(`categories:${listId}`)) || []; meta = await db.cacheGet(`meta:${listId}`); @@ -430,11 +434,13 @@ export async function listDetailView( : null; function markSelection() { - if (!marketField) return; // Leeres Attribut = keine Vorgabe; das Stylesheet hebt eine // getroffene Auswahl hervor. - if (marketField.value) marketField.removeAttribute("data-empty"); - else marketField.setAttribute("data-empty", ""); + for (const field of [marketField, categoryField]) { + if (!field) continue; + if (field.value) field.removeAttribute("data-empty"); + else field.setAttribute("data-empty", ""); + } } if (marketField && lastMarket @@ -443,11 +449,23 @@ export async function listDetailView( } markSelection(); - // Die Warengruppe bleibt im Formular unsichtbar: Sie kommt aus der - // Vorgabe des Artikels oder aus einem Vorschlag. Ein zweites - // Auswahlfeld würde die Zeile überladen, ohne oft gebraucht zu - // werden. - let chosenCategory = null; + const categoryField = categories.length + ? el("select.category-select", { + title: "Warengruppe für diesen Eintrag", + onchange: () => { + markSelection(); + db.cacheSet(`lastcategory:${listId}`, categoryField.value || null) + .catch(() => {}); + }, + }, + el("option", { value: "" }, "— Vorgabe des Artikels —"), + categories.map((c) => el("option", { value: c.id }, c.name))) + : null; + + if (categoryField && lastCategory + && categories.some((c) => c.id === lastCategory)) { + categoryField.value = lastCategory; + } const suggestionBox = el("ul.suggestions", { hidden: true }); @@ -469,12 +487,18 @@ export async function listDetailView( variantField.value = suggestion.variant || ""; // Zuletzt verwendeter Markt des Artikels - genau das, was man // beim Wiederholen eines Einkaufs erwartet. + // Zuletzt verwendeter Markt und zuletzt verwendete Warengruppe + // des Artikels - genau das, was man beim Wiederholen eines + // Einkaufs erwartet. if (marketField && suggestion.market_id && markets.some((m) => m.id === suggestion.market_id)) { marketField.value = suggestion.market_id; - markSelection(); } - chosenCategory = suggestion.category_id || null; + if (categoryField && suggestion.category_id + && categories.some((c) => c.id === suggestion.category_id)) { + categoryField.value = suggestion.category_id; + } + markSelection(); closeSuggestions(); // Zum Ändern einladen, nicht zum Abschicken drängen. qtyField.focus(); @@ -488,10 +512,9 @@ export async function listDetailView( clearTimeout(suggestTimer); const value = nameField.value.trim(); - // Eine frei getippte Eingabe hebt die aus einem Vorschlag - // übernommene Warengruppe auf. Der Markt bleibt stehen: Er ist - // jetzt sichtbar ausgewählt, und wer ihn nicht will, ändert ihn. - chosenCategory = null; + // Markt und Warengruppe bleiben stehen: Beide sind sichtbar + // ausgewählt, und wer sie nicht will, ändert sie. Sie + // stillschweigend zurückzusetzen wäre überraschender. if (value.length < 3 || !state.online) { closeSuggestions(); @@ -551,16 +574,18 @@ export async function listDetailView( variant: variantField.value.trim() || null, }; if (marketField && marketField.value) payload.market_id = marketField.value; - if (chosenCategory) payload.category_id = chosenCategory; + if (categoryField && categoryField.value) { + payload.category_id = categoryField.value; + } nameField.value = ""; countField.value = ""; qtyField.value = ""; unitField.value = ""; variantField.value = ""; - chosenCategory = null; - // Der Markt bleibt absichtlich stehen: Wer eine Liste füllt, - // trägt meist mehrere Artikel für denselben Laden ein. + // Markt und Warengruppe bleiben absichtlich stehen: Wer eine + // Liste füllt, trägt meist mehrere Artikel für denselben Laden + // und oft auch für dasselbe Regal ein. closeSuggestions(); await change("item.create", payload); @@ -599,6 +624,9 @@ export async function listDetailView( variant: variantField.value.trim() || null, }; if (marketField && marketField.value) extras.market_id = marketField.value; + if (categoryField && categoryField.value) { + extras.category_id = categoryField.value; + } // Bereits im eigenen Bestand: ohne Rückfrage auf die Liste. if (lookup.source === "catalog") { @@ -628,6 +656,8 @@ export async function listDetailView( variant: confirmed.variant || null, ...(marketField && marketField.value ? { market_id: marketField.value } : {}), + ...(categoryField && categoryField.value + ? { category_id: categoryField.value } : {}), }); } @@ -638,7 +668,8 @@ export async function listDetailView( return el("div.add-row", {}, el("div.name-wrap", {}, nameField, suggestionBox), el("div.add-fields", {}, countField, qtyField, unitField, variantField), - marketField ? el("div.add-market", {}, marketField) : null, + categoryField ? el("div.add-select", {}, categoryField) : null, + marketField ? el("div.add-select", {}, marketField) : null, el("div.add-actions", {}, cameraAvailable() ? el("button.secondary", { diff --git a/web/html/sw.js b/web/html/sw.js index 520cdca..1725f49 100644 --- a/web/html/sw.js +++ b/web/html/sw.js @@ -5,7 +5,7 @@ // Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet, // wann alte Zwischenspeicher verworfen werden. -const VERSION = "v29"; +const VERSION = "v30"; const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`; const SHELL = [