Files
einkaufsapp/web/html/js/views/manage.js
2026-08-09 18:44:21 +02:00

185 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Märkte und Warengruppen pflegen.
//
// Die Reihenfolge wird gezogen, nicht getippt: `sort_order` verwaltet die
// App selbst und schreibt nach jedem Verschieben 10, 20, 30 … zurück. Die
// Abstände lassen Platz, falls später einmal einzeln eingefügt werden soll.
"use strict";
import { del, get, post, put } from "../api.js";
import { clear, el, mount } from "../dom.js";
import { makeSortable } from "../sortable.js";
import { set, state } from "../store.js";
import { appBar } from "../appbar.js";
const STEP = 10;
export async function manageView(root, { listId, back }) {
let banner = null;
let listName = "";
async function reload() {
const [markets, categories, meta] = await Promise.all([
get(`/api/lists/${listId}/markets`),
get(`/api/lists/${listId}/categories`),
get(`/api/lists/${listId}`),
]);
listName = meta.name;
set({ markets, categories });
}
function say(message, kind = "error") {
banner = { message, kind };
render();
setTimeout(() => { banner = null; render(); }, 6000);
}
async function guarded(fn, { silent = false } = {}) {
try {
await fn();
await reload();
render();
} catch (err) {
if (!silent) say(err.message);
await reload().catch(() => {});
render();
}
}
/** Schreibt die neue Reihenfolge zurück - nur die Einträge, deren Rang
* sich tatsächlich geändert hat. */
async function persistOrder(entries, endpoint, orderedIds) {
const byId = new Map(entries.map((e) => [e.id, e]));
const updates = [];
orderedIds.forEach((id, index) => {
const entry = byId.get(id);
const next = (index + 1) * STEP;
if (entry && entry.sort_order !== next) {
updates.push(put(`/api/lists/${listId}/${endpoint}/${entry.id}`, {
name: entry.name,
sort_order: next,
}));
}
});
if (!updates.length) return;
await guarded(() => Promise.all(updates));
}
function section({ title, entries, endpoint, hint, emptyText }) {
const nameField = el("input", {
type: "text",
maxLength: 120,
placeholder: "Name",
onkeydown: (ev) => { if (ev.key === "Enter") add(); },
});
function add() {
const name = nameField.value.trim();
if (!name) return;
guarded(async () => {
// Neues ans Ende: ein Schritt hinter dem letzten Rang.
const last = entries.length ? Math.max(...entries.map((e) => e.sort_order)) : 0;
await post(`/api/lists/${listId}/${endpoint}`, {
name,
sort_order: last + STEP,
});
nameField.value = "";
});
}
const list = el("ul.sortable", {});
for (const entry of entries) {
const handle = el("button.grip", {
type: "button",
title: "Zum Verschieben ziehen oder mit den Pfeiltasten bewegen",
"aria-label": `${entry.name} verschieben`,
}, "⠿");
const name = el("input.entry-name", {
type: "text",
value: entry.name,
maxLength: 120,
onchange: (ev) => {
const value = ev.target.value.trim();
if (!value) {
ev.target.value = entry.name;
return;
}
guarded(() => put(`/api/lists/${listId}/${endpoint}/${entry.id}`, {
name: value,
sort_order: entry.sort_order,
}));
},
});
const remove = el("button.danger.remove", {
type: "button",
title: `${entry.name} löschen`,
"aria-label": `${entry.name} löschen`,
onclick: () => {
if (!confirm(`${entry.name}“ löschen?`)) return;
guarded(() => del(`/api/lists/${listId}/${endpoint}/${entry.id}`));
},
}, "×");
list.append(el("li.sortable-row", { dataset: { id: entry.id } },
handle, name, remove));
}
if (entries.length > 1) {
makeSortable(list, {
handleSelector: ".grip",
itemSelector: ".sortable-row",
onReorder: (ids) => persistOrder(entries, endpoint, ids),
});
}
return el("section.card", {},
el("h2", {}, title),
hint ? el("p.lead", {}, hint) : null,
entries.length ? list : el("p.empty", {}, emptyText),
el("label", { htmlFor: `add-${endpoint}` }, "Hinzufügen"),
el("div.row", {},
Object.assign(nameField, { id: `add-${endpoint}` }),
el("button.primary", { type: "button", onclick: add }, "Anlegen"))
);
}
function render() {
mount(root,
appBar({ back: { label: "Zurück", onClick: back }, title: listName }),
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
section({
title: "Märkte",
entries: state.markets,
endpoint: "markets",
hint: "Die Reihenfolge bestimmt, in welcher Folge die Märkte in der " +
"Liste und im Ausdruck erscheinen. Am Anfasser ziehen, um sie " +
"zu ändern.",
emptyText: "Noch kein Markt angelegt.",
}),
section({
title: "Warengruppen",
entries: state.categories,
endpoint: "categories",
hint: "Innerhalb eines Marktes wird nach diesen Gruppen gegliedert; " +
"die Artikel darin stehen alphabetisch.",
emptyText: "Noch keine Warengruppe angelegt.",
}),
el("p.footnote", {},
"Ein gelöschter Markt nimmt keine Einträge mit sie landen in der " +
"Gruppe „Ohne Markt“.")
);
}
await reload();
render();
return render;
}