60 lines
4.0 KiB
JavaScript
60 lines
4.0 KiB
JavaScript
export function validateCatalog(value) {
|
|
if (!value || value.currency !== 'CHF' || value.prices !== 'net' || !Number.isFinite(value.vatRate) || value.vatRate < 0 || value.vatRate > 100 || !value.items || typeof value.items !== 'object' || Array.isArray(value.items) || !Object.keys(value.items).length) throw new Error('invalid_catalog');
|
|
for (const item of Object.values(value.items)) {
|
|
if (!item || typeof item.name !== 'string' || !item.name || typeof item.sku !== 'string' || !Number.isSafeInteger(item.unitAmount) || item.unitAmount < 0 || !Number.isSafeInteger(item.setupAmount) || item.setupAmount < 0 || (item.termMonths !== null && (!Number.isSafeInteger(item.termMonths) || item.termMonths < 1))) throw new Error('invalid_catalog_item');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export const grossAmount = (net, rate) => net + Math.round(net * rate / 100);
|
|
export const termLabel = months => months === null ? 'Einmalig' : months === 12 ? 'Pro Jahr' : `Pro ${months} Monate`;
|
|
|
|
if (typeof document !== 'undefined') {
|
|
const list = document.getElementById('price-list');
|
|
const status = document.getElementById('price-status');
|
|
const refresh = document.getElementById('refresh-prices');
|
|
let catalog = null;
|
|
let pending = false;
|
|
const money = new Intl.NumberFormat('de-CH', { style:'currency', currency:'CHF' });
|
|
function el(tag, text, className) { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; }
|
|
function render() {
|
|
list.replaceChildren();
|
|
if (!catalog) return;
|
|
const gross = document.querySelector('[name="tax"]:checked').value === 'gross';
|
|
const amount = value => money.format((gross ? grossAmount(value, catalog.vatRate) : value) / 100);
|
|
const table = el('table', undefined, 'price-table');
|
|
table.append(el('caption', `${gross ? 'Inklusive' : 'Zuzüglich'} ${new Intl.NumberFormat('de-CH').format(catalog.vatRate)} % MwSt. · Preise je Einheit`));
|
|
const head = el('thead'), row = el('tr');
|
|
for (const text of ['Produkt', 'Laufzeit', 'Lizenz', 'Ersteinrichtung']) { const th = el('th', text); th.scope = 'col'; row.append(th); }
|
|
head.append(row); table.append(head);
|
|
const body = el('tbody');
|
|
for (const item of Object.values(catalog.items)) {
|
|
const tr = el('tr'), name = el('th', item.name); name.scope = 'row'; name.append(el('small', item.sku));
|
|
tr.append(name, el('td', termLabel(item.termMonths)), el('td', amount(item.unitAmount), 'amount'), el('td', amount(item.setupAmount), 'amount'));
|
|
body.append(tr);
|
|
}
|
|
table.append(body);
|
|
const wrapper = el('div', undefined, 'table-scroll'); wrapper.tabIndex = 0; wrapper.setAttribute('role', 'region'); wrapper.setAttribute('aria-label', 'Aktuelle Preise'); wrapper.append(table); list.append(wrapper);
|
|
}
|
|
async function load() {
|
|
if (pending) return;
|
|
pending = true; refresh.disabled = true;
|
|
try {
|
|
const response = await fetch('/api/catalog', { cache:'no-store', credentials:'omit', signal:AbortSignal.timeout(10000) });
|
|
if (!response.ok) throw new Error('catalog_unavailable');
|
|
catalog = validateCatalog(await response.json());
|
|
render(); status.classList.remove('error');
|
|
status.textContent = `Stand ${new Date().toLocaleTimeString('de-CH')} · automatisch alle 30 Sekunden aktualisiert.`;
|
|
} catch {
|
|
catalog = null; list.replaceChildren(); status.classList.add('error');
|
|
status.textContent = 'Aktuelle Preise sind vorübergehend nicht erreichbar. Bitte erneut aktualisieren. Es werden keine veralteten Ersatzpreise angezeigt.';
|
|
} finally { pending = false; refresh.disabled = false; }
|
|
}
|
|
refresh.addEventListener('click', load);
|
|
document.querySelectorAll('[name="tax"]').forEach(input => input.addEventListener('change', render));
|
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
|
|
window.addEventListener('pageshow', event => { if (event.persisted) load(); });
|
|
setInterval(() => { if (!document.hidden) load(); }, 30000);
|
|
load();
|
|
}
|