/* Kundenansicht neu (P2): gemeinsame Hilfen der Ansichten. Formate, Bausteinklassen, Navigation, Ereignisse und kleine Merker. Kein DOM-Zugriff beim Import; document und window werden erst beim Aufruf gelesen. */ import { api } from './api.js' import { geheZu } from './router.js' import { hashFuerAnsicht } from './routen.js' import { safeUrl } from '../fn-util.js' /** Klassen der Bausteine aus basis.css. */ export const K = Object.freeze({ knopf: 'kn-knopf', primaer: 'kn-knopf-primaer', zweit: 'kn-knopf-zweit', textknopf: 'kn-textknopf', feld: 'kn-feld', input: 'kn-input', select: 'kn-select', textarea: 'kn-textarea', check: 'kn-check', liste: 'kn-liste', zeile: 'kn-zeile', abschnitt: 'kn-abschnitt', meldung: 'kn-meldung', fehler: 'kn-fehler', leer: 'kn-leer', versteckt: 'hidden', pille: Object.freeze({ grau: 'kn-pille kn-pille-grau', blau: 'kn-pille kn-pille-blau', orange: 'kn-pille kn-pille-orange', violett: 'kn-pille kn-pille-violett', gruen: 'kn-pille kn-pille-gruen', rot: 'kn-pille kn-pille-rot', }), }) export const ABO_NICHT_GELADEN = 'Deine Verträge konnten gerade nicht geladen werden. Bitte versuche es später noch einmal.' /* ---------- Formate ---------- */ const ZAHL_2 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) const ZAHL_1 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) const ZAHL_STD = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) /** Teile eines Zeitpunkts in Berliner Zeit; ungültige Werte ergeben null. */ function berlinTeile(wert, extra = {}) { const d = wert instanceof Date ? wert : new Date(wert) if (!wert || Number.isNaN(d.getTime())) return null const teile = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', ...extra }).formatToParts(d) return Object.fromEntries(teile.map((t) => [t.type, t.value])) } /** 50 → '50,00 €' (geschütztes Leerzeichen vor dem Euro-Zeichen). */ export const fmtEuro = (euro) => `${ZAHL_2.format(Number(euro) || 0)} €` /** 'TT.MM.JJJJ' in Berliner Zeit; ein Wert, der schon so aussieht, bleibt; leer oder ungültig '–'. */ export function fmtDatum(wert) { if (typeof wert === 'string' && /^\d{2}\.\d{2}\.\d{4}$/.test(wert)) return wert const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' }) return t ? `${t.day}.${t.month}.${t.year}` : '–' } /** 'TT.MM.JJ, HH:MM' in Berliner Zeit. */ export function fmtDatumZeit(wert) { const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }) return t ? `${t.day}.${t.month}.${t.year}, ${t.hour}:${t.minute}` : '–' } /** 'TT.MM.' in Berliner Zeit; ungültig ''. */ export function fmtTagMonat(wert) { const t = berlinTeile(wert, { day: '2-digit', month: '2-digit' }) return t ? `${t.day}.${t.month}.` : '' } /** 'HH:MM' in Berliner Zeit; ungültig ''. */ export function fmtUhrzeit(wert) { const t = berlinTeile(wert, { hour: '2-digit', minute: '2-digit' }) return t ? `${t.hour}:${t.minute}` : '' } /** Kalendertag in Berliner Zeit als { j, m, t } (Zahlen); ungültig null. */ export function berlinTag(wert) { const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' }) return t ? { j: Number(t.year), m: Number(t.month), t: Number(t.day) } : null } /** Minuten → '2,5 Std.' | '2 Std.' | '0,75 Std.' (Komma, höchstens zwei Stellen). */ export const fmtStd = (minuten) => `${ZAHL_STD.format(Math.round(((Number(minuten) || 0) / 60) * 100) / 100)} Std.` /** Minuten → '' | '45 min' | '2 h' | '2 h 30 min'. */ export function fmtMinuten(minuten) { const m = Math.round(Number(minuten) || 0) if (m <= 0) return '' const h = Math.floor(m / 60) const rest = m % 60 if (!h) return `${rest} min` return rest ? `${h} h ${rest} min` : `${h} h` } /** Bytes → '12 B' | '1,5 KB' | '5,0 MB'. */ export function fmtBytes(bytes) { const b = Number(bytes) || 0 if (b < 1024) return `${Math.round(b)} B` if (b < 1024 * 1024) return `${ZAHL_1.format(b / 1024)} KB` return `${ZAHL_1.format(b / (1024 * 1024))} MB` } /* ---------- Alt-Module, Navigation, Ereignisse ---------- */ /** Adapter für Alt-Module (auftrag-portal.js), die den Body als JSON-String schicken: das neue api() kodiert Objekte selbst. */ export function legacyRequest(apiFn) { return (pfad, opts = {}) => { const { body } = opts if (typeof body !== 'string') return apiFn(pfad, { ...opts }) let objekt = body try { objekt = JSON.parse(body) } catch { /* kein JSON: unverändert weitergeben */ } return apiFn(pfad, { ...opts, body: objekt }) } } /** Programmatisch navigieren (nur über den Router); wirft der Router, setzt der Hash die Adresse. */ export function navigiere(hash, { ersetzen = false } = {}) { try { geheZu(hash, { ersetzen }) } catch { window.location.hash = hash } } /** Verknüpfung zu einer Ansicht für die Brücke der Hülle: href plus data-view (und data-project). */ export function zuAnsicht(name, projektId = '') { const basis = hashFuerAnsicht(name) ?? '#' if (!projektId) return { href: basis, 'data-view': name } return { href: `${basis}${basis.includes('?') ? '&' : '?'}projekt=${encodeURIComponent(projektId)}`, 'data-view': name, 'data-project': projektId } } /** Titel der Etage melden (die Hülle zeigt ihn im Kopf). */ export function meldeTitel(el, text) { el.dispatchEvent(new CustomEvent('kn:titel', { bubbles: true, detail: { text } })) } /** Zähler neu laden lassen: was = 'chat' | 'vertraege' | 'alle'. Ohne Dokument tut es nichts. */ export function meldeZaehlerNeu(was) { if (typeof document === 'undefined' || typeof document.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return document.dispatchEvent(new CustomEvent('kn:zaehler-neu', { bubbles: true, detail: { was } })) } /* ---------- Merker zwischen Ansichten (einmalig) ---------- */ let hinweis = '' export function merkeHinweis(text) { hinweis = String(text ?? '') } export function nimmHinweis() { const t = hinweis; hinweis = ''; return t } let chatVorgabe = '' export function setzeChatVorgabe(text) { chatVorgabe = String(text ?? '') } export function nimmChatVorgabe() { const t = chatVorgabe; chatVorgabe = ''; return t } /* ---------- Abo ---------- */ /** GET /api/abo → Objekt oder null (jeder Fehler). */ export async function ladeAbo() { try { const antwort = await api('/api/abo') return antwort && typeof antwort === 'object' ? antwort : null } catch { return null } } /** Gemerkte Funktion eines Wechsels (V11): nur wenn key und projectId nichtleere Texte sind. */ export function wechselFunktionAus(sub) { const f = sub?.wechselFunktion if (!f || typeof f.key !== 'string' || !f.key || typeof f.projectId !== 'string' || !f.projectId) return null return { key: f.key, projectId: f.projectId } } export { safeUrl }