Files
einkaufsapp/web/html/js/views/public.js
2026-08-08 20:31:58 +02:00

144 lines
4.4 KiB
JavaScript
Raw Permalink 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.
// Öffentliche Ansicht einer Liste über /s/<token>. Kein Konto nötig.
// Nur ansehen und - sofern erlaubt - abhaken. Auf Druck ausgelegt.
"use strict";
import { get, post } from "../api.js";
import { clear, el, euro, formatPack, mount } from "../dom.js";
function formatDate(iso) {
return new Date(iso).toLocaleDateString("de-DE", {
day: "2-digit", month: "2-digit", year: "numeric",
});
}
export async function publicView(root, { token }) {
let view = null;
let banner = null;
async function load() {
view = await get(`/api/public/${encodeURIComponent(token)}`);
}
function say(message, kind = "error") {
banner = { message, kind };
render();
setTimeout(() => { banner = null; render(); }, 6000);
}
function itemRow(item) {
const bought = item.status === "bought";
const check = view.allow_check
? el("button.check", {
type: "button",
"aria-pressed": bought ? "true" : "false",
title: bought ? "Als offen markieren" : "Als gekauft markieren",
onclick: async () => {
const next = bought ? "open" : "bought";
item.status = next; // sofort anzeigen
render();
try {
await post(`/api/public/${encodeURIComponent(token)}/items/${item.id}`,
{ status: next });
await load();
} catch (err) {
await load().catch(() => {});
say(err.message);
}
render();
},
}, bought ? "✓" : "")
: el("span.check.readonly", { "aria-hidden": "true" }, bought ? "✓" : "");
const pack = formatPack(item.pack_size, item.pack_unit);
return el("li", { className: `item${bought ? " bought" : ""}` },
el("div.item-main", {},
check,
el("span", {
className: `item-count${item.count > 1 ? " many" : ""}`,
}, `${item.count || 1}\u00d7`),
el("div.item-text", {},
el("span.name", {}, item.article_name),
pack || item.variant || item.note
? el("span.sub", {},
[pack, item.variant, item.note].filter(Boolean).join(" · "))
: null),
item.price_cents
? el("span.price-static", {},
euro(item.total_cents ?? item.price_cents))
: null
)
);
}
function render() {
mount(root,
el("header.public-head", {},
el("h1", {}, view.list_name),
el("p.sub", {},
"Geteilte Ansicht",
view.allow_check ? " du kannst Artikel abhaken." : " nur zum Ansehen.",
` Gültig bis ${formatDate(view.expires_at)}.`)
),
banner ? el("p.error", {}, banner.message) : null,
view.markets.length
? view.markets.map((market) =>
el("section.market", {},
el("h2", {},
el("span", {}, market.market_name),
el("span.market-meta", {},
market.open_count === 1 ? "1 offen" : `${market.open_count} offen`,
market.total_cents ? ` · ${euro(market.total_cents)}` : "")),
market.categories.map((cat) =>
el("div.category", {},
el("h3", {}, cat.category_name),
el("ul.items", {}, cat.items.map(itemRow))))))
: el("p.empty", {}, "Die Liste ist leer."),
el("footer.summary", {},
el("div.total", {},
el("span", {}, "Gesamt"),
el("strong", {}, euro(view.grand_total_cents)))),
el("p.footnote.no-print", {},
// Eigene Druckseite statt window.print(): Sie ist auf A4
// ausgelegt und funktioniert auch, wenn der Empfänger die
// Ansicht nur weiterleiten will.
el("a.linklike", {
href: `/api/public/${encodeURIComponent(token)}/print`,
target: "_blank",
rel: "noopener",
}, "Drucken"),
" · ",
el("button.linklike", {
type: "button",
onclick: async () => {
try {
await load();
render();
} catch (err) {
say(err.message);
}
},
}, "Aktualisieren"))
);
}
try {
await load();
} catch (err) {
mount(root,
el("section.card", {},
el("h1", {}, "Liste nicht verfügbar"),
el("p.error", {}, err.message),
el("p.lead", {},
"Bitte die Person, die den Link geteilt hat, um einen neuen."))
);
return;
}
render();
}