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
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.

View File

@@ -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); }

View File

@@ -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", {

View File

@@ -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 = [