Navigation leichter: Handy-Leiste, Schwarz nur für die Hauptaktion, Verträge mit zwei Reitern, kürzeres Konto (V3, V5, V7, V8)
V3: In der Fahrstuhl-Zeile läuft unter dem aktiven Wort eine dünne Linie statt des schwarzen Blocks; das Wort wird dunkel und fett. Die weiße Overlay-Kopie bleibt im Baum, wird aber nicht mehr gezeigt. V5: Terminplaner: Tag/Woche/Monat als hellgrauer Umschalter mit weißer Auswahl, Filter als leise Chips. „Einschalten“ in der Liste Entdecken ist ein heller Knopf; schwarz bleibt der Knopf der Bestätigungsseite. V7: Verträge nur noch mit Abo und Rechnungen. Das Stundenkonto steht beim Wartungs-Abo als Abschnitt unter dem Abo (ohne doppelte Warnungen), #stunden springt dorthin. Der E-Mail-Speicher ist die Unterseite #speicher (Link „ändern“ in der Abo-Zeile), ohne Widerrufsstreifen wie die anderen Bestellseiten. Alte Links bleiben gültig. V8: Konto mit vier Zeilen (Passwort, Verträge kündigen, Vertrag widerrufen, Abmelden), Datenschutz und Impressum klein darunter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a270ff51da
commit
83ac903ab3
18 files changed
+152
-96
No files matched your search
@@ -283,7 +283,7 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
||||
let wahl = null
|
||||
const knopf = h('button', {
|
||||
type: 'button',
|
||||
class: 'kn-fn-knopf',
|
||||
class: 'kn-fn-knopf kn-fn-knopf--zweit', // Navigation leichter V5: jede Karte hat ihn, keiner ist die eine Hauptaktion; schwarz bleibt der Knopf der Bestätigungsseite
|
||||
'data-aktion': 'einschalten',
|
||||
on: {
|
||||
click: () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Kundenansicht neu (P7): Reiter E-Mail in Verträge, der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
/* Kundenansicht neu (P7): Unterseite E-Mail-Speicher der Verträge (#speicher, bis „Navigation leichter“ V7 der Reiter E-Mail), der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Plus: Block „Was du bestellst“, eine Linkzeile zu den Regeln, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die
|
||||
Kundenart-Zeile; kein Häkchen (der Klick auf den Knopf ist die Zustimmung). Minus: genau eine Warnzeile und der Knopf „Speichern“.
|
||||
Die Sätze kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst, der Client sendet nie kundenart. */
|
||||
@@ -84,8 +84,7 @@ function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) {
|
||||
const status = []
|
||||
if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Dein zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.'))
|
||||
if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`))
|
||||
ersetze(panel,
|
||||
h('h3', null, 'E-Mail-Speicher'),
|
||||
ersetze(panel, // die Überschrift „E-Mail-Speicher“ trägt die Unterseite selbst
|
||||
h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`),
|
||||
notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
|
||||
!canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null,
|
||||
|
||||
@@ -33,8 +33,12 @@ function gutschriftZeile(c) {
|
||||
h('td', { 'data-label': 'Zeit', class: 'kn-num' }, `+${fmtStd(c.minutes)}`, h('span', { class: 'kn-zeile-sub' }, ` ${c.expired ? 'abgelaufen' : `nutzbar bis ${fmtDatum(c.expiresAt)}`}`)))
|
||||
}
|
||||
|
||||
/** Reiter-Inhalt Stunden. daten (GET /api/abo) ist optional; ohne sie lädt die Funktion selbst. */
|
||||
export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
||||
/**
|
||||
* Stundenkonto. daten (GET /api/abo) ist optional; ohne sie lädt die Funktion selbst.
|
||||
* eingebettet: Abschnitt unter dem Abo (Navigation leichter V7). Dann ohne die Warnungen und den Bedingungen-Link, die das Abo darüber schon zeigt,
|
||||
* und ohne Hinweis, wenn es kein Wartungs-Abo gibt (der Abschnitt steht dann gar nicht da).
|
||||
*/
|
||||
export async function zeigeStunden(wurzel, { route, me, nav, daten, eingebettet = false } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
@@ -46,7 +50,8 @@ export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
||||
const sub = abo.subscription
|
||||
const l = abo.ledger
|
||||
if (!sub?.active || sub.productKey !== 'wartung' || !l) {
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
leeren(wurzel)
|
||||
if (!eingebettet) wurzel.append(h('p', { class: K.leer }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
return
|
||||
}
|
||||
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
|
||||
@@ -54,16 +59,16 @@ export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
||||
const zeilen = [...(l.usage || []).map(verlaufZeile), ...(l.credits || []).slice().reverse().map(gutschriftZeile)]
|
||||
|
||||
ersetze(wurzel,
|
||||
...aboWarnungen(sub),
|
||||
...(eingebettet ? [] : aboWarnungen(sub)),
|
||||
h('div', { class: 'kn-statistik' },
|
||||
stat(fmtStd(l.balanceMinutes), 'verfügbare Zeit'),
|
||||
stat(fmtStd(l.usedMinutes), 'bisher genutzt'),
|
||||
l.nextExpiry ? stat(fmtStd(l.nextExpiry.minutes), `verfallen am ${fmtDatum(l.nextExpiry.at)}`) : null),
|
||||
vorschuss ? h('p', { class: 'kn-hinweis kn-vorschuss' }, vorschuss) : null,
|
||||
h('p', { class: 'kn-hinweis' }, '1 Stunde pro bezahltem Monat. Angerechnet wird in 15-Minuten-Schritten, die ältesten Stunden zuerst.'),
|
||||
h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Leistungen & Bedingungen')),
|
||||
eingebettet ? null : h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Leistungen & Bedingungen')),
|
||||
h('section', { class: 'kn-sektion' },
|
||||
h('h2', { class: K.abschnitt }, 'Verlauf'),
|
||||
h(eingebettet ? 'h3' : 'h2', { class: K.abschnitt }, 'Verlauf'),
|
||||
zeilen.length
|
||||
? h('div', { class: 'kn-tabelle' }, h('table', null, h('thead', null, h('tr', null, h('th', null, 'Datum'), h('th', null, 'Vorgang'), h('th', null, 'Zeit'))), h('tbody', null, zeilen)))
|
||||
: h('p', { class: 'kn-hinweis' }, 'Noch keine Buchungen.')))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Rechnungen, Abo, Stunden und E-Mail. Der Rahmen lädt Abo und Rechnungen
|
||||
parallel (jede Quelle mit eigenem Fehlerweg), wählt den Reiter allein aus route.reiter und gibt die schon geladenen Antworten an
|
||||
die Reiter-Inhalte weiter. Bestellen, Wechsel und Gutschrift sind Etagen, die die Hülle direkt ruft. */
|
||||
/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Abo und Rechnungen (Navigation leichter V7, vorher dazu Stunden und E-Mail).
|
||||
Das Stundenkonto des Wartungs-Abos steht als Abschnitt unter dem Abo; #stunden öffnet das Abo und springt dorthin. Der E-Mail-Speicher ist
|
||||
die Etage #speicher (Link „ändern“ im Abo). Der Rahmen lädt Abo und Rechnungen parallel (jede Quelle mit eigenem Fehlerweg), wählt den
|
||||
Reiter allein aus route.reiter und gibt die schon geladenen Antworten an die Reiter-Inhalte weiter. Bestellen, Wechsel, Gutschrift und
|
||||
Speicher sind Etagen, die die Hülle direkt ruft. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
@@ -11,26 +13,24 @@ import { offeneRechnungen } from './rechnung-offen.js'
|
||||
import { zeigeRechnungen } from './vertraege-rechnungen.js'
|
||||
import { zeigeAbo } from './vertraege-abo.js'
|
||||
import { zeigeStunden } from './ansicht-stunden.js'
|
||||
import { zeigeSpeicher } from './ansicht-speicher.js'
|
||||
|
||||
/** Nach ?abo=success: so oft und in diesem Abstand wird auf ein laufendes Abo gewartet (Tests verkürzen es). */
|
||||
export const AUF_ABO_WARTEN = { versuche: 10, pauseMs: 2000 }
|
||||
|
||||
const LABEL = { rechnungen: 'Rechnungen', abo: 'Abo', stunden: 'Stunden', email: 'E-Mail' }
|
||||
const LABEL = { abo: 'Abo', rechnungen: 'Rechnungen' }
|
||||
// Reiterklicks navigieren mit den ausdrücklichen Hashes (P1 schreibt #wartung und #abo kanonisch um)
|
||||
const HASH = { rechnungen: '#rechnungen', abo: '#abo', stunden: '#stunden', email: '#speicher' }
|
||||
const HASH = { abo: '#abo', rechnungen: '#rechnungen' }
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
let reiterWechsel = null // Reiterklick mit laufendem Router: der nächste Aufruf nimmt die schon geladenen Antworten
|
||||
|
||||
/** Sichtbare Reiter: ohne laufendes Abo Rechnungen und Abo; Hosting dazu E-Mail; Wartung dazu Stunden und E-Mail. */
|
||||
export function sichtbareReiter(daten) {
|
||||
const sub = daten?.subscription
|
||||
const reiter = ['rechnungen', 'abo']
|
||||
if (!sub?.active) return reiter
|
||||
if (sub.productKey === 'hosting') return [...reiter, 'email']
|
||||
return [...reiter, 'stunden', 'email']
|
||||
/** Sichtbare Reiter: immer Abo und Rechnungen (Navigation leichter V7). Stunden stehen im Abo, der E-Mail-Speicher ist die Etage #speicher. */
|
||||
export function sichtbareReiter() {
|
||||
return ['abo', 'rechnungen']
|
||||
}
|
||||
|
||||
/** Gibt es ein Stundenkonto unter dem Abo? Nur mit laufendem Wartungs-Abo. */
|
||||
export const mitStundenkonto = (daten) => Boolean(daten?.subscription?.active && daten.subscription.productKey === 'wartung')
|
||||
|
||||
/** Reiterwahl allein aus route.reiter (routeReiter) und den Daten, nie aus der Adressleiste. Nur der Bereichs-Einstieg (ohne Reiter) springt auf Rechnungen. */
|
||||
export function waehleReiter({ routeReiter, sichtbar, offeneRechnungen: offen }) {
|
||||
if (routeReiter) return sichtbar.includes(routeReiter) ? routeReiter : 'abo'
|
||||
@@ -79,7 +79,8 @@ export async function zeigeVertraege(wurzel, { route, me, nav } = {}) {
|
||||
const sichtbar = sichtbareReiter(daten)
|
||||
const offen = offeneRechnungen(rechnungen).length
|
||||
const gewaehlt = waehleReiter({ routeReiter: route?.reiter, sichtbar, offeneRechnungen: offen })
|
||||
const stundenOhneWartung = route?.reiter === 'stunden' && !sichtbar.includes('stunden')
|
||||
const stundenGewuenscht = route?.reiter === 'stunden' // #stunden: Abo zeigen und zum Stundenkonto springen
|
||||
const stundenkonto = mitStundenkonto(daten)
|
||||
const hinweis = nimmHinweis()
|
||||
|
||||
const inhalt = h('div', { class: 'kn-reiter-inhalt' })
|
||||
@@ -87,14 +88,20 @@ export async function zeigeVertraege(wurzel, { route, me, nav } = {}) {
|
||||
leeren(inhalt)
|
||||
if (key === 'rechnungen') return zeigeRechnungen(inhalt, { me, antwort: rechnungen ?? undefined })
|
||||
if (!daten) return Promise.resolve()
|
||||
if (key === 'abo') {
|
||||
if (stundenOhneWartung) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
const platz = h('div', { class: 'kn-reiter-abo' })
|
||||
inhalt.append(platz)
|
||||
return zeigeAbo(platz, { daten, me })
|
||||
if (stundenGewuenscht && !stundenkonto) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
const platz = h('div', { class: 'kn-reiter-abo' })
|
||||
inhalt.append(platz)
|
||||
const fertig = [zeigeAbo(platz, { daten, me })]
|
||||
if (stundenkonto) {
|
||||
const konto = h('div')
|
||||
const abschnitt = h('section', { class: 'kn-sektion kn-stundenkonto', id: 'kn-stundenkonto', 'aria-labelledby': 'kn-stundenkonto-titel' },
|
||||
h('h2', { class: K.abschnitt, id: 'kn-stundenkonto-titel' }, 'Stundenkonto'), konto)
|
||||
inhalt.append(abschnitt)
|
||||
fertig.push(zeigeStunden(konto, { route, me, nav, daten, eingebettet: true }).then(() => {
|
||||
if (stundenGewuenscht && nav !== 'back' && typeof abschnitt.scrollIntoView === 'function') abschnitt.scrollIntoView({ block: 'start' })
|
||||
}))
|
||||
}
|
||||
if (key === 'stunden') return zeigeStunden(inhalt, { route, me, nav, daten })
|
||||
return zeigeSpeicher(inhalt, { route, me, nav, daten })
|
||||
return Promise.all(fertig)
|
||||
}
|
||||
|
||||
const bei = (key) => {
|
||||
|
||||
@@ -423,3 +423,6 @@
|
||||
.kn-rechnungen .kn-rechnung-aktionen { justify-content: flex-start; padding-top: 6px; }
|
||||
.kn-rechnungen .kn-rechnung-aktionen::before { content: none; }
|
||||
}
|
||||
|
||||
/* Navigation leichter (V7): Stundenkonto als Abschnitt unter dem Abo; #stunden springt hierher, der Kopf deckt die Überschrift nicht zu */
|
||||
.kn-stundenkonto { scroll-margin-top: calc(var(--kn-kopf-h, 52px) + 16px); }
|
||||
+13
-14
@@ -64,7 +64,10 @@
|
||||
.kn-konto-zeile { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 46px; padding: 0; border-bottom: 1px solid var(--kn-line); text-align: left; color: var(--kn-text); }
|
||||
.kn-konto-zeile:hover .kn-konto-text { text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
.kn-konto-zeile--betont .kn-konto-text { font-weight: 600; }
|
||||
.kn-konto-zeile[data-konto="abmelden"] { border-bottom: 0; color: var(--kn-text-2); }
|
||||
.kn-konto-zeile[data-konto="abmelden"] { color: var(--kn-text-2); }
|
||||
/* Navigation leichter (V8): Datenschutz und Impressum klein nebeneinander unter den vier Zeilen */
|
||||
.kn-konto-liste { display: flex; flex-wrap: wrap; column-gap: 16px; }
|
||||
.kn-konto-zeile--klein { width: auto; min-height: 36px; border-bottom: 0; font-size: 13px; line-height: 18px; color: var(--kn-text-2); }
|
||||
.kn-konto-text { display: block; }
|
||||
.kn-konto-hinweis { display: block; margin-top: 1px; font-size: 13px; line-height: 18px; font-weight: 400; color: var(--kn-text-2); }
|
||||
|
||||
@@ -73,6 +76,7 @@
|
||||
.kn-konto { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 92vh; padding: 8px max(20px, env(safe-area-inset-right)) calc(22px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); border-radius: 12px 12px 0 0; animation: kn-sheet-hoch 500ms var(--kn-bremse); }
|
||||
.kn-konto-zu { min-height: 44px; }
|
||||
.kn-konto-zeile { min-height: 46px; }
|
||||
.kn-konto-zeile--klein { min-height: 44px; }
|
||||
}
|
||||
@keyframes kn-sheet-hoch { from { transform: translateY(100%); } }
|
||||
|
||||
@@ -133,7 +137,9 @@
|
||||
#kn-fehler { margin: 12px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); }
|
||||
}
|
||||
|
||||
/* ---------- Leiste (Handy): Fahrstuhl-Zeile mit Balken ---------- */
|
||||
/* ---------- Leiste (Handy): Fahrstuhl-Zeile mit Linie ----------
|
||||
Unter dem aktiven Wort läuft eine dünne schwarze Linie (der Gleiter), das Wort selbst wird dunkel und fett. Schwarze Flächen bleiben den Knöpfen
|
||||
vorbehalten. Die weiße Overlay-Kopie der Wörter aus reiter.js wird dafür nicht mehr gezeigt. */
|
||||
|
||||
.kn-leiste { display: none; }
|
||||
.kn-leiste-reel { height: var(--kn-zeile-h); overflow: hidden; }
|
||||
@@ -144,25 +150,18 @@
|
||||
.kn-balken { position: relative; flex: 1; min-width: 0; height: 44px; padding-right: 6px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; } /* 6 px Luft rechts: der Zähler am letzten Wort ragt über das Wort hinaus und würde sonst abgeschnitten */
|
||||
.kn-balken::-webkit-scrollbar { display: none; }
|
||||
.kn-balken-basis { position: relative; z-index: 1; display: flex; width: max-content; min-width: 100%; height: 44px; }
|
||||
.kn-gleiter { position: absolute; z-index: 2; top: 4px; left: 0; height: 36px; overflow: hidden; border-radius: 6px; background: var(--kn-accent); pointer-events: none; transition: transform 550ms var(--kn-feder), width 550ms var(--kn-feder); }
|
||||
.kn-overlay { position: absolute; top: -4px; left: 0; display: flex; height: 44px; transition: transform 550ms var(--kn-feder); }
|
||||
.kn-fb .kn-overlay { display: none; }
|
||||
.kn-fb .kn-gleiter { z-index: 0; }
|
||||
.kn-fb .kn-t[aria-selected="true"] { color: #fff; }
|
||||
.kn-gleiter { position: absolute; z-index: 2; bottom: 2px; left: 0; height: 2px; overflow: hidden; border-radius: 1px; background: var(--kn-accent); pointer-events: none; transition: transform 550ms var(--kn-feder), width 550ms var(--kn-feder); }
|
||||
.kn-overlay { display: none; }
|
||||
|
||||
.kn-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 4px; font-size: 15px; font-weight: 600; color: var(--kn-text-2); white-space: nowrap; text-align: center; transition: color 250ms; }
|
||||
.kn-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 4px; font-size: 15px; font-weight: 500; color: var(--kn-text-2); white-space: nowrap; text-align: center; transition: color 250ms; }
|
||||
.kn-t[aria-selected="true"] { color: var(--kn-text); font-weight: 600; }
|
||||
/* Fokusring: .kn [tabindex]:focus-visible und .kn button:focus-visible (module.css, 0-3-0 und 0-2-1, später geladen) setzen +2px und schlugen die alte Regel
|
||||
(0-2-0); der Ring lag außerhalb und wurde vom Balken (overflow) abgeschnitten. Mit .kn und [role="tab"] (0-4-0) bleibt er innen. Der aktive Reiter liegt
|
||||
auf dem schwarzen Gleiter: dort zeichnet der Gleiter einen hellen Innenring. */
|
||||
(0-2-0); der Ring lag außerhalb und wurde vom Balken (overflow) abgeschnitten. Mit .kn und [role="tab"] (0-4-0) bleibt er innen. */
|
||||
.kn .kn-t[role="tab"]:focus-visible { outline-offset: -2px; border-radius: 6px; }
|
||||
.kn .kn-balken:has(.kn-t:focus-visible) .kn-gleiter { outline: 2px solid #fff; outline-offset: -3px; }
|
||||
.kn .kn-fb .kn-t[aria-selected="true"]:focus-visible { outline-color: #fff; }
|
||||
.kn-schrift-klein .kn-t { font-size: 14px; }
|
||||
.kn-overlay .kn-t { color: #fff; }
|
||||
|
||||
.kn-l { position: relative; line-height: 20px; }
|
||||
.kn-zahl { position: absolute; top: -9px; left: 100%; min-width: 16px; height: 16px; margin-left: -6px; padding: 0 4px; border-radius: 8px; background: #b3093c; color: #fff; font-size: 11px; line-height: 16px; font-weight: 700; text-align: center; box-shadow: 0 0 0 2px var(--kn-bg); animation: kn-zahl-ein 400ms cubic-bezier(.3,1.5,.5,1); }
|
||||
.kn-overlay .kn-zahl { box-shadow: 0 0 0 2px var(--kn-accent); }
|
||||
.kn-reiter-t .kn-zahl { top: -7px; }
|
||||
.kn-zahl--weg { transform: scale(0); transition: transform 250ms; animation: none; }
|
||||
@keyframes kn-zahl-ein { from { transform: scale(0); } }
|
||||
|
||||
@@ -30,6 +30,7 @@ export const ETAGE_ANSICHTEN = {
|
||||
gutschrift: ['/neu/ansicht-gutschrift.js', 'zeigeGutschrift', 'etage-body'],
|
||||
wechsel: ['/neu/ansicht-wechsel.js', 'zeigeWechsel', 'etage-body'],
|
||||
bestellen: ['/neu/ansicht-bestellen.js', 'zeigeBestellen', 'etage-body'],
|
||||
speicher: ['/neu/ansicht-speicher.js', 'zeigeSpeicher', 'etage-body'], // Navigation leichter V7: Unterseite statt Reiter E-Mail
|
||||
}
|
||||
// Gemeinsame Funktionsmodule, wörtlich aus app.js:1938-1945 (Wurzel: der Behälter mit der alten ID)
|
||||
export const FUNKTIONS_MODULE = {
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben.
|
||||
Reihenfolge der Zeilen (V3): Passwort ändern, Datenschutz, Impressum, App installieren (nur wenn sinnvoll), Verträge kündigen,
|
||||
Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden. Namen und Mails kommen nur als Text in die Seite.
|
||||
Reihenfolge der Zeilen (V3, geändert mit „Navigation leichter“ V8): Passwort ändern, App installieren (nur wenn sinnvoll), Verträge kündigen,
|
||||
Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden; darunter klein Datenschutz und Impressum. Namen und Mails kommen nur als Text in die Seite.
|
||||
Das Sheet ist ein Menü: Esc, ein Tipp aufs Dunkle und „Schließen“ schließen es; Zurück schließt es über das Overlay des Routers. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
@@ -22,17 +22,17 @@ export function installAngebot({ handy, standalone, prompt, ios }) {
|
||||
|
||||
const HINWEIS_IOS = 'Tippe auf Teilen und dann auf Zum Home-Bildschirm.'
|
||||
|
||||
/** Zeilen des Kontos in der Reihenfolge von V3. */
|
||||
/** Zeilen des Kontos: vier volle Zeilen, Datenschutz und Impressum klein darunter (klein: true). */
|
||||
export function kontoEintraege({ fristen, install } = {}) {
|
||||
const frist = fristen?.anzeigen === true
|
||||
return [
|
||||
{ id: 'passwort', text: 'Passwort ändern', href: '#passwort' },
|
||||
{ id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true },
|
||||
{ id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true },
|
||||
...(install ? [{ id: 'installieren', text: 'App installieren', ...(install === 'ios' ? { hinweis: HINWEIS_IOS } : {}) }] : []),
|
||||
{ id: 'kuendigen', text: 'Verträge kündigen', href: '/kuendigen.html', betont: true },
|
||||
{ id: 'widerrufen', text: 'Vertrag widerrufen', href: '/widerrufen.html', betont: true, ...(frist ? { pille: 'Frist läuft' } : {}) },
|
||||
{ id: 'abmelden', text: 'Abmelden' },
|
||||
{ id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true, klein: true },
|
||||
{ id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true, klein: true },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function baueKonto({ me, fristen, install, onAbmelden, onInstall, overlay
|
||||
|
||||
function zeile(e) {
|
||||
const inhalt = [h('span', { class: 'kn-konto-text' }, e.text, e.hinweis ? h('span', { class: 'kn-konto-hinweis' }, e.hinweis) : null), e.pille ? h('span', { class: 'kn-pille kn-pille-orange' }, e.pille) : null]
|
||||
const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}`
|
||||
const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}${e.klein ? ' kn-konto-zeile--klein' : ''}`
|
||||
if (e.href === undefined) return h('button', { class: klasse, type: 'button', 'data-konto': e.id, on: { click: () => (e.id === 'abmelden' ? onAbmelden() : onInstall?.()) } }, inhalt)
|
||||
const intern = e.href.startsWith('#')
|
||||
return h('a', { class: klasse, href: e.href, 'data-konto': e.id, target: e.neuerTab ? '_blank' : null, rel: e.neuerTab ? 'noopener' : null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und schwarzem Balken (reiter.js, art 'balken').
|
||||
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und einer dünnen Linie unter dem aktiven Wort (reiter.js, art 'balken').
|
||||
Desktop: Menü links, Wörter ohne Icons. Beide zeigen dieselben Zähler. Dazu der reine Zustand der Bildschirmtastatur
|
||||
(Leiste und Streifen verschwinden nur bei Texteingabe) und seine Verdrahtung mit visualViewport, focusin und focusout. */
|
||||
|
||||
|
||||
+12
-9
@@ -109,14 +109,15 @@
|
||||
.kn .tm-reiter:focus-visible { outline-offset: -2px; }
|
||||
.kn .tm-panel:focus { outline: none; }
|
||||
|
||||
/* ---- Werkzeugleiste: Ansicht, Pfeile, Sprung, Filter ---- */
|
||||
/* ---- Werkzeugleiste: Ansicht, Pfeile, Sprung, Filter ----
|
||||
Navigation leichter (V5): Schwarz nur für die Hauptaktion. Die Ansicht (Tag/Woche/Monat) ist ein hellgrauer Umschalter mit weißer Auswahl,
|
||||
die Filter sind leise Chips mit grauer Auswahl; schwarz bleibt „+ Termin eintragen“. */
|
||||
.kn .tm-leiste { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; }
|
||||
.kn .tm-leiste [hidden], .kn .tm-filter[hidden] { display: none; }
|
||||
.kn .tm-segment { display: inline-flex; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); overflow: hidden; }
|
||||
.kn .tm-seg { min-height: 32px; padding: 0 14px; border: 0; background: none; color: var(--kn-text); font: inherit; font-weight: 500; cursor: pointer; transition: background-color .12s; }
|
||||
.kn .tm-seg + .tm-seg { box-shadow: -1px 0 0 var(--kn-line); }
|
||||
.kn .tm-seg:hover { background: var(--kn-bg-hover); }
|
||||
.kn .tm-seg[aria-pressed='true'] { background: var(--kn-accent); color: #fff; }
|
||||
.kn .tm-segment { display: inline-flex; padding: 2px; border-radius: 8px; background: var(--kn-bg-subtle); box-shadow: inset 0 0 0 1px var(--kn-line); overflow: hidden; }
|
||||
.kn .tm-seg { min-height: 28px; padding: 0 14px; border: 0; border-radius: 6px; background: none; color: var(--kn-text-2); font: inherit; font-weight: 500; cursor: pointer; transition: background-color .12s, color .12s; }
|
||||
.kn .tm-seg:hover { color: var(--kn-text); }
|
||||
.kn .tm-seg[aria-pressed='true'] { background: var(--kn-bg); color: var(--kn-text); font-weight: 600; box-shadow: var(--kn-ring); }
|
||||
.kn .tm-nav { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.kn .tm-heute { min-width: 72px; }
|
||||
.kn .tm-pfeil { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); font: inherit; font-size: 18px; line-height: 1; cursor: pointer; }
|
||||
@@ -124,9 +125,9 @@
|
||||
.kn .tm-sprung { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn .tm-zeitraum { margin: 20px 0 8px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||
.kn .tm-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||
.kn .tm-chip { min-height: 28px; padding: 0 12px; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color .12s; }
|
||||
.kn .tm-chip:hover { background: var(--kn-bg-hover); }
|
||||
.kn .tm-chip[aria-pressed='true'] { background: var(--kn-accent); color: #fff; }
|
||||
.kn .tm-chip { min-height: 28px; padding: 0 10px; border: 0; border-radius: var(--kn-radius); background: none; color: var(--kn-text-2); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color .12s, color .12s; }
|
||||
.kn .tm-chip:hover { background: var(--kn-bg-hover); color: var(--kn-text); }
|
||||
.kn .tm-chip[aria-pressed='true'] { background: var(--kn-grau-bg); color: var(--kn-text); font-weight: 600; }
|
||||
.kn .tm-werkzeug-zeile { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
|
||||
.kn .tm-werkzeuge { display: contents; }
|
||||
.kn .tm-werkzeuge button[aria-expanded='true'] { background: var(--kn-bg-hover); }
|
||||
@@ -340,6 +341,8 @@
|
||||
.kn .tm-leiste { gap: 8px; }
|
||||
.kn .tm-segment { width: 100%; }
|
||||
.kn .tm-seg { flex: 1; padding-left: 8px; padding-right: 8px; }
|
||||
.kn .tm-filter { gap: 2px; } /* die fünf leisen Chips passen auf 390 px in eine Zeile */
|
||||
.kn .tm-chip { padding-left: 8px; padding-right: 8px; }
|
||||
.kn .tm-nav { width: 100%; }
|
||||
.kn .tm-nav .tm-heute { flex: 1; }
|
||||
.kn .tm-sprung { width: 100%; }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* Kundenansicht neu: gemeinsame Reiter. Zwei Arten mit demselben Baustein:
|
||||
'unterstrich' = Reiter einer Hauptseite (basis.css, .kn-reiter), 'balken' = Fahrstuhl-Zeile und Reiter in Etage 2:
|
||||
ein schwarzer Gleiter fährt federnd unter das aktive Wort, darin eine Overlay-Kopie der Wörter in Weiß mit Gegenbewegung
|
||||
(final-2-frei.html). Das Overlay ist aria-hidden und trägt keine Rollen. Zahlenfunktionen sind rein und werden mit node --test geprüft;
|
||||
ein Gleiter fährt federnd unter das aktive Wort, darin eine Overlay-Kopie der Wörter in Weiß mit Gegenbewegung
|
||||
(final-2-frei.html). Seit „Navigation leichter“ (V3) ist der Gleiter in huelle.css eine dünne Linie und das Overlay ausgeblendet;
|
||||
Messen und Rückfall laufen unverändert weiter. Das Overlay ist aria-hidden und trägt keine Rollen. Zahlenfunktionen sind rein und werden mit node --test geprüft;
|
||||
Messen und Bewegen braucht getBoundingClientRect und entfällt ohne Browser. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ROUTEN = Object.freeze(Object.fromEntries(Object.entries({
|
||||
wartung: { bereich: 'vertraege', reiter: 'abo' },
|
||||
rechnungen: { bereich: 'vertraege', reiter: 'rechnungen' },
|
||||
stunden: { bereich: 'vertraege', reiter: 'stunden' },
|
||||
speicher: { bereich: 'vertraege', reiter: 'email' },
|
||||
speicher: { bereich: 'vertraege', etage: 'speicher' },
|
||||
gutschrift: { bereich: 'vertraege', etage: 'gutschrift' },
|
||||
wechsel: { bereich: 'vertraege', etage: 'wechsel' },
|
||||
'bestellen-hosting': { bereich: 'vertraege', etage: 'bestellen', produkt: 'hosting' },
|
||||
@@ -42,6 +42,7 @@ const TITEL_BEREICH = { website: 'Website', funktionen: 'Funktionen', chat: 'Cha
|
||||
const TITEL_ETAGE = {
|
||||
projekt: 'Projekt', news: 'News', termine: 'Terminplaner', webshop: 'Webshop', buchungen: 'Buchungskalender', zeiten: 'Öffnungszeiten',
|
||||
analyse: 'Analyse', gutschrift: 'Übernahmegutschrift', wechsel: 'Abo wechseln', bestellen: 'Abo bestellen', passwort: 'Passwort ändern',
|
||||
speicher: 'E-Mail-Speicher',
|
||||
}
|
||||
|
||||
// Alter data-view-Name (VIEW_HASHES, app.js:1933) → Hash-Name
|
||||
@@ -107,8 +108,8 @@ export function titelFuer(route) {
|
||||
return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? ''
|
||||
}
|
||||
|
||||
/** Etagen, auf denen etwas bestellt wird: Abo bestellen, Wechsel und die Übernahmegutschrift. Dazu der Reiter E-Mail der Verträge (Zusatzspeicher). */
|
||||
const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift']
|
||||
/** Etagen, auf denen etwas bestellt wird: Abo bestellen, Wechsel, die Übernahmegutschrift und der E-Mail-Speicher (Zusatzspeicher; bis „Navigation leichter“ V7 ein Reiter der Verträge). */
|
||||
const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift', 'speicher']
|
||||
|
||||
/**
|
||||
* Steht auf dieser Seite der Widerrufsstreifen? Nicht auf den Bestellseiten: Dort müssen Kerninfo und Knopf „Zahlungspflichtig bestellen“ auf einem
|
||||
@@ -117,6 +118,5 @@ const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift']
|
||||
*/
|
||||
export function zeigtWiderrufsstreifen(route) {
|
||||
if (!route) return true
|
||||
if (BESTELL_ETAGEN.includes(route.etage)) return false
|
||||
return !(route.bereich === 'vertraege' && route.reiter === 'email')
|
||||
return !BESTELL_ETAGEN.includes(route.etage)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr } from './ansicht-hilfen.js'
|
||||
import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr, navigiere } from './ansicht-hilfen.js'
|
||||
|
||||
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
|
||||
const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.'
|
||||
@@ -113,7 +113,7 @@ function vertragsBlock(daten, sub, viewAs) {
|
||||
fakt(gekuendigt(sub) ? 'Leistungen' : 'Abrechnung', aboZeile(sub)),
|
||||
fakt('Laufzeit', laufzeitText(sub)),
|
||||
sub.offerText ? fakt('Angebot', sub.offerText) : null,
|
||||
fakt('E-Mail-Speicher', `${gb} GB${einheiten ? ` (${einheiten} Zusatzpaket${einheiten === 1 ? '' : 'e'})` : ''}`)),
|
||||
fakt('E-Mail-Speicher', [`${gb} GB${einheiten ? ` (${einheiten} Zusatzpaket${einheiten === 1 ? '' : 'e'})` : ''} · `, speicherLink()])),
|
||||
leistungen(katalog?.features),
|
||||
h('p', { class: 'kn-links' },
|
||||
h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Vertragsbedingungen'), ' · ',
|
||||
@@ -122,6 +122,12 @@ function vertragsBlock(daten, sub, viewAs) {
|
||||
sub.paymentProblem ? zahlungsKnopf(viewAs) : null)
|
||||
}
|
||||
|
||||
/** „ändern“ am E-Mail-Speicher: öffnet die Unterseite #speicher (Navigation leichter V7). Mit Zusatztaste bleibt es ein normaler Link. */
|
||||
function speicherLink() {
|
||||
const ohneZusatz = (ev) => !(ev.defaultPrevented || ev.button > 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
||||
return h('a', { href: '#speicher', 'data-aktion': 'speicher', on: { click: (ev) => { if (!ohneZusatz(ev)) return; ev.preventDefault(); navigiere('#speicher') } } }, 'ändern')
|
||||
}
|
||||
|
||||
function gutschriftLink() {
|
||||
const badge = h('span', { class: 'kn-badge', 'data-badge': true, hidden: true })
|
||||
const link = h('a', { class: `${K.knopf} kn-gutschrift-link`, ...zuAnsicht('takeover') }, 'Übernahmegutschrift beantragen')
|
||||
|
||||
Reference in new issue
Block a user