neue Archivfunktion und Optimierung an Artikelbearbeitung
This commit is contained in:
@@ -220,6 +220,22 @@ export async function listDetailView(
|
||||
|
||||
const deferred = item.status === "deferred";
|
||||
|
||||
const nameEdit = el("input", {
|
||||
type: "text",
|
||||
value: item.article_name,
|
||||
maxLength: 200,
|
||||
onchange: (ev) => {
|
||||
const value = ev.target.value.trim();
|
||||
if (!value) {
|
||||
// Leeres Feld ist kein Löschbefehl - der alte Name bleibt.
|
||||
ev.target.value = item.article_name;
|
||||
return;
|
||||
}
|
||||
if (value === item.article_name) return;
|
||||
change("item.update", { item_id: item.id, article_name: value });
|
||||
},
|
||||
});
|
||||
|
||||
const variantField = el("input", {
|
||||
type: "text",
|
||||
value: item.variant || "",
|
||||
@@ -293,6 +309,12 @@ export async function listDetailView(
|
||||
|
||||
return el("div.item-menu", {},
|
||||
hintBlock,
|
||||
el("label", {}, "Artikel"),
|
||||
nameEdit,
|
||||
el("p.sub", {},
|
||||
"Steht der Artikel nur auf diesem Eintrag, wird er umbenannt – " +
|
||||
"ein Tippfehler verschwindet damit ganz. Wird er auch anderswo " +
|
||||
"verwendet, bekommt nur dieser Eintrag den neuen Artikel."),
|
||||
el("label", {}, "Anzahl und Gebinde",
|
||||
el("span.hint", {}, " (Preis gilt je Gebinde)")),
|
||||
el("div.add-fields", {}, countEdit, packSizeEdit, packUnitEdit),
|
||||
@@ -342,8 +364,16 @@ export async function listDetailView(
|
||||
maxLength: 200,
|
||||
placeholder: "Artikel hinzufügen",
|
||||
autocomplete: "off",
|
||||
onkeydown: (ev) => { if (ev.key === "Enter") addItem(); },
|
||||
// Der eingebaute Vervollständiger würde die eigene Vorschlagsliste
|
||||
// überdecken.
|
||||
autocapitalize: "sentences",
|
||||
onkeydown: (ev) => {
|
||||
if (ev.key === "Enter") { closeSuggestions(); addItem(); }
|
||||
if (ev.key === "Escape") closeSuggestions();
|
||||
},
|
||||
oninput: () => scheduleSuggestions(),
|
||||
});
|
||||
|
||||
// Anzahl und Gebinde getrennt: Der Preis gilt je Gebinde, die
|
||||
// Summe ist Preis mal Anzahl. Steckte beides in einem Feld, ergaben
|
||||
// 500 ml zu 2,99 EUR eine Summe von 1495 EUR.
|
||||
@@ -362,33 +392,106 @@ export async function listDetailView(
|
||||
title: "z. B. ml, g, Stk",
|
||||
onkeydown: (ev) => { if (ev.key === "Enter") addItem(); },
|
||||
});
|
||||
|
||||
// Vorschläge aus dem, was auf dieser Liste schon vorkommt - so muss
|
||||
// "bunt, ganz" nur einmal getippt werden.
|
||||
const suggestions = new Set();
|
||||
for (const market of view?.markets || []) {
|
||||
for (const cat of market.categories) {
|
||||
for (const item of cat.items) {
|
||||
if (item.variant) suggestions.add(item.variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
const variantList = el("datalist", { id: "variant-suggestions" },
|
||||
[...suggestions].sort((a, b) => a.localeCompare(b, "de"))
|
||||
.map((v) => el("option", { value: v })));
|
||||
|
||||
const variantField = el("input.variant", {
|
||||
type: "text",
|
||||
maxLength: 200,
|
||||
placeholder: "Eigenschaft",
|
||||
title: "Nähere Bestimmung, z. B. „bunt, ganz“ oder „laktosefrei“",
|
||||
autocomplete: "off",
|
||||
list: "variant-suggestions",
|
||||
onkeydown: (ev) => { if (ev.key === "Enter") addItem(); },
|
||||
});
|
||||
// el() setzt "list" nicht als Eigenschaft - das Attribut muss direkt
|
||||
// gesetzt werden, sonst findet der Browser die Vorschlagsliste nicht.
|
||||
variantField.setAttribute("list", "variant-suggestions");
|
||||
|
||||
// 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;
|
||||
let chosenCategory = null;
|
||||
|
||||
const suggestionBox = el("ul.suggestions", { hidden: true });
|
||||
|
||||
function closeSuggestions() {
|
||||
suggestionBox.hidden = true;
|
||||
suggestionBox.replaceChildren();
|
||||
}
|
||||
|
||||
/** Übernimmt einen Vorschlag in die Felder - aber trägt ihn NICHT
|
||||
* ein. So bleibt Gelegenheit, Menge oder Eigenschaft zu ändern,
|
||||
* bevor der Artikel auf der Liste landet. */
|
||||
function applySuggestion(suggestion) {
|
||||
nameField.value = suggestion.name;
|
||||
countField.value = suggestion.count && suggestion.count > 1
|
||||
? String(suggestion.count) : "";
|
||||
qtyField.value = suggestion.pack_size
|
||||
? formatQuantity(suggestion.pack_size) : "";
|
||||
unitField.value = suggestion.pack_unit || "";
|
||||
variantField.value = suggestion.variant || "";
|
||||
chosenMarket = suggestion.market_id || null;
|
||||
chosenCategory = suggestion.category_id || null;
|
||||
closeSuggestions();
|
||||
// Zum Ändern einladen, nicht zum Abschicken drängen.
|
||||
qtyField.focus();
|
||||
}
|
||||
|
||||
let suggestTimer = null;
|
||||
let suggestToken = 0;
|
||||
|
||||
function scheduleSuggestions() {
|
||||
// Getippte Zeichen sammeln, statt bei jedem Anschlag zu fragen.
|
||||
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;
|
||||
chosenCategory = null;
|
||||
|
||||
if (value.length < 3 || !state.online) {
|
||||
closeSuggestions();
|
||||
return;
|
||||
}
|
||||
suggestTimer = setTimeout(() => loadSuggestions(value), 220);
|
||||
}
|
||||
|
||||
async function loadSuggestions(value) {
|
||||
const token = ++suggestToken;
|
||||
let rows;
|
||||
try {
|
||||
rows = await get(
|
||||
`/api/lists/${listId}/articles/suggest?q=${encodeURIComponent(value)}`);
|
||||
} catch {
|
||||
// Vorschläge sind Beiwerk: Wenn sie nicht kommen, tippt man
|
||||
// eben weiter.
|
||||
return;
|
||||
}
|
||||
// Antwort einer älteren Anfrage verwerfen.
|
||||
if (token !== suggestToken) return;
|
||||
if (!rows.length) {
|
||||
closeSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionBox.replaceChildren(...rows.map((row) => {
|
||||
const details = [
|
||||
row.count && row.count > 1 ? `${row.count}×` : null,
|
||||
formatPack(row.pack_size, row.pack_unit) || null,
|
||||
row.variant,
|
||||
markets.find((m) => m.id === row.market_id)?.name,
|
||||
].filter(Boolean);
|
||||
|
||||
return el("li", {},
|
||||
el("button", {
|
||||
type: "button",
|
||||
// mousedown statt click: Ein Klick käme erst nach dem
|
||||
// blur des Eingabefelds - und das schließt die Liste.
|
||||
onmousedown: (ev) => { ev.preventDefault(); applySuggestion(row); },
|
||||
onclick: () => applySuggestion(row),
|
||||
},
|
||||
el("span.name", {}, row.name),
|
||||
details.length ? el("span.sub", {}, details.join(" · ")) : null));
|
||||
}));
|
||||
suggestionBox.hidden = false;
|
||||
}
|
||||
|
||||
async function addItem() {
|
||||
const name = nameField.value.trim();
|
||||
@@ -400,11 +503,18 @@ export async function listDetailView(
|
||||
pack_unit: unitField.value.trim() || null,
|
||||
variant: variantField.value.trim() || null,
|
||||
};
|
||||
if (chosenMarket) payload.market_id = chosenMarket;
|
||||
if (chosenCategory) payload.category_id = chosenCategory;
|
||||
|
||||
nameField.value = "";
|
||||
countField.value = "";
|
||||
qtyField.value = "";
|
||||
unitField.value = "";
|
||||
variantField.value = "";
|
||||
chosenMarket = null;
|
||||
chosenCategory = null;
|
||||
closeSuggestions();
|
||||
|
||||
await change("item.create", payload);
|
||||
document.getElementById("add-name")?.focus();
|
||||
}
|
||||
@@ -434,15 +544,17 @@ export async function listDetailView(
|
||||
return;
|
||||
}
|
||||
|
||||
const extras = {
|
||||
count: parseCount(countField.value),
|
||||
pack_size: parseQuantity(qtyField.value),
|
||||
pack_unit: unitField.value.trim() || null,
|
||||
variant: variantField.value.trim() || null,
|
||||
};
|
||||
|
||||
// Bereits im eigenen Bestand: ohne Rückfrage auf die Liste.
|
||||
if (lookup.source === "catalog") {
|
||||
await change("item.create", {
|
||||
article_id: lookup.article_id,
|
||||
barcode: code,
|
||||
count: parseCount(countField.value),
|
||||
pack_size: parseQuantity(qtyField.value),
|
||||
pack_unit: unitField.value.trim() || null,
|
||||
variant: variantField.value.trim() || null,
|
||||
article_id: lookup.article_id, barcode: code, ...extras,
|
||||
});
|
||||
countField.value = "";
|
||||
qtyField.value = "";
|
||||
@@ -473,7 +585,7 @@ export async function listDetailView(
|
||||
// Schaltflächen in einer Zeile - auf schmalen Geräten lief der
|
||||
// letzte Knopf aus der Box.
|
||||
return el("div.add-row", {},
|
||||
nameField,
|
||||
el("div.name-wrap", {}, nameField, suggestionBox),
|
||||
el("div.add-fields", {}, countField, qtyField, unitField, variantField),
|
||||
el("div.add-actions", {},
|
||||
cameraAvailable()
|
||||
@@ -483,8 +595,7 @@ export async function listDetailView(
|
||||
onclick: scanAndAdd,
|
||||
}, "Scannen")
|
||||
: null,
|
||||
el("button.primary", { type: "button", onclick: addItem }, "Hinzufügen")),
|
||||
variantList
|
||||
el("button.primary", { type: "button", onclick: addItem }, "Hinzufügen"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user