- 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>
58 lines
2.5 KiB
JavaScript
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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
|
|
}
|
|
})
|
|
}
|