187 lines
5.5 KiB
JavaScript
187 lines
5.5 KiB
JavaScript
// 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";
|
||
|
||
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,
|
||
el("header.bar", {},
|
||
el("button.linklike", { type: "button", onclick: back },
|
||
"‹ Zurück zur Liste"),
|
||
listName ? el("span.who", {}, listName) : null),
|
||
|
||
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;
|
||
}
|