Kundenansicht neu E-Fix F1: Kerninfo und Knopf der Bestellseiten passen in den nutzbaren Bereich (390x844, 375x667)
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>
This commit is contained in:
1 parent
779320c080
commit
d4050f870a
8 files changed
+148
-41
No files matched your search
@@ -1,6 +1,8 @@
|
||||
/* Kundenansicht neu (P7): Bestellseite je Abo (#bestellen-hosting, #bestellen-wartung) im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Reihenfolge ohne Klappe und ohne Scrollbereich: Block „Was du bestellst“, eine Linkzeile, bei Privatkunden ein Häkchen, der Knopf
|
||||
„Zahlungspflichtig bestellen“ (§ 312j BGB: Kerninfo und Knopf auf demselben Bildschirm), darunter die Kundenart-Zeile.
|
||||
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
|
||||
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
|
||||
Die Sätze (Linkzeile, Häkchen, Kundenart-Zeile) kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst:
|
||||
der Client sendet nie kundenart oder startChoice, bei Privatkunden nur earlyStart (das Häkchen) und den gezeigten Leistungsbeginn.
|
||||
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
|
||||
@@ -8,7 +10,7 @@
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js'
|
||||
import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
|
||||
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
|
||||
@@ -35,10 +37,10 @@ function preise(product, offer) {
|
||||
const mitZahlen = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0
|
||||
const preis = fmtEuro(product.amountCents / 100)
|
||||
const preisZeile = !offer
|
||||
? `${preis} pro Monat (Gesamtpreis, ${UST}), keine weiteren Kosten`
|
||||
? `${preis} pro Monat (Gesamtpreis, ${UST})`
|
||||
: mitZahlen
|
||||
? `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate, danach ${fmtEuro(offerRegular / 100)} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST}), keine weiteren Kosten`
|
||||
: `${preis} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST}), keine weiteren Kosten`
|
||||
? `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate, danach ${fmtEuro(offerRegular / 100)} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
|
||||
: `${preis} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
|
||||
return { preisZeile, erste: fmtEuro((mitZahlen ? offerMonthly : product.amountCents) / 100) }
|
||||
}
|
||||
|
||||
@@ -46,13 +48,13 @@ function preise(product, offer) {
|
||||
function faelligBeginn({ lage, erste, frist }) {
|
||||
if (lage === 'frist') {
|
||||
return {
|
||||
faellig: `Erste Abbuchung am ${frist || 'Ende der 14-tägigen Widerrufsfrist'}: ${erste} (${UST}); heute wird nichts abgebucht`,
|
||||
beginn: `nach Ablauf der 14-tägigen Widerrufsfrist${frist ? ` (am ${frist})` : ''}; bis dahin keine Leistung und keine Abbuchung`,
|
||||
faellig: `erst am ${frist || 'Ende der Widerrufsfrist'}: ${erste}`,
|
||||
beginn: `nach Ablauf der 14-tägigen Widerrufsfrist${frist ? ` (am ${frist})` : ''}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
faellig: `Heute fällig: ${erste} (${UST})`,
|
||||
beginn: lage === 'wunsch' ? 'sofort, auf deinen ausdrücklichen Wunsch vor Ablauf der Widerrufsfrist' : 'sofort',
|
||||
faellig: `heute ${erste}`,
|
||||
beginn: lage === 'wunsch' ? 'sofort, auf deinen ausdrücklichen Wunsch' : 'sofort',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,13 +84,19 @@ function bestellung(daten, productKey, { viewAs, kundenart }) {
|
||||
const start = faelligBeginn({ lage: privat ? 'frist' : 'sofort', erste, frist })
|
||||
|
||||
const block = baueBlock([
|
||||
{ label: 'Leistung', text: `${product.name}: ${(product.features || []).map(([t]) => t).join(', ')}` },
|
||||
{ label: 'Leistung', text: labelVon(daten, productKey), link: { text: 'Leistungen ansehen', href: leistungLink(T.abo.linkzeile, productKey) } },
|
||||
{ label: 'Preis', text: preisZeile },
|
||||
{ id: 'abo-due-summary', label: 'Fällig', text: start.faellig },
|
||||
{ id: 'abo-start-summary', label: 'Beginn', text: start.beginn },
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate, kündbar bis 1 Monat vor Ende; danach monatlich` },
|
||||
{ label: 'Zahlung', text: 'Karte oder SEPA über Stripe' },
|
||||
], { id: 'abo-block' })
|
||||
// Die ausführlichen Sätze stehen unter dem Knopf
|
||||
const einzelheiten = baueEinzelheiten([
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar` },
|
||||
{ label: 'Zahlung', text: 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen' },
|
||||
], { id: 'abo-block' })
|
||||
{ label: 'Kosten', text: 'keine weiteren Kosten' },
|
||||
], { id: 'abo-einzelheiten' })
|
||||
|
||||
const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null
|
||||
const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true })
|
||||
@@ -103,7 +111,8 @@ function bestellung(daten, productKey, { viewAs, kundenart }) {
|
||||
haken ? h('label', { class: K.check }, haken, h('span', { id: 'abo-beginn-text' }, T.abo.haken)) : null,
|
||||
fehler,
|
||||
h('div', { class: 'kn-aktionen' }, kaufen, viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' }))
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' }),
|
||||
einzelheiten)
|
||||
|
||||
// Das Häkchen schaltet die Zeilen „Fällig“ und „Beginn“ um
|
||||
haken?.addEventListener('change', () => {
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
der Wechselrechnung (Bedingungen § 9 Abs. 3). Die Sätze kommen vom Server (GET /api/abo → bestelltexte); der Server entscheidet die
|
||||
Kundenart selbst, der Client sendet nie kundenart. Ein Betrieb braucht kein Häkchen.
|
||||
Die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen.
|
||||
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
|
||||
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
|
||||
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js'
|
||||
import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
|
||||
|
||||
const SCHLUESSEL = 'kn-wechsel-funktion'
|
||||
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
|
||||
@@ -75,18 +77,32 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
|
||||
const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, early: false, fehler: '', fehlerKlasse: null, zeichne: null }
|
||||
aktiv = lauf
|
||||
|
||||
/** Zeilen des Blocks aus der Vorschau des Servers (Leistung, Wartungs-Abo, Zusatzspeicher, Gutschrift, fällig, Abbuchungstag, Laufzeit, Beginn). */
|
||||
/** Zeilen des Blocks aus der Vorschau des Servers, knapp: Leistung, Preis, Zusatzspeicher, Gutschrift, Fällig (mit Abbuchungstag), Laufzeit, Beginn. */
|
||||
const zeilenVon = (p) => {
|
||||
const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents
|
||||
const tag = berlinTag(p.nextChargeAt)
|
||||
return [
|
||||
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.' },
|
||||
{ label: 'Wartungs-Abo', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` },
|
||||
{ label: 'Leistung', text: 'Wartungs-Abo statt Hosting-Abo', link: { text: 'Leistungen ansehen', href: leistungLink(T.wechsel.linkzeile, 'wartung') } },
|
||||
{ label: 'Preis', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` },
|
||||
...(units > 0
|
||||
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST}), insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }]
|
||||
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht, insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }]
|
||||
: []),
|
||||
...((Number(p.creditCents) || 0) > 0
|
||||
? [{ label: 'Gutschrift für nicht genutzte Tage', text: `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (${UST})` }]
|
||||
? [{ label: 'Gutschrift', text: `−${fmtEuro(p.creditCents / 100)} für nicht genutzte Tage des Hosting-Abos` }]
|
||||
: []),
|
||||
{ label: 'Fällig', text: `heute ${fmtEuro(p.amountDueCents / 100)}; danach ab ${fmtDatum(p.nextChargeAt)}${tag ? ` jeweils am ${tag.t}.` : ''}` },
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate), danach monatlich kündbar` },
|
||||
{ label: 'Beginn', text: 'sofort mit der Zahlung; einen späteren Beginn gibt es nicht' },
|
||||
]
|
||||
}
|
||||
|
||||
/** Die ausführlichen Sätze unter dem Knopf (Wortlaut wie bisher im Block). */
|
||||
const einzelheitenVon = (p) => {
|
||||
const tag = berlinTag(p.nextChargeAt)
|
||||
return [
|
||||
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.' },
|
||||
...(units > 0
|
||||
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST})` }]
|
||||
: []),
|
||||
{ label: 'Heute fällig', text: `${fmtEuro(p.amountDueCents / 100)} (${UST})` },
|
||||
{ label: 'Neuer Abbuchungstag', text: `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}` },
|
||||
@@ -119,7 +135,8 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
|
||||
h('div', { class: 'kn-aktionen' },
|
||||
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
|
||||
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' })))
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' }),
|
||||
p ? baueEinzelheiten(einzelheitenVon(p), { id: 'switch-einzelheiten' }) : null))
|
||||
}
|
||||
lauf.zeichne = zeichne
|
||||
|
||||
|
||||
@@ -336,6 +336,9 @@
|
||||
.kn-speicher-warnung { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-orange-fg); }
|
||||
.kn-speicher-warnung a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.kn-block dd.kn-fehler { color: var(--kn-danger); }
|
||||
.kn-einzelheiten { margin: 20px 0 0; }
|
||||
.kn-einzelheiten > h3 { margin: 0 0 8px; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--kn-text-2); }
|
||||
.kn-einzelheiten .kn-block { color: var(--kn-text-2); }
|
||||
.kn-gutschrift-bedingungen { max-width: 640px; }
|
||||
|
||||
/* ------------------------------------------------------------------ Handy --- */
|
||||
@@ -353,8 +356,17 @@
|
||||
.kn-pw { max-width: none; }
|
||||
.kn-tk-feld input:not([type="checkbox"]):not([type="file"]) { height: 44px; font-size: 16px; }
|
||||
.kn-stepper .kn-knopf { min-width: 44px; }
|
||||
.kn-block-zeile { grid-template-columns: minmax(0, 1fr); gap: 0; padding: 6px 0; }
|
||||
.kn-block dt { font-size: 12px; line-height: 16px; }
|
||||
/* Bestellseiten (F1): Kerninfo und Knopf müssen im nutzbaren Bereich zwischen Kopf und Leiste zusammen stehen (375 x 667: gut 550 px).
|
||||
Darum läuft die Beschriftung in der Zeile mit (Label: Text) statt eine eigene Zeile zu brauchen, und der Abstand ist knapp. */
|
||||
.kn-bestellen > h2 { margin: 0 0 6px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||
.kn-block { margin-bottom: 10px; }
|
||||
.kn-block-zeile { display: block; padding: 5px 0; }
|
||||
.kn-block dt, .kn-block dd { display: inline; }
|
||||
.kn-block dt { font-weight: 600; color: var(--kn-text); }
|
||||
.kn-block dt::after { content: ": "; }
|
||||
.kn-linkzeile { margin-bottom: 8px; }
|
||||
.kn-bestellen .kn-check { margin-bottom: 8px; }
|
||||
.kn-sperrzeile { margin-bottom: 8px; }
|
||||
.kn-bestellen .kn-knopf { width: 100%; min-height: 44px; }
|
||||
.kn .kn-chat .chat-bubble { max-width: 88%; }
|
||||
.kn .kn-chat .chat-messages { min-height: 240px; max-height: 55vh; }
|
||||
|
||||
@@ -70,9 +70,30 @@ export function baueKundenartZeile(kundenart, texte, { id } = {}) {
|
||||
return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur)
|
||||
}
|
||||
|
||||
/** Block „Was du bestellst“: zeilen [{ id?, label, text, klasse? }] als Beschriftungsliste. Der Text ist immer Text. */
|
||||
/**
|
||||
* 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) => h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text))))
|
||||
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 !== ''
|
||||
|
||||
Reference in new issue
Block a user