Direkte Eingabe der Warengruppe biem Erfassen ergänzt

This commit is contained in:
2026-08-10 11:00:21 +02:00
parent a2d911f7e3
commit a917c5f64e
4 changed files with 74 additions and 41 deletions

View File

@@ -1319,26 +1319,26 @@ Der Zeilenabstand der Eingabemaske kommt aus einer einzigen Regel
Namenszeile sie steckt seit der Vorschlagsliste in einem `.name-wrap` war Namenszeile sie steckt seit der Vorschlagsliste in einem `.name-wrap` war
eine solche Einzelregel wirkungslos geworden, ohne dass es auffiel. 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 Unter den Detailfeldern stehen zwei Auswahlfelder erst die Warengruppe, dann
nachbearbeitet werden beim Füllen einer Liste sind das schnell zwanzig der Markt. Ohne sie müsste jeder Eintrag nachbearbeitet werden; beim Füllen
Handgriffe. einer Liste sind das schnell zwanzig Handgriffe.
Zwei Eigenheiten: **Beide bleiben nach dem Hinzufügen stehen** und überleben den
**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, 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 **Die leere Auswahl heißt „Vorgabe des Artikels", nicht „ohne Markt".** Der
der Unterschied: Bei leerer Auswahl greift der Server auf den im Artikelstamm Unterschied ist wesentlich: Bei leerer Auswahl greift der Server auf die im
hinterlegten Standardmarkt zurück. Wer wirklich keinen Markt will, entfernt ihn Artikelstamm hinterlegte Vorgabe zurück. Wer wirklich keine Zuordnung will,
danach im „⋯"-Menü des Eintrags. entfernt sie danach im „⋯"-Menü des Eintrags.
Die Warengruppe bleibt im Formular unsichtbar. Sie kommt aus der Vorgabe des **Vorschläge belegen beide Felder vor** mit dem Markt und der Warengruppe,
Artikels oder aus einem Vorschlag; ein zweites Auswahlfeld würde die Zeile in denen der Artikel zuletzt stand. Tippst du den Namen danach frei um,
überladen, ohne oft gebraucht zu werden. 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. Sortiert nach Häufigkeit, dann nach Aktualität; höchstens acht Vorschläge.

View File

@@ -1560,9 +1560,10 @@ body.modal-open .app-footer { display: none; }
.app-footer { display: none !important; } .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%; width: 100%;
padding: .65rem .5rem; padding: .65rem .5rem;
font: inherit; font: inherit;
@@ -1572,6 +1573,7 @@ select.market-select {
border-radius: var(--radius); border-radius: var(--radius);
} }
/* Ist ein Markt gewählt, ist das die Voreinstellung für die nächsten /* Ist etwas gewählt, gilt es auch für die nächsten Artikel - das soll
Artikel - das soll man sehen, ohne hinzusehen. */ man sehen, ohne hinzusehen. */
select.market-select:not([data-empty]) { border-color: var(--accent); } select.market-select:not([data-empty]),
select.category-select:not([data-empty]) { border-color: var(--accent); }

View File

