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

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