Erste Produktivversion
This commit is contained in:
186
web/html/js/views/manage.js
Normal file
186
web/html/js/views/manage.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user