Angabe des Markts direkt bei der Artikelanlage

This commit is contained in:
2026-08-10 09:33:23 +02:00
parent 777e3e5447
commit c6d7023779
4 changed files with 103 additions and 13 deletions

View File

@@ -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=…`

View File

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

View File

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

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 = "v27";
const VERSION = "v28";
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
const SHELL = [