From 05a59c4d61cf0bb01846ff54c59f5033853a3ccb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:37:03 +0000 Subject: [PATCH] =?UTF-8?q?Oberfl=C3=A4che:=20Speicher-Stepper=20bei=20gep?= =?UTF-8?q?lanter=20Reduzierung,=20vollst=C3=A4ndige=20Bestell=C3=BCbersic?= =?UTF-8?q?ht,=20Hinweis=20bei=20gesperrten=20=C3=84nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- public/app.js | 82 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/public/app.js b/public/app.js index 64f8095..7ea6481 100644 --- a/public/app.js +++ b/public/app.js @@ -890,6 +890,13 @@ function renderOrderView(view, data, productKey) { const price = fmtEuro(product.amountCents / 100) const lateStart = data.lateStartAt ? fmtDateShort(data.lateStartAt) : '' const isWartung = productKey === 'wartung' + // Betrag der ersten Abbuchung: Angebotspreis während des Rabatts, sonst Katalogpreis + const offerMonthly = Number(offer?.monthlyPriceCents) + const offerRegular = Number(offer?.regularPriceCents) || product.amountCents + const offerMonths = Number(offer?.months) + const hasOfferNumbers = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0 + const firstCents = hasOfferNumbers ? offerMonthly : product.amountCents + const firstAmount = fmtEuro(firstCents / 100) view.innerHTML = `

${escapeHtml(label)}

@@ -925,9 +932,10 @@ function renderOrderView(view, data, productKey) { + @@ -944,6 +952,10 @@ function renderOrderView(view, data, productKey) { view.querySelectorAll('input[name="abo-start-choice"]').forEach((radio) => radio.addEventListener('change', () => { const el = view.querySelector('#abo-start-summary') + const due = view.querySelector('#abo-due-summary') + if (due) due.textContent = radio.value === 'sofort' + ? `Heute fällig: ${firstAmount} (gemäß § 19 UStG ohne Umsatzsteuer)` + : `Erste Abbuchung am ${lateStart || 'Ende der 14-tägigen Widerrufsfrist'}: ${firstAmount} (gemäß § 19 UStG ohne Umsatzsteuer); heute wird nichts abgebucht` if (el) el.textContent = radio.value === 'sofort' ? 'sofort, auf Ihren ausdrücklichen Wunsch vor Ablauf der Widerrufsfrist' : `nach Ablauf der 14-tägigen Widerrufsfrist${lateStart ? ` (am ${lateStart})` : ''}; bis dahin keine Leistung und keine Abbuchung` @@ -982,6 +994,13 @@ function renderOrderView(view, data, productKey) { /* Wechsel ins Wartungs-Abo (#wechsel): Vorschau vom Server, Bestellübersicht direkt über dem Knopf (§ 312j BGB), Zustimmung, Bestellung. Der Wechsel wirkt erst mit der Zahlung der Wechselrechnung (Bedingungen § 9 Abs. 3). */ const switchUi = { notice: '', loadedAt: 0, busy: false } +// Zurück aus dem Bfcache (z. B. von der Zahlungsseite): eine hängende Bestellung nicht gesperrt lassen +window.addEventListener('pageshow', () => { + if (!switchUi.busy) return + switchUi.busy = false + const view = document.getElementById('view-wechsel') + if (view && aboState.data) renderSwitchView(view, aboState.data) +}) function renderSwitchView(view, data) { const sub = data.subscription @@ -1002,7 +1021,7 @@ function renderSwitchView(view, data) { const p = state.preview const rows = [] if (p) { - const monthly = wartung.amountCents + const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents rows.push(``) if (units > 0) { rows.push(``) @@ -1011,7 +1030,9 @@ function renderSwitchView(view, data) { rows.push(``) } rows.push(``) - rows.push(``) + const nd = new Date(p.nextChargeAt) + const nextDay = Number.isNaN(nd.getTime()) ? '' : nd.toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: 'numeric' }) + rows.push(``) rows.push(``) rows.push('') } @@ -1202,22 +1223,31 @@ function renderStoragePanel(container, data) { const canChange = sub.canChangeStorage === true const current = Number(storage.units || 0) const start = pendingReduction && Number.isInteger(pendingReduction.units) ? pendingReduction.units : current - const state = { target: start, preview: null, previewError: '', loading: false, busy: false, error: '', token: 0 } + const state = { target: start, preview: null, previewError: '', loading: start !== current, busy: false, error: '', token: 0 } + const lockedReason = sub.switchPending ? 'Wechsel wartet auf die Zahlung' + : aboIsCancelled(sub) ? 'Abo gekündigt' + : sub.paymentProblem ? 'Zahlung offen' : 'bei Fragen schreiben Sie uns gern im Chat' const notice = storageUi.notice storageUi.notice = '' const paint = () => { - const delta = state.target - start + // Kosten, Beschriftung und Zustimmung richten sich nach den gebuchten Paketen; start ist nur die Anfangsstellung + const delta = state.target - current const p = state.preview let summary = '' - if (canChange && delta !== 0) { + if (canChange && (delta !== 0 || pendingReduction)) { const plus = delta > 0 + const same = delta === 0 const totalCents = state.target * unitCents const facts = [] + const proBad = plus && p && !state.loading && !(p.prorationCents != null && p.prorationCents !== '' && Number.isFinite(Number(p.prorationCents))) + if (same) { + facts.push(`
Änderung
geplante Reduzierung aufheben: Sie behalten ${current} Paket${current === 1 ? '' : 'e'} (${gbOf(current)} GB), es entstehen keine Kosten
`) + } else { facts.push(`
Neue Gesamtgröße
${gbOf(state.target)} GB
`) facts.push(`
Preis
${fmtEuro(totalCents / 100)} pro Monat für ${state.target} Paket${state.target === 1 ? '' : 'e'} (${plus ? 'Mehrpreis' : 'Ersparnis'} ${fmtEuro(Math.abs(delta) * unitCents / 100)} pro Monat; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)
`) - if (state.previewError) { - facts.push(`
Vorschau
${escapeHtml(state.previewError)}
`) + if (state.previewError || proBad) { + facts.push(`
Vorschau
${escapeHtml(state.previewError || 'Der Betrag konnte nicht berechnet werden. Bitte versuchen Sie es später erneut.')}
`) } else if (!p || state.loading) { facts.push('
Vorschau
wird berechnet …
') } else if (plus) { @@ -1231,7 +1261,8 @@ function renderStoragePanel(container, data) { facts.push(`
Wirkt zum
${fmtDateShort(p.effectiveAt)}, zum Ende des Abrechnungsmonats
`) } facts.push('
Laufzeit
monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo
') - const hint = plus + } + const hint = same ? '' : plus ? `` : `

Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räumen Sie vorher auf. Mehr dazu in den Regeln zum Zusatzspeicher und in der Widerrufsbelehrung.

` @@ -1240,7 +1271,7 @@ function renderStoragePanel(container, data) {
${facts.join('')}
${hint} - + ` } const status = [] @@ -1250,6 +1281,7 @@ function renderStoragePanel(container, data) {

E-Mail-Speicher

${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 ? `

${escapeHtml(notice)}

` : ''} + ${!canChange ? `

Änderungen sind derzeit nicht möglich – ${lockedReason}.

` : ''} ${canChange ? `
@@ -1264,18 +1296,9 @@ function renderStoragePanel(container, data) { container.querySelector('#storage-submit')?.addEventListener('click', submit) } - const step = async (d) => { - const next = state.target + d - if (state.busy || next < 0 || next > maxUnits) return - state.target = next - state.preview = null - state.previewError = '' - state.loading = next !== start - const token = ++state.token - paint() - if (next === start) return + const loadPreview = async (token) => { try { - const p = await api('/api/abo/storage/preview', { method: 'POST', body: JSON.stringify({ units: next }) }) + const p = await api('/api/abo/storage/preview', { method: 'POST', body: JSON.stringify({ units: state.target }) }) if (token !== state.token) return state.preview = p } catch (err) { @@ -1286,10 +1309,22 @@ function renderStoragePanel(container, data) { paint() } + const step = (d) => { + const next = state.target + d + if (state.busy || next < 0 || next > maxUnits) return + state.target = next + state.preview = null + state.previewError = '' + state.loading = next !== current + const token = ++state.token + paint() + if (next !== current) loadPreview(token) + } + const submit = async () => { const errEl = container.querySelector('#storage-error') hideError(errEl) - const plus = state.target > start + const plus = state.target > current const terms = container.querySelector('#storage-terms') if (plus && !terms?.checked) { showError(errEl, 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.') @@ -1327,6 +1362,9 @@ function renderStoragePanel(container, data) { } paint() + if (start !== current && canChange) { + loadPreview(++state.token) + } } /* "Postfach anfragen": öffnet den Chat mit vorbereiteter Nachricht (Eingabefeld wird nicht automatisch gesendet) */
Leistung${escapeHtml(product.name)}: ${(product.features || []).map(([t]) => escapeHtml(t)).join(', ')}
Preis${offer - ? `${escapeHtml(offer.text)} (individuelles Angebot; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten` + ? `${hasOfferNumbers ? `${escapeHtml(fmtEuro(offerMonthly / 100))} pro Monat für die ersten ${escapeHtml(String(offerMonths))} Monate, danach ${escapeHtml(fmtEuro(offerRegular / 100))} pro Monat` : ''} (individuelles Angebot: ${escapeHtml(offer.text)}; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten` : `${escapeHtml(price)} pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten`}
Abrechnungmonatlich im Voraus, erstmals zum Leistungsbeginn
FälligBitte oben wählen: sofort oder nach Ablauf der 14-tägigen Widerrufsfrist
LeistungsbeginnBitte oben wählen: sofort oder nach Ablauf der 14-tägigen Widerrufsfrist${lateStart ? ` (am ${lateStart})` : ''}
LaufzeitMindestlaufzeit ${escapeHtml(String(product.minTermMonths))} Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar
ZahlungKreditkarte oder SEPA-Lastschrift über Stripe
Wartungs-Abo${escapeHtml(fmtEuro(monthly / 100))} pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer); enthalten sind Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat
Zusatzspeicher${units} Paket${units === 1 ? '' : 'e'} (+${escapeHtml(String(units * (Number(mail.gbPerUnit) || 0)))} GB) bleiben gebucht: ${escapeHtml(fmtEuro(units * unitCents / 100))} pro Monat zusätzlich (gemäß § 19 UStG ohne Umsatzsteuer), insgesamt ${escapeHtml(fmtEuro((monthly + units * unitCents) / 100))} pro Monat
Gutschrift für nicht genutzte Tage−${escapeHtml(fmtEuro(p.creditCents / 100))} aus dem bereits bezahlten Hosting-Abo (gemäß § 19 UStG ohne Umsatzsteuer)
Heute fällig${escapeHtml(fmtEuro(p.amountDueCents / 100))} (gemäß § 19 UStG ohne Umsatzsteuer)
Neuer AbbuchungstagAb dem Wechsel jeweils am ${escapeHtml(String(new Date(p.nextChargeAt).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: 'numeric' })))}. eines Monats; nächste Abbuchung am ${fmtDateShort(p.nextChargeAt)}
Neuer Abbuchungstag${nextDay ? `Ab dem Wechsel jeweils am ${escapeHtml(nextDay)}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDateShort(p.nextChargeAt)}
LaufzeitMindestlaufzeit bis ${fmtDateShort(p.minTermEnd)} (12 Monate ab dem Wechsel), danach monatlich kündbar
BeginnDer Wechsel wird erst mit der Zahlung wirksam. Bei Kreditkarte, sobald die Zahlung erfolgreich ist (ggf. nach Bestätigung bei Ihrer Bank); bei SEPA-Lastschrift sofort mit der Bestellung, wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. Einen späteren Beginn gibt es nicht.