From c6d70237795e731d7b90026d790e04a2d8d18857 Mon Sep 17 00:00:00 2001 From: Marco Morath Date: Mon, 10 Aug 2026 09:33:23 +0200 Subject: [PATCH] Angabe des Markts direkt bei der Artikelanlage --- README.md | 23 +++++++++- web/html/app.css | 18 ++++++++ web/html/js/views/list-detail.js | 73 +++++++++++++++++++++++++++----- web/html/sw.js | 2 +- 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f184c3f..c94ed41 100644 --- a/README.md +++ b/README.md @@ -1309,11 +1309,32 @@ abgehakten und aufgeräumten sind die interessanten. Was jemand vor drei Jahren einmal gekauft hat, hilft dagegen nicht mehr und würde die Liste nur verlängern. -Ein Antippen füllt Name, Anzahl, Gebinde, Eigenschaft sowie Markt und +Ein Antippen füllt Name, Anzahl, Gebinde, Eigenschaft, **Markt** und Warengruppe mit den zuletzt verwendeten Werten – **trägt den Artikel aber noch nicht ein.** Der Fokus springt ins Gebinde-Feld, damit noch Änderungen möglich sind. Erst „Hinzufügen" oder Enter übernimmt. +### 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. + +Zwei Eigenheiten: + +**Der gewählte Markt bleibt nach dem Hinzufügen stehen** und überlebt auch den +Ansichtswechsel (gespeichert je Liste in IndexedDB). Wer eine Liste füllt, +trägt meist mehrere Artikel für denselben Laden ein. + +**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 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. + Sortiert nach Häufigkeit, dann nach Aktualität; höchstens acht Vorschläge. `GET /api/lists/{id}/articles/suggest?q=…` diff --git a/web/html/app.css b/web/html/app.css index b7fbcf3..ea68fd4 100644 --- a/web/html/app.css +++ b/web/html/app.css @@ -1556,3 +1556,21 @@ body.modal-open .app-footer { display: none; } Einkaufsliste zeigen, nicht die Fußzeile der Anwendung. */ .app-footer { display: none !important; } } + +/* --- Marktauswahl in der Eingabezeile --- */ + +.add-market { margin-bottom: .5rem; } + +select.market-select { + width: 100%; + padding: .65rem .5rem; + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--line); + 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); } diff --git a/web/html/js/views/list-detail.js b/web/html/js/views/list-detail.js index d53f2c9..ad55536 100644 --- a/web/html/js/views/list-detail.js +++ b/web/html/js/views/list-detail.js @@ -33,6 +33,8 @@ 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. + let lastMarket = null; // ------------------------------------------------------------------- // Laden @@ -59,6 +61,9 @@ export async function listDetailView( async function readLocal() { view = await composeView(listId); + // cacheGet liefert den gespeicherten Wert unmittelbar; ein noch + // nie gesetzter Schlüssel ergibt undefined. + lastMarket = await db.cacheGet(`lastmarket:${listId}`).catch(() => null) || null; markets = (await db.cacheGet(`markets:${listId}`)) || []; categories = (await db.cacheGet(`categories:${listId}`)) || []; meta = await db.cacheGet(`meta:${listId}`); @@ -402,10 +407,46 @@ export async function listDetailView( onkeydown: (ev) => { if (ev.key === "Enter") addItem(); }, }); - // Vorgeschlagener Markt und Warengruppe: werden mit übernommen, - // sind im Formular aber nicht sichtbar - sie stehen im Menü des - // Eintrags und würden die Eingabezeile überladen. - let chosenMarket = null; + // Marktauswahl direkt im Formular: Ohne sie müsste jeder Eintrag + // nachbearbeitet werden, und beim Füllen einer Liste sind das + // schnell zwanzig Handgriffe. + // + // Die leere Auswahl heißt bewusst "Vorgabe des Artikels" und nicht + // "ohne Markt": Der Server greift in diesem Fall auf die im + // Artikelstamm hinterlegte Vorgabe zurück. + const marketField = markets.length + ? el("select.market-select", { + title: "Markt für diesen Eintrag", + onchange: () => { + markSelection(); + // Merken, damit die nächsten Artikel derselben Runde nicht + // erneut ausgewählt werden müssen. + db.cacheSet(`lastmarket:${listId}`, marketField.value || null) + .catch(() => {}); + }, + }, + el("option", { value: "" }, "— Vorgabe des Artikels —"), + markets.map((m) => el("option", { value: m.id }, m.name))) + : 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", ""); + } + + if (marketField && lastMarket + && markets.some((m) => m.id === lastMarket)) { + marketField.value = lastMarket; + } + 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 suggestionBox = el("ul.suggestions", { hidden: true }); @@ -426,7 +467,13 @@ export async function listDetailView( ? formatQuantity(suggestion.pack_size) : ""; unitField.value = suggestion.pack_unit || ""; variantField.value = suggestion.variant || ""; - chosenMarket = suggestion.market_id || null; + // Zuletzt verwendeter Markt 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; closeSuggestions(); // Zum Ändern einladen, nicht zum Abschicken drängen. @@ -441,10 +488,9 @@ export async function listDetailView( clearTimeout(suggestTimer); const value = nameField.value.trim(); - // Eine frei getippte Eingabe hebt eine zuvor übernommene - // Zuordnung auf - sonst landete der neue Artikel im Markt des - // alten Vorschlags. - chosenMarket = null; + // 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; if (value.length < 3 || !state.online) { @@ -504,7 +550,7 @@ export async function listDetailView( pack_unit: unitField.value.trim() || null, variant: variantField.value.trim() || null, }; - if (chosenMarket) payload.market_id = chosenMarket; + if (marketField && marketField.value) payload.market_id = marketField.value; if (chosenCategory) payload.category_id = chosenCategory; nameField.value = ""; @@ -512,8 +558,9 @@ export async function listDetailView( qtyField.value = ""; unitField.value = ""; variantField.value = ""; - chosenMarket = null; chosenCategory = null; + // Der Markt bleibt absichtlich stehen: Wer eine Liste füllt, + // trägt meist mehrere Artikel für denselben Laden ein. closeSuggestions(); await change("item.create", payload); @@ -551,6 +598,7 @@ export async function listDetailView( pack_unit: unitField.value.trim() || null, variant: variantField.value.trim() || null, }; + if (marketField && marketField.value) extras.market_id = marketField.value; // Bereits im eigenen Bestand: ohne Rückfrage auf die Liste. if (lookup.source === "catalog") { @@ -578,6 +626,8 @@ export async function listDetailView( pack_size: parseQuantity(confirmed.packSize), pack_unit: confirmed.packUnit || null, variant: confirmed.variant || null, + ...(marketField && marketField.value + ? { market_id: marketField.value } : {}), }); } @@ -588,6 +638,7 @@ 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, el("div.add-actions", {}, cameraAvailable() ? el("button.secondary", { diff --git a/web/html/sw.js b/web/html/sw.js index 39b8f40..7148066 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 = "v27"; +const VERSION = "v28"; const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`; const SHELL = [