// Preisvergleich zwischen Märkten. // // Bewusst keine Matrix Artikel × Markt: Bei vier Märkten und dreißig // Artikeln wird die auf einem Telefon unlesbar. Stattdessen je Artikel // eine Karte mit den Märkten nach Preis sortiert - man sucht ohnehin // "wo ist X am günstigsten", nicht "wie sieht die ganze Tabelle aus". "use strict"; import { get } from "../api.js"; import { el, euro, formatQuantity, mount } from "../dom.js"; import { appBar } from "../appbar.js"; let expandedId = null; function formatDate(iso) { return new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit", }); } /** Preis je Einheit, geliefert in Zehntelcent. */ function unitPrice(deci, unit) { if (!deci || !unit) return null; return `${euro(deci / 10)} je ${unit}`; } export async function pricesView(root, { listId, back }) { let data = null; let banner = null; let detail = null; async function reload() { data = await get(`/api/lists/${listId}/prices`); } function say(message) { banner = message; render(); setTimeout(() => { banner = null; render(); }, 6000); } async function toggle(articleId) { if (expandedId === articleId) { expandedId = null; detail = null; render(); return; } expandedId = articleId; detail = null; render(); try { detail = await get(`/api/lists/${listId}/articles/${articleId}/prices`); render(); } catch (err) { say(err.message); } } function marketRow(row, marketId, cents, isBest) { return el("div", { className: `price-row${isBest ? " best" : ""}` }, el("span.market", {}, data.market_names[marketId] || "?"), el("span.amount", {}, euro(cents)), isBest && row.spread_cents > 0 ? el("span.badge", {}, "günstigster") : row.spread_cents > 0 ? el("span.diff", {}, `+${euro(cents - row.best_cents)}`) : null ); } function detailBlock() { if (!detail) return el("p.loading", {}, "Wird geladen …"); return el("div.price-detail", {}, el("h4", {}, "Beobachtungen je Markt"), el("ul.price-markets", {}, detail.markets.map((m) => el("li", {}, el("span.market", {}, m.market_name), el("span.sub", {}, m.latest_pack_size ? `${formatQuantity(m.latest_pack_size)} ${m.latest_pack_unit || ""} · ` : "", unitPrice(m.unit_price_deci, m.latest_pack_unit) ? `${unitPrice(m.unit_price_deci, m.latest_pack_unit)} · ` : "", m.observations === 1 ? "einmal erfasst" : `${m.observations}× erfasst, ${euro(m.min_cents)}–${euro(m.max_cents)}`, ` · zuletzt ${formatDate(m.latest_at)}`), el("span.amount", {}, euro(m.latest_cents))))), detail.history.length > 1 ? el("details.price-history", {}, el("summary", {}, `Verlauf (${detail.history.length} Einträge)`), el("ul", {}, detail.history.map((h) => el("li", {}, `${formatDate(h.recorded_at)} · ${h.market_name} · ${euro(h.price_cents)}`, h.pack_size ? ` (${formatQuantity(h.pack_size)} ${h.pack_unit || ""})` : "")))) : null ); } function articleCard(row) { const entries = Object.entries(row.prices).sort((a, b) => a[1] - b[1]); return el("section.card.price-card", {}, el("button.price-head", { type: "button", onclick: () => toggle(row.article_id), "aria-expanded": expandedId === row.article_id ? "true" : "false", }, el("span.name", {}, row.article_name), row.spread_cents > 0 ? el("span.spread", {}, `bis ${euro(row.spread_cents)} Unterschied`) : el("span.spread", {}, "überall gleich")), el("div.price-rows", {}, entries.map(([marketId, cents]) => marketRow(row, marketId, cents, row.best_market_ids.includes(marketId)))), expandedId === row.article_id ? detailBlock() : null ); } function render() { const withSpread = data.rows.filter((r) => r.spread_cents > 0); const savings = withSpread.reduce((sum, r) => sum + r.spread_cents, 0); mount(root, appBar({ back: { label: "Zurück", onClick: back }, title: "Preise" }), banner ? el("p.error", {}, banner) : null, el("section.card", {}, el("h1", {}, "Preise"), data.rows.length ? el("p.lead", {}, `${data.rows.length} Artikel mit erfassten Preisen. `, withSpread.length ? `Bei ${withSpread.length} davon unterscheiden sich die Märkte – ` + `zusammen ${euro(savings)} Unterschied, wenn jeder Artikel ` + `im günstigsten Markt gekauft würde.` : "Bislang gibt es keine Preisunterschiede zwischen den Märkten.") : el("p.empty", {}, "Noch keine Preise erfasst. Trag beim Einkaufen Preise an den " + "Artikeln ein – sobald ein Eintrag einem Markt zugeordnet ist, " + "wird der Preis hier gesammelt.")), data.rows.map(articleCard), data.rows.length ? el("p.footnote", {}, "Erfasst werden Markt, Artikel, Betrag und Zeitpunkt – ohne " + "Angabe, wer den Preis eingetragen hat.") : null ); } await reload(); render(); return render; }