// 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; }