Files
Webklar-Kundenbereich/public/legal-form.js
T
ClaudeandClaude Opus 5.5 65429aa3ac Wartungs-Abo (50 EUR/Monat) ueber Stripe mit Stundenkonto im Portal
- Bestellung auf der Portal-Startseite: Zusammenfassung, AGB- und
  Vorab-Leistungs-Zustimmung, Button 'Zahlungspflichtig bestellen',
  danach Stripe Checkout (Abo, Kleinunternehmer-Fusszeile)
- Stripe-Webhook /api/abo/webhook spiegelt Abos nach Appwrite
  (subscriptions) und schreibt je bezahlter Rechnung 60 min gut
  (hourCredits, idempotent ueber Invoice-ID, 12 Monate gueltig)
- Stundenkonto: FIFO-Verbrauch aus Worksheets mit countsToPlan,
  15-Minuten-Takt, Mehraufwand separat ausgewiesen
- Stripe-Kundenportal fuer Zahlungsart/Rechnungen/Kuendigung
- Oeffentlicher Kuendigungsbutton (§ 312k) und Widerrufsbutton
  (§ 356a) mit Eingangsbestaetigung per Mail
- Abo-Bedingungen inkl. Leistungsbeschreibung und Widerrufsbelehrung,
  als Anhang der Vertragsbestaetigung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:04:45 +00:00

58 lines
2.5 KiB
JavaScript

// Gemeinsame Logik fuer Kuendigungs- (§ 312k BGB) und Widerrufsbutton (§ 356a BGB).
// Oeffentlich, ohne Login. Zweistufig: Einstiegsbutton -> Formular -> Bestaetigungsbutton.
function initLegalForm({ endpoint, fields }) {
const intro = document.getElementById('intro')
const form = document.getElementById('decl-form')
const done = document.getElementById('done')
const errEl = document.getElementById('form-error')
document.getElementById('start-btn').addEventListener('click', () => {
intro.classList.add('hidden')
form.classList.remove('hidden')
form.querySelector('input').focus()
})
const reasonWrap = document.getElementById('reason-wrap')
form.querySelectorAll('[name="kind"]').forEach((r) =>
r.addEventListener('change', () => {
reasonWrap?.classList.toggle('hidden', form.kind.value !== 'ausserordentlich')
})
)
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
errEl.classList.add('hidden')
const payload = {}
for (const f of fields) payload[f] = form.elements[f]?.value?.trim?.() ?? form.elements[f]?.value ?? ''
const btn = form.querySelector('button[type="submit"]')
btn.disabled = true
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(data.error || `Fehler ${res.status}`)
const at = new Date(data.receivedAt).toLocaleString('de-DE', { dateStyle: 'full', timeStyle: 'short' })
const esc = (s) => String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c])
document.getElementById('done-at').textContent = at
document.getElementById('done-list').innerHTML = fields
.filter((f) => payload[f])
.map((f) => `<dt>${esc(form.querySelector(`[data-label-for="${f}"]`)?.dataset.label || f)}</dt><dd>${esc(
form.elements[f]?.type === 'radio' || form.elements[f] instanceof RadioNodeList
? form.querySelector(`[name="${f}"]:checked`)?.dataset.text || payload[f]
: payload[f]
)}</dd>`)
.join('')
form.classList.add('hidden')
done.classList.remove('hidden')
window.scrollTo({ top: 0, behavior: 'smooth' })
} catch (err) {
errEl.textContent = err.message
errEl.classList.remove('hidden')
btn.disabled = false
}
})
}