Angabe des Markts direkt bei der Artikelanlage
This commit is contained in:
23
README.md
23
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
|
einmal gekauft hat, hilft dagegen nicht mehr und würde die Liste nur
|
||||||
verlängern.
|
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
|
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
|
nicht ein.** Der Fokus springt ins Gebinde-Feld, damit noch Änderungen möglich
|
||||||
sind. Erst „Hinzufügen" oder Enter übernimmt.
|
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.
|
Sortiert nach Häufigkeit, dann nach Aktualität; höchstens acht Vorschläge.
|
||||||
|
|
||||||
`GET /api/lists/{id}/articles/suggest?q=…`
|
`GET /api/lists/{id}/articles/suggest?q=…`
|
||||||
|
|||||||
@@ -1556,3 +1556,21 @@ body.modal-open .app-footer { display: none; }
|
|||||||
Einkaufsliste zeigen, nicht die Fußzeile der Anwendung. */
|
Einkaufsliste zeigen, nicht die Fußzeile der Anwendung. */
|
||||||
.app-footer { display: none !important; }
|
.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); }
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ 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.
|
||||||
|
let lastMarket = null;
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Laden
|
// Laden
|
||||||
@@ -59,6 +61,9 @@ export async function listDetailView(
|
|||||||
|
|
||||||
async function readLocal() {
|
async function readLocal() {
|
||||||
view = await composeView(listId);
|
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}`)) || [];
|
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}`);
|
||||||
@@ -402,10 +407,46 @@ export async function listDetailView(
|
|||||||
onkeydown: (ev) => { if (ev.key === "Enter") addItem(); },
|
onkeydown: (ev) => { if (ev.key === "Enter") addItem(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vorgeschlagener Markt und Warengruppe: werden mit übernommen,
|
// Marktauswahl direkt im Formular: Ohne sie müsste jeder Eintrag
|
||||||
// sind im Formular aber nicht sichtbar - sie stehen im Menü des
|
// nachbearbeitet werden, und beim Füllen einer Liste sind das
|
||||||
// Eintrags und würden die Eingabezeile überladen.
|
// schnell zwanzig Handgriffe.
|
||||||
let chosenMarket = null;
|
//
|
||||||
|
// 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;
|
let chosenCategory = null;
|
||||||
|
|
||||||
const suggestionBox = el("ul.suggestions", { hidden: true });
|
const suggestionBox = el("ul.suggestions", { hidden: true });
|
||||||
@@ -426,7 +467,13 @@ export async function listDetailView(
|
|||||||
? formatQuantity(suggestion.pack_size) : "";
|
? formatQuantity(suggestion.pack_size) : "";
|
||||||
unitField.value = suggestion.pack_unit || "";
|
unitField.value = suggestion.pack_unit || "";
|
||||||
variantField.value = suggestion.variant || "";
|
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;
|
chosenCategory = suggestion.category_id || null;
|
||||||
closeSuggestions();
|
closeSuggestions();
|
||||||
// Zum Ändern einladen, nicht zum Abschicken drängen.
|
// Zum Ändern einladen, nicht zum Abschicken drängen.
|
||||||
@@ -441,10 +488,9 @@ export async function listDetailView(
|
|||||||
clearTimeout(suggestTimer);
|
clearTimeout(suggestTimer);
|
||||||
const value = nameField.value.trim();
|
const value = nameField.value.trim();
|
||||||
|
|
||||||
// Eine frei getippte Eingabe hebt eine zuvor übernommene
|
// Eine frei getippte Eingabe hebt die aus einem Vorschlag
|
||||||
// Zuordnung auf - sonst landete der neue Artikel im Markt des
|
// übernommene Warengruppe auf. Der Markt bleibt stehen: Er ist
|
||||||
// alten Vorschlags.
|
// jetzt sichtbar ausgewählt, und wer ihn nicht will, ändert ihn.
|
||||||
chosenMarket = null;
|
|
||||||
chosenCategory = null;
|
chosenCategory = null;
|
||||||
|
|
||||||
if (value.length < 3 || !state.online) {
|
if (value.length < 3 || !state.online) {
|
||||||
@@ -504,7 +550,7 @@ export async function listDetailView(
|
|||||||
pack_unit: unitField.value.trim() || null,
|
pack_unit: unitField.value.trim() || null,
|
||||||
variant: variantField.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;
|
if (chosenCategory) payload.category_id = chosenCategory;
|
||||||
|
|
||||||
nameField.value = "";
|
nameField.value = "";
|
||||||
@@ -512,8 +558,9 @@ export async function listDetailView(
|
|||||||
qtyField.value = "";
|
qtyField.value = "";
|
||||||
unitField.value = "";
|
unitField.value = "";
|
||||||
variantField.value = "";
|
variantField.value = "";
|
||||||
chosenMarket = null;
|
|
||||||
chosenCategory = null;
|
chosenCategory = null;
|
||||||
|
// Der Markt bleibt absichtlich stehen: Wer eine Liste füllt,
|
||||||
|
// trägt meist mehrere Artikel für denselben Laden ein.
|
||||||
closeSuggestions();
|
closeSuggestions();
|
||||||
|
|
||||||
await change("item.create", payload);
|
await change("item.create", payload);
|
||||||
@@ -551,6 +598,7 @@ export async function listDetailView(
|
|||||||
pack_unit: unitField.value.trim() || null,
|
pack_unit: unitField.value.trim() || null,
|
||||||
variant: variantField.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.
|
// Bereits im eigenen Bestand: ohne Rückfrage auf die Liste.
|
||||||
if (lookup.source === "catalog") {
|
if (lookup.source === "catalog") {
|
||||||
@@ -578,6 +626,8 @@ export async function listDetailView(
|
|||||||
pack_size: parseQuantity(confirmed.packSize),
|
pack_size: parseQuantity(confirmed.packSize),
|
||||||
pack_unit: confirmed.packUnit || null,
|
pack_unit: confirmed.packUnit || null,
|
||||||
variant: confirmed.variant || 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", {},
|
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,
|
||||||
el("div.add-actions", {},
|
el("div.add-actions", {},
|
||||||
cameraAvailable()
|
cameraAvailable()
|
||||||
? el("button.secondary", {
|
? el("button.secondary", {
|
||||||
|
|||||||
@@ -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 = "v27";
|
const VERSION = "v28";
|
||||||
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
||||||
|
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
|
|||||||
Reference in New Issue
Block a user