Die Messung im Werkzeug nahm innerHeight; nutzbar ist am Handy nur der Bereich zwischen Kopf und fester Leiste (735 px bei 844, 558 px bei 667). Gemessen vor der Korrektur (Blockanfang bis Knopfunterkante, 375x667): Abo bestellen privat 799 px, Wechsel privat 888 px, Zusatzspeicher 567 px. - Block knapp: Leistung als eine Zeile mit Link "Leistungen ansehen" auf die Leistungsbeschreibung, Preis, Fällig, Beginn, Laufzeit und Zahlung in je ein bis zwei Zeilen. - Die ausführlichen Sätze stehen unter dem Knopf und der Kundenart-Zeile (baueEinzelheiten), ohne Klappe. - Handy: Beschriftung läuft in der Zeile mit, kleinere Überschrift, knappere Abstände. - Die Wortlaute des Blocks sind gekürzt und gehören in die Rechtsrunde. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
116 lines
5.7 KiB
JavaScript
116 lines
5.7 KiB
JavaScript
/* Kundenansicht neu (P7): gemeinsame Bausteine der vier Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher, Gutschrift-Antrag).
|
|
Die Sätze kommen vom Server (GET /api/abo → bestelltexte); hier werden sie nur gezeichnet. Link-Texte stehen in der Vorlage in
|
|
eckigen Klammern, ihre Adressen daneben in urls. Jede Adresse läuft durch sicherLink; Kundentext wird nie als HTML gelesen.
|
|
Keine h1: den Titel meldet die Ansicht mit meldeTitel. */
|
|
|
|
import { h } from './dom.js'
|
|
import { K } from './ansicht-hilfen.js'
|
|
|
|
/** 'unternehmen' | 'privat': nur exakt 'unternehmen' (me.customer.kundenartWirksam, sonst daten.kundenart) ist Betrieb; alles andere privat (E4). */
|
|
export function kundenartVon(me, daten = null) {
|
|
return me?.customer?.kundenartWirksam === 'unternehmen' || daten?.kundenart === 'unternehmen' ? 'unternehmen' : 'privat'
|
|
}
|
|
|
|
/** Adresse nur, wenn sie ein Pfad auf derselben Herkunft ('/…', nicht '//…'), ein Anker ('#name…') oder http(s) ohne Zugangsdaten ist; sonst ''. */
|
|
export function sicherLink(url) {
|
|
if (typeof url !== 'string' || url === '') return ''
|
|
if (/[\s\u0000-\u001f\u007f\\]/.test(url)) return ''
|
|
if (/^\/(?![/\\])/.test(url)) return url
|
|
if (/^#[a-z][\w-]*(\?[\w=&.%-]*)?$/i.test(url)) return url
|
|
if (/^https?:\/\/[^/?#]+/i.test(url)) {
|
|
try {
|
|
const u = new URL(url)
|
|
return u.hostname !== '' && u.username === '' && u.password === '' ? url : ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* 'Es gilt [A] und [B].' mit ['/a', '/b'] → Teile [{ text }, { text, href? }]. Die Markierungen bekommen der Reihe nach ihre Adresse;
|
|
* eine unsichere oder fehlende Adresse macht den Teil zu einem Teil ohne href. Eine offene Klammer bleibt Text.
|
|
*/
|
|
export function linkTeile(text, urls) {
|
|
const basis = typeof text === 'string' ? text : ''
|
|
const adressen = Array.isArray(urls) ? urls : []
|
|
const teile = []
|
|
let ab = 0
|
|
let nr = 0
|
|
const muster = /\[([^\]]*)\]/g
|
|
for (let m = muster.exec(basis); m; m = muster.exec(basis)) {
|
|
if (m.index > ab) teile.push({ text: basis.slice(ab, m.index) })
|
|
const href = sicherLink(adressen[nr])
|
|
nr += 1
|
|
teile.push(href ? { text: m[1], href } : { text: m[1] })
|
|
ab = m.index + m[0].length
|
|
}
|
|
if (ab < basis.length) teile.push({ text: basis.slice(ab) })
|
|
return teile
|
|
}
|
|
|
|
const ausserhalb = (href) => !href.startsWith('#')
|
|
|
|
/**
|
|
* <p id> mit den Links der Vorlage; '/…' und http(s) öffnen in neuem Tab (target _blank, rel noopener), '#…' bleibt im Fenster.
|
|
* klasse: Vorgabe 'kn-linkzeile'. neuerTab: auch '#…' öffnet in neuem Tab (Bedingungen der Gutschrift neben dem Formular).
|
|
*/
|
|
export function baueLinkzeile({ text, urls } = {}, { id, klasse = 'kn-linkzeile', neuerTab = false } = {}) {
|
|
const kinder = linkTeile(text, urls).map((t) =>
|
|
t.href ? h('a', { href: t.href, ...(neuerTab || ausserhalb(t.href) ? { target: '_blank', rel: 'noopener' } : {}) }, t.text) : t.text)
|
|
return h('p', { id, class: klasse }, kinder)
|
|
}
|
|
|
|
/** <p id>: „Du bestellst als Betrieb“ | „Du bestellst privat“, ' · ', dann der Hinweis zur Korrektur mit seinem Link. */
|
|
export function baueKundenartZeile(kundenart, texte, { id } = {}) {
|
|
const lage = kundenart === 'unternehmen' ? texte?.unternehmen : texte?.privat
|
|
const korrektur = linkTeile(texte?.korrektur?.text, texte?.korrektur?.urls).map((t) =>
|
|
t.href ? h('a', { href: t.href, ...(ausserhalb(t.href) ? { target: '_blank', rel: 'noopener' } : {}) }, t.text) : t.text)
|
|
return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur)
|
|
}
|
|
|
|
/**
|
|
* Block „Was du bestellst“: zeilen [{ id?, label, text, klasse?, link? }] als Beschriftungsliste. Der Text ist immer Text.
|
|
* link { text, href }: hinter dem Text ' · ' und ein Link (nur mit sicherer Adresse; Adressen außerhalb der Seite öffnen in neuem Tab).
|
|
*/
|
|
export function baueBlock(zeilen, { id } = {}) {
|
|
return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => {
|
|
const href = z.link ? sicherLink(z.link.href) : ''
|
|
const link = href ? [' · ', h('a', { href, ...(ausserhalb(href) ? { target: '_blank', rel: 'noopener' } : {}) }, z.link.text)] : null
|
|
return h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text, link))
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Die ausführlichen Sätze UNTER dem Knopf (und der Kundenart-Zeile): Überschrift „Im Einzelnen“ und dieselbe Beschriftungsliste wie der Block.
|
|
* Der Block über dem Knopf trägt die Kerninfo knapp, damit sie mit dem Knopf auf einen Handy-Bildschirm passt; hier steht sie ausführlich.
|
|
*/
|
|
export function baueEinzelheiten(zeilen, { id } = {}) {
|
|
return h('section', { id, class: 'kn-einzelheiten' }, h('h3', null, 'Im Einzelnen'), baueBlock(zeilen))
|
|
}
|
|
|
|
/** Adresse der Leistungsbeschreibung: die Bedingungen (erste Adresse der Linkzeile) mit Anker ('wartung', 'hosting'); unsicher oder fehlend ''. */
|
|
export function leistungLink(linkzeile, anker) {
|
|
const basis = sicherLink(Array.isArray(linkzeile?.urls) ? linkzeile.urls[0] : '')
|
|
return basis && !basis.includes('#') ? `${basis}#${anker}` : ''
|
|
}
|
|
|
|
const nichtLeer = (x) => typeof x === 'string' && x !== ''
|
|
|
|
/**
|
|
* true, wenn die Antwort den Wortlaut trägt: status, knopf und die Sätze der Seite (art 'abo' | 'wechsel' | 'speicher' | 'gutschrift').
|
|
* Ohne ihn zeigt keine Seite einen Bestellknopf.
|
|
*/
|
|
export function hatWortlaut(daten, art = 'abo') {
|
|
const t = daten?.bestelltexte
|
|
if (!t || typeof t !== 'object' || !nichtLeer(t.status) || !nichtLeer(t.knopf)) return false
|
|
if (art === 'gutschrift') return nichtLeer(t.gutschrift?.satz?.text) && nichtLeer(t.gutschrift?.knopf)
|
|
if (!nichtLeer(t[art]?.linkzeile?.text)) return false
|
|
if (art === 'wechsel') return nichtLeer(t.wechsel.haken) && nichtLeer(t.wechsel.gesperrt)
|
|
if (art === 'abo') return nichtLeer(t.abo.haken)
|
|
return true
|
|
}
|
|
|
|
export { K }
|