Files
Webklar-Kundenbereich/public/neu/ansicht-hilfen.js
T
ClaudeandClaude Sonnet 5.5 140275022d Kundenansicht neu E-Fix M6: Zahlungsknöpfe sind nach der Rückkehr von Stripe wieder bedienbar
Nach "Zahlungspflichtig bestellen" setzt die Seite den Knopf auf "Weiter zur Zahlung…" (gesperrt) und leitet zu Stripe. Bricht der Kunde per
Zurück-Wischgeste ab, stellt Safari die Seite aus dem Bfcache her: der Knopf blieb gesperrt, ein neuer Versuch ging nur nach Neuladen. Nur
ansicht-wechsel.js hatte einen pageshow-Handler. Neuer Helfer beiRueckkehr(el, fn) in ansicht-hilfen.js (pageshow mit persisted, entfernt sich,
sobald das Element nicht mehr im Dokument steht); genutzt von der Bestellseite und von "Zahlungsart aktualisieren" (Verträge).
Speicher und Gutschrift leiten nicht zu Stripe, dort ist nichts zu tun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:45:15 +00:00

207 lines
8.4 KiB
JavaScript
Raw 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.
/* 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, aktuelleRoute } 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.'
/** Inhalt von el ersetzen; null, undefined und false (leere Platzhalter) werden übersprungen, nie als Text "null" angehängt. */
export function ersetze(el, ...kinder) {
el.replaceChildren(...kinder.flat(Infinity).filter((k) => k !== null && k !== undefined && k !== false))
return el
}
/* ---------- 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). Läuft kein Router oder wirft er, trägt der Hash die Adresse. */
export function navigiere(hash, { ersetzen = false } = {}) {
try {
if (aktuelleRoute() === null) { window.location.hash = hash; return }
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 } }))
}
/**
* Zurück aus dem Bfcache (E-Fix M6): kommt der Kunde von der Zahlungsseite von Stripe per Wischgeste zurück, stellt Safari die Seite so wieder her,
* wie sie war (Knopf gesperrt, "Weiter zur Zahlung…"). fn läuft bei pageshow mit persisted, solange `el` im Dokument steht; ist es ersetzt worden,
* entfernt sich der Hörer. Ohne Fenster tut es nichts.
*/
export function beiRueckkehr(el, fn) {
const win = globalThis.window
if (!win || typeof win.addEventListener !== 'function') return
const hoerer = (ev) => {
if (!el.isConnected) { win.removeEventListener('pageshow', hoerer); return }
if (ev.persisted) fn()
}
win.addEventListener('pageshow', hoerer)
}
/** 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 }
}
/** Adressen aus Daten (previewUrl, portalUrl, paymentUrl): http(s) wie safeUrl, dazu Pfade auf derselben Herkunft ("/vorschau-hinweis.html").
* Nicht zugelassen: javascript:, data:, "//host" und "/\\host" (Browser lesen den Rückstrich wie einen Schrägstrich). Sonst ''. */
export function sichereAdresse(url) {
const text = typeof url === 'string' ? url : ''
return safeUrl(text) || (/^\/(?![/\\])/.test(text) ? text : '')
}
export { safeUrl }