From c600118374fbae578ed3778286082e1fb6ca069e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 22:42:37 +0000 Subject: [PATCH] =?UTF-8?q?Abo-Angebote:=20G=C3=BCltigkeit=20=C3=BCberall?= =?UTF-8?q?=20sichtbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das "Angebot gültig bis" stand bisher nur in der Angebotsliste, auf der Bestellseite und in der Mail. Jetzt auch: - Admin, Formular "Neues Angebot": eigene Zeile "Angebot gültig bis TT.MM.JJJJ" in der Vorschau, in der Bestätigungsabfrage und in der Erfolgsmeldung (bei laufendem Abo nicht, dort gilt das Angebot sofort) - Kundenbereich, Abo-Karte (Seitenleiste bzw. Tab "Wartung"): Zeile "Angebot gültig bis TT.MM.JJJJ" unter dem Preis Das Datum aus dem Datumsfeld wird ohne Date-Umweg formatiert, im Portal nach Berliner Kalender (wie bisher). Nebenbei: "danach 50 €" mit geschütztem Leerzeichen, damit das € auf dem Handy nicht allein umbricht. Co-Authored-By: Claude Opus 5.5 --- public/admin.js | 11 ++++++++--- public/app.js | 3 ++- public/kundenbereich.css | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/public/admin.js b/public/admin.js index 0fc430a..d0b6741 100644 --- a/public/admin.js +++ b/public/admin.js @@ -802,6 +802,9 @@ function offerPreviewText(price, months, regular, running) { return `${lead} ${amount}, danach ${eur(regular)} pro Monat` } +// Datumsfeld "2026-10-27" -> "27.10.2026", ohne Umweg über Date (keine Zeitzonen-Verschiebung) +const fmtYmd = (ymd) => String(ymd || '').split('-').reverse().join('.') + function showOfferMessage(text) { const el = document.getElementById('offer-message') el.textContent = text @@ -899,6 +902,7 @@ function renderOfferPreview(form) { } if (ok) { lines.push(`${offerPreviewText(v.monthlyPriceCents, v.months, regular, running)} · Ersparnis ${euroFromCents((regular - v.monthlyPriceCents) * v.months)}`) + if (!running) lines.push(`Angebot gültig bis ${fmtYmd(v.validUntil)}`) } lines.push(running ? `Laufendes Abo: gilt ab der nächsten Abbuchung${ctx.subscription.currentPeriodEnd ? ` (${fmtDate(ctx.subscription.currentPeriodEnd)})` : ''} und wird sofort in Stripe angewendet.` @@ -930,8 +934,9 @@ async function saveOffer(form) { errEl.classList.add('hidden') const v = offerFormValues(form) const ctx = offerState.context - const text = offerPreviewText(v.monthlyPriceCents, v.months, ctx.regularPriceCents, Boolean(ctx.subscription?.active)) - if (!window.confirm(`Angebot für ${ctx.customer.name || 'den Kunden'} anlegen?\n\n${text}\n\nDer Coupon wird sofort in Stripe angelegt${ctx.subscription?.active ? ' und auf das laufende Abo angewendet' : ''}.`)) return + const running = Boolean(ctx.subscription?.active) + const text = offerPreviewText(v.monthlyPriceCents, v.months, ctx.regularPriceCents, running) + if (!window.confirm(`Angebot für ${ctx.customer.name || 'den Kunden'} anlegen?\n\n${text}${running ? '' : `\nGültig bis ${fmtYmd(v.validUntil)}`}\n\nDer Coupon wird sofort in Stripe angelegt${running ? ' und auf das laufende Abo angewendet' : ''}.`)) return const save = form.querySelector('#offer-save') save.disabled = true try { @@ -939,7 +944,7 @@ async function saveOffer(form) { const o = result.offer form.classList.add('hidden') showOfferMessage([ - `Angebot ${o.ref} für ${o.customerName} angelegt: ${o.text}.`, + `Angebot ${o.ref} für ${o.customerName} angelegt: ${o.text}${o.scope === 'laufend' ? '' : `, gültig bis ${fmtDate(o.validUntil)}`}.`, `Stripe-Coupon ${o.stripeCouponId} ist eingerichtet${o.status === 'aktiv' ? ' und am laufenden Abo aktiv' : ''}.`, v.notifyCustomer ? (result.mailSent ? 'Die E-Mail an den Kunden ist raus.' : `E-Mail an den Kunden fehlgeschlagen (${result.mailError}).`) : '', ].filter(Boolean).join(' ')) diff --git a/public/app.js b/public/app.js index 4929d4a..2ac816e 100644 --- a/public/app.js +++ b/public/app.js @@ -803,8 +803,9 @@ function renderAboSideOffer(card, data, { ended }) {

Wartungs-Abo

${offer ? 'Ihr persönliches Angebot' : ''}

${offer - ? `${euroShort(offer.monthlyPriceCents)}${offer.months === 1 ? 'im ersten Monat' : `pro Monat in den ersten ${offer.months} Monaten`}, danach 50 €` + ? `${euroShort(offer.monthlyPriceCents)}${offer.months === 1 ? 'im ersten Monat' : `pro Monat in den ersten ${offer.months} Monaten`}, danach 50 €` : '50 €pro Monat'}

+ ${offer?.validUntil ? `

Angebot gültig bis ${fmtDateShort(offer.validUntil)}

` : ''}

${data.available diff --git a/public/kundenbereich.css b/public/kundenbereich.css index 56d2e19..da3959c 100644 --- a/public/kundenbereich.css +++ b/public/kundenbereich.css @@ -122,6 +122,8 @@ display: inline-block; margin: 0 0 10px; padding: 3px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 700; color: #86efac; background: rgba(74, 222, 128, 0.1); border: 1px solid rgba(74, 222, 128, 0.4); } +.kb-offer-until { margin: -6px 0 14px; font-size: 12.5px; color: var(--kb-muted); } +.kb-offer-until strong { color: #fff; } .kb-fine { margin: 10px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--kb-faint); } .kb-fine a { color: #fff; font-weight: 700; } .kb-status { display: inline-block; padding: 3px 11px; border-radius: 999px; font-size: 12px; font-weight: 700; } @@ -713,7 +715,7 @@ button.kb-avatar:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } #view-wartung .kb-abo { padding: 20px 18px 18px; border-radius: 20px; } #view-wartung .kb-abo h3 { font-size: 22px; } #view-wartung .kb-price strong, #view-wartung .kb-hours strong { font-size: 32px; } - #view-wartung .kb-price span, #view-wartung .kb-hours span, #view-wartung .kb-sub { font-size: 14px; } + #view-wartung .kb-price span, #view-wartung .kb-hours span, #view-wartung .kb-sub, #view-wartung .kb-offer-until { font-size: 14px; } #view-wartung .kb-checks { gap: 9px; font-size: 14.5px; } #view-wartung .kb-fine { font-size: 12.5px; } #view-wartung .kb-side-alert { font-size: 13.5px; }