@@ -33,8 +33,10 @@ export async function listDetailView(
let banner = null; let banner = null;
/** article_id -> { best_market_ids, best_cents, spread_cents, prices } */ /** article_id -> { best_market_ids, best_cents, spread_cents, prices } */
let priceHints = new Map(); 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 lastMarket = null;
let lastCategory = null;
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Laden // Laden
@@ -64,6 +66,8 @@ export async function listDetailView(
// cacheGet liefert den gespeicherten Wert unmittelbar; ein noch // cacheGet liefert den gespeicherten Wert unmittelbar; ein noch
// nie gesetzter Schlüssel ergibt undefined. // nie gesetzter Schlüssel ergibt undefined.
lastMarket = await db.cacheGet(`lastmarket:${listId}`).catch(() => null) || null; lastMarket = await db.cacheGet(`lastmarket:${listId}`).catch(() => null) || null;
lastCategory =
await db.cacheGet(`lastcategory:${listId}`).catch(() => null) || null;
markets = (await db.cacheGet(`markets:${listId}`)) || []; markets = (await db.cacheGet(`markets:${listId}`)) || [];
categories = (await db.cacheGet(`categories:${listId}`)) || []; categories = (await db.cacheGet(`categories:${listId}`)) || [];
meta = await db.cacheGet(`meta:${listId}`); meta = await db.cacheGet(`meta:${listId}`);
@@ -430,11 +434,13 @@ export async function listDetailView(
: null; : null;
function markSelection() { function markSelection() {
if (!marketField) return;
// Leeres Attribut = keine Vorgabe; das Stylesheet hebt eine // Leeres Attribut = keine Vorgabe; das Stylesheet hebt eine
// getroffene Auswahl hervor. // getroffene Auswahl hervor.
if (marketField.value) marketField.removeAttribute("data-empty"); for (const field of [marketField, categoryField]) {
else marketField.setAttribute("data-empty", ""); if (!field) continue;
if (field.value) field.removeAttribute("data-empty");
else field.setAttribute("data-empty", "");
}
} }
if (marketField && lastMarket if (marketField && lastMarket
@@ -443,11 +449,23 @@ export async function listDetailView(
} }
markSelection(); markSelection();
// Die Warengruppe bleibt im Formular unsichtbar: Sie kommt aus der const categoryField = categories.length
// Vorgabe des Artikels oder aus einem Vorschlag. Ein zweites ? el("select.category-select", {
// Auswahlfeld würde die Zeile überladen, ohne oft gebraucht zu title: "Warengruppe für diesen Eintrag",
// werden. onchange: () => {
let chosenCategory = null; 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 }); const suggestionBox = el("ul.suggestions", { hidden: true });
@@ -469,12 +487,18 @@ export async function listDetailView(
variantField.value = suggestion.variant || ""; variantField.value = suggestion.variant || "";
// Zuletzt verwendeter Markt des Artikels - genau das, was man // Zuletzt verwendeter Markt des Artikels - genau das, was man
// beim Wiederholen eines Einkaufs erwartet. // 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 if (marketField && suggestion.market_id
&& markets.some((m) => m.id === suggestion.market_id)) { && markets.some((m) => m.id === suggestion.market_id)) {
marketField.value = 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(); closeSuggestions();
// Zum Ändern einladen, nicht zum Abschicken drängen. // Zum Ändern einladen, nicht zum Abschicken drängen.
qtyField.focus(); qtyField.focus();
@@ -488,10 +512,9 @@ export async function listDetailView(
clearTimeout(suggestTimer); clearTimeout(suggestTimer);
const value = nameField.value.trim(); const value = nameField.value.trim();
// Eine frei getippte Eingabe hebt die aus einem Vorschlag // Markt und Warengruppe bleiben stehen: Beide sind sichtbar
// übernommene Warengruppe auf. Der Markt bleibt stehen: Er ist // ausgewählt, und wer sie nicht will, ändert sie. Sie
// jetzt sichtbar ausgewählt, und wer ihn nicht will, ändert ihn. // stillschweigend zurückzusetzen wäre überraschender.
chosenCategory = null;
if (value.length < 3 || !state.online) { if (value.length < 3 || !state.online) {
closeSuggestions(); closeSuggestions();
@@ -551,16 +574,18 @@ export async function listDetailView(
variant: variantField.value.trim() || null, variant: variantField.value.trim() || null,
}; };
if (marketField && marketField.value) payload.market_id = marketField.value; 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 = ""; nameField.value = "";
countField.value = ""; countField.value = "";
qtyField.value = ""; qtyField.value = "";
unitField.value = ""; unitField.value = "";
variantField.value = ""; variantField.value = "";
chosenCategory = null; // Markt und Warengruppe bleiben absichtlich stehen: Wer eine
// Der Markt bleibt absichtlich stehen: Wer eine Liste füllt, // Liste füllt, trägt meist mehrere Artikel für denselben Laden
// trägt meist mehrere Artikel für denselben Laden ein. // und oft auch für dasselbe Regal ein.
closeSuggestions(); closeSuggestions();
await change("item.create", payload); await change("item.create", payload);
@@ -599,6 +624,9 @@ export async function listDetailView(
variant: variantField.value.trim() || null, variant: variantField.value.trim() || null,
}; };
if (marketField && marketField.value) extras.market_id = marketField.value; 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. // Bereits im eigenen Bestand: ohne Rückfrage auf die Liste.
if (lookup.source === "catalog") { if (lookup.source === "catalog") {
@@ -628,6 +656,8 @@ export async function listDetailView(
variant: confirmed.variant || null, variant: confirmed.variant || null,
...(marketField && marketField.value ...(marketField && marketField.value
? { market_id: 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", {}, return el("div.add-row", {},
el("div.name-wrap", {}, nameField, suggestionBox), el("div.name-wrap", {}, nameField, suggestionBox),
el("div.add-fields", {}, countField, qtyField, unitField, variantField), 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", {}, el("div.add-actions", {},
cameraAvailable() cameraAvailable()
? el("button.secondary", { ? el("button.secondary", {

View File

@@ -5,7 +5,7 @@
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet, // Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
// wann alte Zwischenspeicher verworfen werden. // wann alte Zwischenspeicher verworfen werden.
const VERSION = "v29"; const VERSION = "v30";
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`; const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
const SHELL = [ const SHELL = [