Files
beh-rde-brief/webseite/demo.html
T
JUSNandClaude Opus 5.5 2af4b7ed41 Korrekturen aus zwei Pruefrunden vor der MBE-Vorfuehrung
Demo-Seite:
- Ampel in Kalendertagen, naechstes kommendes Datum, "Termin am"/"Frist bis"
- nie "Deutsch | Deutsch"; Beraterin bekommt immer Deutsch (d=1), das Handy
  erkennt seine Sprache selbst und speichert die PC-Vorgabe nicht dauerhaft
- Code-Feld ausserhalb von #eingabe; tolerante Code-Eingabe; Ablaufdatum
  im Banner; leeres/abgelaufenes Einrichtungs-Kontingent ohne Kaufangebot,
  mit Telefonnummer; nach Aufladen reicht ein Neuladen
- Handy-Modus: endet nach dem ersten Brief, uebersteht ein Neuladen
  (sessionStorage), keine Sackgasse ohne Zugang, ehrlicher Hinweis +
  "Noch einmal senden", wenn das Ergebnis den PC nicht erreicht
- PC wartet geduldig, solange verbunden; meldet langsame Analysen, fehlende
  Verbindung, fehlenden Zugang; Warnung beim Verlassen
- Einwilligung pro Brief, nennt Gesundheit/Herkunft/Aufenthalt
- localStorage nur ueber abgesicherten Helfer; Dateifeld wird geleert
- RTL (bdi, dir=auto), Sprachliste mit deutschem Zusatz, groesserer QR

Worker: KI-Aufruf und Router-catch liefern immer JSON mit CORS; Logs ohne
Briefinhalt; [observability]; Nutzungszaehler je Analyse (Datum, Sprache,
390 Tage); normCode + 30 Fehlversuche; Uebergabe-TTL gleitend.
Werkzeug: Auswertung pro Woche/Sprache, Eingabepruefung, --bis in der Hilfe.
Datenschutz: SCC statt DPF, "in der Regel 30 Tage", Server-Protokolle,
Nutzungszaehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:34:15 +02:00

1773 lines
86 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gratis-Demo: Behördenbrief online erklären lassen | BehördenKlar</title>
<meta name="description" content="Testen Sie BehördenKlar direkt im Browser: Brief fotografieren, hochladen, in einfacher Sprache erklärt bekommen. 2 Analysen gratis, ohne Konto.">
<meta name="keywords" content="Amtsbrief, Behördenbrief, verständlich, einfache Sprache, Jobcenter, Ausländerbehörde, Warteliste">
<meta name="author" content="BehördenKlar">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<meta name="theme-color" content="#1d4e89">
<meta name="apple-mobile-web-app-title" content="BehördenKlar">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta property="og:locale:alternate" content="tr_TR">
<meta property="og:locale:alternate" content="ar_AR">
<meta property="og:locale:alternate" content="en_US">
<meta property="og:locale:alternate" content="uk_UA">
<meta property="og:locale:alternate" content="ru_RU">
<meta property="og:locale:alternate" content="pl_PL">
<link rel="manifest" href="/manifest.json">
<meta name="google-site-verification" content="6o1sIKmK7WSloQrdTs2q4WtQsCUBB-bRa24gNXzVeFc">
<meta name="msvalidate.01" content="...">
<meta name="apple-app-site-association" content="...">
<meta name="app-links" content="...">
<meta name="target-audience" content="Alle, die schwierige Post bekommen — Privatpersonen, Selbständige, Menschen mit Deutsch als Fremdsprache">
<meta name="accessibility" content="WCAG 2.1 AA, Vorlesen, einfache Sprache, mehrsprachig">
<meta name="topics" content="civic-tech, legal-tech, accessibility, education, language-simplification, vulnerable-populations">
<meta name="target-organizations" content="Beratungsstellen, Jobcenter, Caritas, Diakonie, VHS, Migrationsberatung">
<meta name="problem-statement" content="Amtsdeutsch und Vertragssprache sind unnötig schwer. BehördenKlar macht daraus Klartext mit Frist und To-do-Liste.">
<link rel="canonical" href="https://behoerdenklar.de/demo">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<meta property="og:site_name" content="BehördenKlar">
<meta property="og:type" content="website">
<meta property="og:locale" content="de_DE">
<meta property="og:url" content="https://behoerdenklar.de/demo">
<meta property="og:title" content="Probieren Sie es aus: Ihr Behördenbrief, einfach erklärt">
<meta property="og:description" content="Direkt im Browser testen — 2 Gratis-Analysen, ohne Konto. Brief fotografieren, Klartext lesen.">
<meta property="og:image" content="https://behoerdenklar.de/vorschau.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<style>
:root {
--papier: #faf7f0;
--papier-dunkel: #f1ecdf;
--tinte: #21211d;
--grau: #6b675e;
--amtsblau: #1d4e89;
--stempelrot: #b3372b;
--marker: #ffe66b;
--gruen: #2e7d4f;
--rot: #b3372b;
--gelb: #a06b00;
color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33, 33, 29, 0.025) 34px 35px),
var(--papier);
color: var(--tinte);
line-height: 1.7;
font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte);
font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .absender a { color: inherit; text-decoration: none; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
header { padding: 40px 0 8px; }
.betreff-label {
font-size: 13px; color: var(--grau); text-transform: uppercase;
letter-spacing: 0.14em; margin-bottom: 10px;
}
h1 {
font-size: clamp(30px, 6vw, 44px);
line-height: 1.15; font-weight: 700; letter-spacing: -0.01em;
}
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.anrede { margin-top: 20px; font-size: 18px; max-width: 580px; }
section { margin-top: 44px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
h2 .nr { font-family: 'Courier New', ui-monospace, monospace; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33, 33, 29, 0.9);
padding: 28px 26px; position: relative;
}
.formular-kopf {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--grau); margin-bottom: 10px;
}
.cta {
display: inline-block; width: 100%; padding: 17px 30px;
background: var(--amtsblau); color: #fff; text-decoration: none;
font-family: inherit; font-size: 18px; font-weight: 700; text-align: center;
border: 2px solid var(--tinte);
box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
cursor: pointer;
}
.cta:hover:not(:disabled) { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.cta:disabled { opacity: 0.45; cursor: not-allowed; }
.konsens {
display: flex; gap: 12px; align-items: flex-start;
font-size: 15px; color: var(--grau); margin: 14px 0 18px;
}
.konsens input { width: 22px; height: 22px; margin-top: 4px; flex-shrink: 0; accent-color: var(--amtsblau); }
.konsens a { color: var(--amtsblau); }
.klein { font-size: 13.5px; color: var(--grau); margin-top: 12px; }
.klein a { color: var(--grau); }
.status { text-align: center; padding: 26px 10px; display: none; }
.status .gross { font-size: 20px; font-weight: 700; }
.status .sub { color: var(--grau); margin-top: 6px; font-size: 15px; }
.sanduhr {
display: inline-block; font-size: 34px;
animation: drehen 2.4s linear infinite;
}
@keyframes drehen { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.fehlerbox {
display: none; margin-top: 16px;
border: 2px solid var(--stempelrot); background: #fdf1ef;
color: #7c2018; padding: 14px 16px; font-size: 16px;
}
.email-teil { display: none; margin-top: 16px; }
.wl-input {
width: 100%; padding: 15px 16px; font-size: 17px; font-family: inherit;
border: 2px solid var(--tinte); background: #fff; color: var(--tinte);
margin-bottom: 12px;
}
.wl-input::placeholder { color: #9a958a; }
/* Ergebnis */
#ergebnis { display: none; }
.anruf-satz { border-inline-start: 3px solid var(--amtsblau); background: #fff; padding: 6px 12px; margin: 10px 0; }
.anruf-de { font-weight: 700; margin: 0; }
.anruf-bed { color: var(--grau); font-size: 15px; margin: 4px 0 0; }
.kontakt-zeile { margin: 6px 0; font-size: 16px; }
.az-wert { font-family: 'Courier New', ui-monospace, monospace; font-weight: 700; background: var(--marker); padding: 1px 6px; }
#ergAnrufKontakt .cta { display: inline-block; width: auto; margin: 4px 0 8px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33, 33, 29, 0.28);
padding: 22px 24px; position: relative; margin-top: 22px;
box-shadow: 4px 5px 0 rgba(33, 33, 29, 0.08);
}
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--gruen); color: var(--papier); padding: 3px 10px;
}
.karten-label.neutral { background: var(--tinte); }
.ampelzeile {
margin-top: 14px; padding-top: 12px;
border-top: 1px dashed rgba(33, 33, 29, 0.3);
font-size: 16px; font-weight: 700;
}
.todo { padding: 8px 0 8px 2px; border-bottom: 1px dotted rgba(33,33,29,0.3); font-size: 16.5px; }
.todo::before { content: '☐'; margin-inline-end: 10px; color: var(--amtsblau); font-size: 18px; }
details { border-bottom: 1px solid rgba(33, 33, 29, 0.2); padding: 12px 4px; }
summary { font-weight: 700; cursor: pointer; font-size: 16.5px; list-style: none; }
summary::-webkit-details-marker { display: none; }
details p { margin-top: 8px; color: var(--grau); font-size: 15.5px; }
.stempel {
display: inline-block; padding: 7px 14px;
border: 2.5px solid var(--stempelrot); color: var(--stempelrot);
font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase;
transform: rotate(-4deg); opacity: 0.9;
}
.hinweis-recht {
margin-top: 18px; font-size: 14px; color: var(--grau);
border-top: 1px dashed rgba(33,33,29,0.3); padding-top: 12px;
}
/* Warteliste-Formular (wie Startseite) */
.wl-form { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
.bonus-zeile {
margin: 14px 0; padding: 12px 16px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
font-size: 16px;
}
/* Handy <-> PC */
.cta-zwei { background: #fff; color: var(--amtsblau); font-size: 16.5px; padding: 13px 20px; margin-top: 12px; }
.verbunden {
display: none; margin-bottom: 16px; padding: 12px 16px;
background: #eaf1fa; border: 2px solid var(--amtsblau); font-size: 16px;
}
.qr-box { display: none; margin-top: 16px; padding: 18px; background: #fff; border: 2px dashed var(--amtsblau); text-align: center; }
.qr-box .qr { width: 290px; max-width: 100%; aspect-ratio: 1; height: auto; margin: 10px auto; }
.qr-box .qr svg { width: 100%; height: 100%; display: block; }
.qr-box ol { text-align: start; margin: 8px auto 0; max-width: 420px; padding-inline-start: 22px; font-size: 15.5px; }
.qr-box .zustand { margin-top: 12px; font-weight: 700; font-size: 16.5px; }
.qr-box .abbrechen { margin-top: 10px; background: none; border: none; color: var(--grau); text-decoration: underline; font-family: inherit; font-size: 14.5px; cursor: pointer; }
.umschalter { display: none; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.umschalter button {
font-family: inherit; font-size: 15px; padding: 6px 14px; cursor: pointer;
border: 1.5px solid var(--tinte); background: #fff; color: var(--tinte);
}
.umschalter button[aria-pressed="true"] { background: var(--tinte); color: var(--papier); }
.link-knopf { background: none; border: none; color: var(--amtsblau); font-family: inherit; font-size: inherit; text-decoration: underline; cursor: pointer; padding: 0; }
footer { margin-top: 48px; font-size: 14.5px; color: var(--grau); }
footer a { color: var(--grau); }
</style>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "BehördenKlar",
"description": "KI erklärt schwierige Briefe von Ämtern, Versicherungen, Vermietern und Banken in einfacher Sprache — mit Fristen und Handlungsanleitung",
"applicationCategory": "AccessibilityApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
},
"inLanguage": ["de", "tr", "ar", "en", "uk", "ru", "pl", "fa", "ro", "fr"],
"featureList": [
"Amtsbriefe fotografieren und erklären lassen",
"Klartext in einfacher Sprache",
"Übersetzung in über 25 Sprachen",
"Automatische Erinnerungen für Fristen",
"Fertige Antworten generieren",
"Vorlesen der Inhalte",
"Keine Anmeldung erforderlich"
],
"targetGroup": "Alle, die schwierige offizielle Post bekommen — Privatpersonen und Selbständige",
"author": {
"@type": "Organization",
"name": "BehördenKlar"
}
}
</script>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender"><a href="index.html">📬 BehördenKlar</a> <span style="font-weight:400;font-size:.62em;opacity:.6;letter-spacing:.04em;">by Webklar</span></span>
<span class="az mono">Formular D-01 · Gratis-Demo</span>
</div>
<header>
<p class="betreff-label">Betreff</p>
<h1>Probieren Sie es aus —<br><span class="markiert">mit Ihrem eigenen Brief.</span></h1>
<p class="anrede">
Kein Konto. Keine App-Installation. Laden Sie ein Foto Ihres
Behördenbriefs hoch — in etwa einer Minute lesen Sie ihn in einfacher
Sprache. <strong>2 Analysen gratis.</strong>
</p>
</header>
<section id="demo">
<h2><span class="nr">§ 1</span> Brief hochladen</h2>
<div class="antrag">
<p class="formular-kopf">Antrag auf Klartext · 2 Gratis-Analysen</p>
<p class="verbunden" id="verbundenBanner"></p>
<div id="eingabe">
<p id="kaufOk" style="display: none; background: #e7f4ea; border: 1px solid #b7dfc1; color: #1a7f37; padding: 12px 14px; border-radius: 8px; margin-bottom: 14px; font-weight: 600;">
✅ Kauf erfolgreich! Ihr Guthaben wurde aktiviert.
</p>
<p id="guthabenBanner" class="bonus-zeile" style="display: none;">
<span id="guthabenText">💳 Ihr Guthaben:</span> <strong id="guthabenZahl">0</strong> Analysen — laden Sie den nächsten Brief hoch.
<br><button type="button" class="link-knopf" id="guthabenHandyKnopf" style="font-size: 14.5px;">📱 Auch auf dem Handy nutzen</button>
</p>
<div class="qr-box" id="guthabenQrBox">
<strong>Guthaben aufs Handy holen</strong>
<div class="qr" id="guthabenQr"></div>
<p class="klein" style="margin-top: 0;">Mit der Handy-Kamera scannen. Danach können Sie Briefe auch direkt am Handy erklären lassen.</p>
</div>
<p style="margin-bottom: 14px; font-size: 15.5px;">
🌐 <label for="sprachWahl">Erklärung in dieser Sprache:</label>
<select id="sprachWahl" style="font-family: inherit; font-size: 15.5px; padding: 5px 8px; border: 1.5px solid var(--tinte); background: #fff; margin-inline-start: 6px;"></select>
</p>
<label class="konsens">
<input type="checkbox" id="konsens">
<span>
Ich bin einverstanden, dass das Foto dieses Briefs zur Erklärung
an den Server von BehördenKlar und an den KI-Dienst Anthropic
(USA) geschickt wird. Briefe können sehr persönliche Angaben
enthalten, zum Beispiel zu Gesundheit, Herkunft oder Aufenthalt.
BehördenKlar speichert den Brief nicht. Anthropic löscht ihn in
der Regel nach 30 Tagen und nutzt ihn nicht zum Training.
<a href="datenschutz.html" target="_blank">Datenschutzerklärung</a>
</span>
</label>
<div id="emailTeil" class="email-teil">
<p style="margin-bottom: 10px;">
🎉 <strong>Ihre erste Analyse ist verbraucht.</strong> Für die
zweite geben Sie bitte Ihre E-Mail-Adresse ein — so stellen wir
sicher, dass jeder genau 2 Gratis-Analysen bekommt.
</p>
<input class="wl-input" type="email" id="email" placeholder="Ihre E-Mail-Adresse" autocomplete="email">
<p class="klein" style="margin: 0 0 12px;">
Die Adresse wird nur verschlüsselt (als Prüfsumme) gespeichert,
um mitzuzählen — Sie bekommen dadurch keine E-Mails von uns.
</p>
</div>
<input type="file" id="datei" accept="image/jpeg,image/png,application/pdf" style="display: none;">
<button class="cta" id="startKnopf">📷 Brief auswählen &amp; erklären lassen</button>
<button class="cta cta-zwei" id="handyKnopf" type="button" style="display: none;">📱 Lieber mit dem Handy fotografieren</button>
<div class="qr-box" id="handyBox">
<strong>Mit dem Handy fotografieren</strong>
<div class="qr" id="handyQr"></div>
<ol>
<li>Handy-Kamera auf den Code richten und den Link öffnen.</li>
<li>Am Handy den Brief fotografieren.</li>
<li>Die Erklärung erscheint hier <em>und</em> auf dem Handy.</li>
</ol>
<label class="konsens" id="handyDeutschZeile" style="justify-content: center; margin: 14px 0 0;">
<input type="checkbox" id="handyDeutsch" checked>
<span>Hier am Bildschirm immer auf Deutsch zeigen, am Handy in der Sprache der Person</span>
</label>
<p class="zustand" id="handyZustand">⏳ Warte auf das Handy …</p>
<p class="klein" style="margin-top: 6px;">Verschlüsselt: Den Schlüssel kennt nur dieser QR-Code. Der Code gilt 15 Minuten.</p>
<button type="button" class="abbrechen" id="handyAbbrechen">Abbrechen</button>
</div>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAESi2p41B915R4hW" data-action="turnstile-spin-v1" style="margin-top: 14px;"></div>
<script>
// Lokaler Test (wrangler dev): offizieller Turnstile-Testschlüssel, der immer durchlässt
if (/^(localhost|127\.0\.0\.1)$/.test(location.hostname)) {
document.querySelector('.cf-turnstile').setAttribute('data-sitekey', '1x00000000000000000000AA');
}
</script>
<p class="klein">
Geht mit Foto (JPG/PNG) oder PDF, max. 5 MB. Tipp: gutes Licht,
Brief von oben, ganzer Text im Bild.
</p>
<p class="klein">
📱 <strong>Am Handy:</strong> Wählen Sie „Galerie" oder „Dateien",
wenn Sie den Brief schon fotografiert haben. Tippen Sie auf
„Kamera", öffnet sich die Kamera-App Ihres Handys — und die fragt
nach ihren eigenen Berechtigungen. <strong>Diese Abfrage kommt von
Ihrem Handy, nicht von uns:</strong> Diese Seite greift weder auf
Kamera noch auf Mikrofon zu.
</p>
</div>
<div class="status" id="status">
<span class="sanduhr">⏳</span>
<p class="gross">Ihr Brief wird analysiert…</p>
<p class="sub">Das dauert bis zu einer Minute. Bitte lassen Sie die Seite geöffnet.</p>
</div>
<div class="fehlerbox" id="fehler"></div>
<details id="codeTeil" style="margin-top: 16px; border-top: 1px solid rgba(33, 33, 29, 0.2);">
<summary>🤝 Sie haben einen Code von einer Beratungsstelle?</summary>
<p>Dann ist BehördenKlar für Sie kostenlos. Geben Sie den Code hier ein:</p>
<input class="wl-input" id="codeFeld" placeholder="z. B. MBE-K7QX" autocapitalize="characters" autocomplete="off" spellcheck="false" style="margin-top: 10px; text-transform: uppercase;">
<button class="cta" id="codeKnopf" type="button">Code einlösen</button>
</details>
</div>
</section>
<section id="kaufBlock" style="display: none;">
<h2><span class="nr">§ 2</span> Guthaben kaufen &amp; sofort nutzen</h2>
<div class="antrag">
<p class="formular-kopf">Im Browser nutzen · ohne App Store</p>
<h3 style="font-size: 24px; line-height: 1.3;">Guthaben kaufen — sofort loslegen</h3>
<p style="margin-top: 10px;">
Sie müssen nicht erst testen: Kaufen Sie direkt Guthaben und nutzen Sie
BehördenKlar hier im Browser — auf jedem Gerät, ohne App. Ihr Guthaben
bleibt gespeichert und verfällt nicht.
</p>
<div id="paketListe" style="display: flex; flex-direction: column; gap: 12px; margin-top: 18px;"></div>
<p style="margin-top: 16px; background: #eef4fb; border-inline-start: 4px solid var(--amtsblau); padding: 12px 14px; font-size: 15.5px;">
📧 <strong>Bitte geben Sie beim Bezahlen Ihre E-Mail-Adresse an.</strong>
Sie bekommen danach einen persönlichen Zugangs-Link — damit nutzen Sie
Ihr Guthaben auf <strong>jedem Gerät</strong>: Handy, Tablet, Computer.
Ohne diesen Link ist Ihr Guthaben nur in diesem einen Browser
gespeichert und wäre weg, wenn Sie den Verlauf löschen.
</p>
<label class="konsens" style="margin-top: 18px;">
<input type="checkbox" id="kaufKonsens">
<span>
Ich verlange ausdrücklich, dass die Analysen sofort nach dem Kauf zur
Verfügung stehen, und bestätige, dass mein Widerrufsrecht damit
erlischt. Ich habe die <a href="agb.html" target="_blank">AGB</a> und
die <a href="widerruf.html" target="_blank">Widerrufsbelehrung</a>
gelesen.
</span>
</label>
<div class="fehlerbox" id="kaufFehler" style="display: none; margin-top: 12px;"></div>
<p class="klein" style="margin-top: 12px;">
🔒 Sichere Bezahlung über Stripe — wir sehen Ihre Kartendaten nie.
Keine Umsatzsteuer (§ 19 UStG). Guthaben verfällt nicht.
</p>
<p class="klein" style="margin-top: 10px;">
Anbieter: <strong>Webklar GbR</strong> — BehördenKlar ist ein Angebot von
Webklar. Auf Ihrer Kontoabrechnung erscheint deshalb
<strong>„Webklar"</strong>.
</p>
</div>
</section>
<section id="ergebnis">
<h2><span class="nr">§ 3</span> Ihr Brief in Klartext</h2>
<div class="umschalter" id="sprachUmschalter" role="group" aria-label="Sprache der Erklärung"></div>
<div class="brief-karte">
<span class="karten-label neutral" id="ergBrieftyp" dir="auto" style="font-family: inherit; letter-spacing: 0; text-transform: none;">Brief</span>
<p style="margin-top: 6px;"><strong id="ergKern" dir="auto"></strong></p>
<p class="ampelzeile" id="ergAmpel" style="display: none;"></p>
</div>
<div class="brief-karte" id="ergKalenderKarte" style="display: none;">
<span class="karten-label" id="ergKalenderLabel">Nicht vergessen</span>
<div id="ergKalenderText" style="margin-top: 6px;"></div>
<button class="cta" id="kalenderKnopf" type="button" style="margin-top: 12px;">📅 In meinen Kalender eintragen</button>
<p class="klein" id="ergKalenderHinweis">
Ihr Handy erinnert Sie <strong>3 Tage</strong> und <strong>1 Tag</strong> vorher.
Der Eintrag landet nur in Ihrem eigenen Kalender — wir speichern nichts.
</p>
</div>
<div class="brief-karte">
<span class="karten-label">Einfach erklärt</span>
<p id="ergErklaerung" dir="auto" style="white-space: pre-line;"></p>
</div>
<div class="brief-karte" id="ergChecklisteKarte" style="display: none;">
<span class="karten-label neutral">Das müssen Sie tun</span>
<div id="ergCheckliste"></div>
</div>
<div class="brief-karte" id="ergAnrufKarte" style="display: none;">
<span class="karten-label neutral">Anrufen</span>
<div id="ergAnrufKontakt" style="margin-top: 6px;"></div>
<div id="ergAnrufSaetze"></div>
</div>
<div class="brief-karte" id="ergBegriffeKarte" style="display: none;">
<span class="karten-label neutral">Schwere Wörter erklärt</span>
<div id="ergBegriffe"></div>
</div>
<div id="mitnehmenTeil" style="display: none; margin-top: 22px;">
<button class="cta cta-zwei" id="mitnehmenKnopf" type="button">📱 Ergebnis aufs Handy mitnehmen</button>
<div class="qr-box" id="mitnehmenBox">
<strong>Ergebnis aufs Handy</strong>
<div class="qr" id="mitnehmenQr"></div>
<p class="klein" style="margin-top: 0;">Mit der Handy-Kamera scannen. Dort gibt es den Kalender-Eintrag und die Anruf-Hilfe. Verschlüsselt, der Code gilt 15 Minuten.</p>
</div>
</div>
<p class="hinweis-recht">
⚖️ BehördenKlar erklärt Briefe, ersetzt aber keine Rechtsberatung. Bei
wichtigen Entscheidungen hilft eine Beratungsstelle oder ein Anwalt.
</p>
</section>
<section id="werbung" style="display: none;">
<h2><span class="nr">§ 4</span> Wie geht es weiter?</h2>
<div class="antrag">
<p class="formular-kopf" id="restZeile">Warteliste</p>
<h3 id="wlTitel" style="font-size: 24px; line-height: 1.3;">Das hat Ihnen geholfen?</h3>
<p style="margin-top: 10px;">
Die App kann noch mehr: Briefe <strong>übersetzen in über 25 Sprachen</strong>,
Fristen mit <strong>Erinnerung</strong>, fertige
<strong>Antwort-Entwürfe</strong> — und Ihre Briefe bleiben
verschlüsselt auf Ihrem Handy gespeichert.
</p>
<p class="bonus-zeile">
🎁 Warteliste = <strong>5 Gratis-Analysen</strong> in der App statt 3.
</p>
<p id="nochmalZeile" style="display: none; margin-bottom: 14px;">
<a href="#demo" style="color: var(--amtsblau); font-weight: 700;">↑ Noch einen
Brief testen? Ihre zweite Gratis-Analyse wartet oben.</a>
</p>
<form class="wl-form" action="https://86550585.sibforms.com/serve/MUIFAAvOQCpFg148k-xD2Ze2PDVKKaFEIaLECCCMe9fGKd_uIurPljyn_IhTrkR0EkM_pQp_8VBzcHZ6N9sKDX5GBZi9elYKN6isXGPUUxcAgO37OKhTFZyMlJiC9ikRmPI8Q2tU2GvqP6esJ5ReGfv1htZV_OfXRIgW59P6pkeUsYKxqdXA7nWOaWxNTYz4MzzoGg7czJMJ2fxuEg==" method="POST">
<input class="wl-input" style="margin-bottom: 0;" type="email" name="EMAIL" id="wlEmail" placeholder="Ihre E-Mail-Adresse" required autocomplete="email">
<input type="text" name="email_address_check" value="" style="position:absolute; left:-5000px;" tabindex="-1" autocomplete="off" aria-hidden="true">
<input type="hidden" name="locale" value="de">
<input type="hidden" name="html_type" value="simple">
<button class="cta" type="submit">Auf die Warteliste eintragen →</button>
</form>
<p class="klein">
Eine E-Mail zum Start, keine Werbung, jederzeit abmeldbar —
<a href="datenschutz.html">Datenschutzerklärung</a>.
</p>
<div style="margin-top: 16px;"><span class="stempel">Bald im App Store</span></div>
</div>
</section>
<footer>
<a href="index.html">← Zur Startseite</a> ·
<a href="agb.html">AGB</a> ·
<a href="widerruf.html">Widerruf</a> ·
<a href="impressum.html">Impressum</a> ·
<a href="datenschutz.html">Datenschutzerklärung</a>
</footer>
</div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script src="qrcode.js"></script>
<script src="uebergabe.js"></script>
<script>
(function () {
'use strict';
var BASIS = /^(localhost|127\.0\.0\.1)$/.test(location.hostname)
? 'http://localhost:8799' // lokaler Test mit `wrangler dev`
: 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev';
var ENDPUNKT = BASIS + '/demo';
var KAUFEN_ENDPUNKT = BASIS + '/kaufen';
var STATUS_ENDPUNKT = BASIS + '/kauf-status';
var MAX_KANTE = 2000; // wie in der App: reicht für A4, spart Datenvolumen
// Guthaben-Pakete (Stripe TEST-Preise). Für Live später die Live-price_…-IDs.
var PAKETE = [
{ priceId: 'price_1U6BNnBsRA1AOdnedGW5eefh', titel: '5 Analysen', preis: '2,99 €' },
{ priceId: 'price_1U6BPRBsRA1AOdnemDUalzb8', titel: '15 Analysen', preis: '6,99 €', best: true },
];
// Browser-Speicher abgesichert: Manche Browser (Safari „Alle Cookies
// blockieren", App-interne Browser) werfen schon beim Zugriff einen Fehler.
// Dann merken wir uns die Werte nur, solange die Seite offen ist.
var speicher = (function () {
var ersatz = {};
return {
get: function (k) { try { return window.localStorage.getItem(k); } catch (e) { return k in ersatz ? ersatz[k] : null; } },
set: function (k, v) { try { window.localStorage.setItem(k, v); } catch (e) { ersatz[k] = String(v); } },
remove: function (k) { try { window.localStorage.removeItem(k); } catch (e) { delete ersatz[k]; } },
};
})();
// Zielsprache: kommt als ?sprache=tr von den Sprachseiten, wird gemerkt.
var SPRACH_SCHLUESSEL = 'bk_sprache';
var SPRACH_DEUTSCH = {
tr:'Türkisch', ar:'Arabisch', ru:'Russisch', uk:'Ukrainisch', pl:'Polnisch', ro:'Rumänisch',
en:'Englisch', fa:'Farsi/Dari', ku:'Kurdisch', bg:'Bulgarisch', hr:'Kroatisch', sr:'Serbisch',
bs:'Bosnisch', sq:'Albanisch', it:'Italienisch', el:'Griechisch', es:'Spanisch', pt:'Portugiesisch',
fr:'Französisch', hu:'Ungarisch', vi:'Vietnamesisch', hi:'Hindi', ur:'Urdu', ps:'Paschtu',
ti:'Tigrinya', so:'Somali', zh:'Chinesisch'
};
var SPRACH_NAMEN = {
de:'Deutsch', tr:'Türkçe', ar:'العربية', ru:'Русский', uk:'Українська', pl:'Polski',
ro:'Română', en:'English', fa:'فارسی', ku:'Kurdî', bg:'Български', hr:'Hrvatski',
sr:'Српски', bs:'Bosanski', sq:'Shqip', it:'Italiano', el:'Ελληνικά', es:'Español',
pt:'Português', fr:'Français', hu:'Magyar', vi:'Tiếng Việt', hi:'हिन्दी', ur:'اردو',
ps:'پښتو', ti:'ትግርኛ', so:'Soomaali', zh:'中文'
};
var sitzungsSprache = null; // vom PC per QR vorgegeben — gilt nur, solange die Seite offen ist
function zielsprache() {
if (sitzungsSprache) return sitzungsSprache;
var p = new URLSearchParams(window.location.search).get('sprache');
if (p && SPRACH_NAMEN[p]) { speicher.set(SPRACH_SCHLUESSEL, p); return p; }
var g = speicher.get(SPRACH_SCHLUESSEL); if (g && SPRACH_NAMEN[g]) return g;
// RA4: Sprache des HANDYS vorschlagen (nicht speichern — eine Auswahl von Hand gewinnt).
// Nicht am PC: Dort wählt die Beraterin die Sprache ihres Gegenübers.
var amPc = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
var liste = amPc ? [] : (navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language || ''];
for (var i = 0; i < liste.length; i++) {
var kurz = String(liste[i] || '').toLowerCase().split('-')[0];
if (kurz === 'de') break;
if (SPRACH_NAMEN[kurz]) return kurz;
}
return 'de';
}
var konsens = document.getElementById('konsens');
var startKnopf = document.getElementById('startKnopf');
var dateiFeld = document.getElementById('datei');
var emailTeil = document.getElementById('emailTeil');
var emailFeld = document.getElementById('email');
var status = document.getElementById('status');
var eingabe = document.getElementById('eingabe');
var fehlerBox = document.getElementById('fehler');
var kaufBlock = document.getElementById('kaufBlock');
var werbung = document.getElementById('werbung');
var guthabenBanner = document.getElementById('guthabenBanner');
var guthabenZahl = document.getElementById('guthabenZahl');
var guthabenText = document.getElementById('guthabenText');
var codeTeil = document.getElementById('codeTeil');
var codeFeld = document.getElementById('codeFeld');
var codeKnopf = document.getElementById('codeKnopf');
var kaufOk = document.getElementById('kaufOk');
var paketListe = document.getElementById('paketListe');
var kaufKonsens = document.getElementById('kaufKonsens');
var kaufFehler = document.getElementById('kaufFehler');
// Wallet (passwortloses Guthaben) — Token + zuletzt bekannter Stand lokal.
var WALLET_SCHLUESSEL = 'bk_wallet';
var GUTHABEN_SCHLUESSEL = 'bk_guthaben';
function walletToken() { return speicher.get(WALLET_SCHLUESSEL) || ''; }
function setzeWallet(t) { speicher.set(WALLET_SCHLUESSEL, t); }
function guthaben() { return parseInt(speicher.get(GUTHABEN_SCHLUESSEL) || '0', 10); }
function setzeGuthaben(n) { speicher.set(GUTHABEN_SCHLUESSEL, String(Math.max(0, n | 0))); }
function hatGuthaben() { return !!walletToken() && guthaben() > 0; }
// Einrichtungs-Code: Name der Beratungsstelle, über die das Guthaben läuft
var EINRICHTUNG_SCHLUESSEL = 'bk_einrichtung';
function einrichtung() { return speicher.get(EINRICHTUNG_SCHLUESSEL) || ''; }
// BE3: Ablaufdatum des Einrichtungs-Zugangs (kommt beim Einlösen vom Server)
var ABLAUF_SCHLUESSEL = 'bk_einrichtung_bis';
var CODE_SCHLUESSEL = 'bk_einrichtung_code';
function einrichtungBis() { return speicher.get(ABLAUF_SCHLUESSEL) || ''; }
function setzeEinrichtung(name) {
if (name) speicher.set(EINRICHTUNG_SCHLUESSEL, name);
else { speicher.remove(EINRICHTUNG_SCHLUESSEL); speicher.remove(ABLAUF_SCHLUESSEL); speicher.remove(CODE_SCHLUESSEL); }
}
// Handy ↔ PC: Ist dieses Gerät gerade das Handy, das für einen PC
// fotografiert? Ein Guthaben aus dem QR-Code (w) gilt nur für diese
// Sitzung und wird hier NICHT gespeichert.
var handyModus = null;
var leseModus = false; // Ergebnis vom PC übernommen, nur anzeigen
var istPc = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
function walletFuerAnalyse() {
if (handyModus && handyModus.w) return handyModus.w;
return hatGuthaben() ? walletToken() : '';
}
/** An den PC melden. Liefert true/false, ob es geklappt hat (Fehler werden nicht geworfen). */
function meldeAnPc(daten, fach) {
if (!handyModus) return Promise.resolve(false);
return BKUebergabe.senden(handyModus.ue, fach || 'status', daten)
.then(function () { return true; }, function () { return false; });
}
/** Das Ergebnis ist das Wichtigste: bis zu 3 Versuche mit kurzer Pause. */
async function ergebnisAnPc(paket) {
for (var i = 0; i < 3; i++) {
if (await meldeAnPc(paket, 'ergebnis')) return true;
await new Promise(function (r) { setTimeout(r, 1500); });
}
return false;
}
function zeigeGuthabenBanner(n) {
var bis = einrichtungBis();
guthabenText.textContent = einrichtung()
? '🤝 Kostenlos über ' + einrichtung() + (bis ? ' (bis ' + deDatum(bis) + ')' : '') + ':'
: '💳 Ihr Guthaben:';
guthabenZahl.textContent = n;
guthabenBanner.style.display = 'block';
}
// Schlüsselname bewusst 'v2': setzt evtl. falsch gezählte Stände (aus der
// Testphase) bei bestehenden Besuchern automatisch einmalig zurück.
var SPEICHER_SCHLUESSEL = 'bk_demo_v2';
function benutzt() { return parseInt(speicher.get(SPEICHER_SCHLUESSEL) || '0', 10); }
function setzeBenutzt(n) { speicher.set(SPEICHER_SCHLUESSEL, String(n)); }
var LEER_HINWEIS = 'Die kostenlosen Analysen sind aufgebraucht oder abgelaufen. Ratsuchende: Bitte fragen Sie bei Ihrer Beratungsstelle nach. Beratungsstelle: Ein Anruf genügt, Justin Klein, 0170 4969375. Nach dem Aufladen einfach diese Seite neu laden oder den Code unten noch einmal eingeben.';
var KEIN_ZUGANG_HANDY = 'Auf diesem Handy sind die kostenlosen Analysen verbraucht. Bitte am Computer den Code der Beratungsstelle einlösen und dann einen neuen QR-Code erzeugen.';
function zeigeStand() {
if (handyModus && handyModus.fertig) return;
// Handy-Modus: nie Kaufangebot oder Code-Feld (die Beratung ist dabei)
if (handyModus && !handyModus.w) {
kaufBlock.style.display = 'none';
codeTeil.style.display = 'none';
guthabenBanner.style.display = 'none';
if (benutzt() >= 2) {
eingabe.style.display = 'none';
fehlerBox.style.display = 'block';
fehlerBox.textContent = KEIN_ZUGANG_HANDY;
meldeAnPc({ schritt: 'fehler', code: 'kein_zugang', meldung: 'Am Handy sind die kostenlosen Analysen verbraucht.' });
} else {
eingabe.style.display = 'block';
emailTeil.style.display = benutzt() === 1 ? 'block' : 'none';
}
return;
}
// Einrichtungs-Zugang leer: kein Kaufangebot (Ratsuchende zahlen nichts),
// Code-Feld bleibt, damit nach dem Aufladen ein neues Öffnen reicht
if (einrichtung() && walletToken() && guthaben() <= 0 && !(handyModus && handyModus.w)) {
eingabe.style.display = 'none';
guthabenBanner.style.display = 'none';
kaufBlock.style.display = 'none';
codeTeil.style.display = 'block';
fehlerBox.style.display = 'block';
fehlerBox.textContent = LEER_HINWEIS;
return;
}
if (einrichtung() && hatGuthaben()) {
document.querySelector('#demo .formular-kopf').textContent = 'Antrag auf Klartext · kostenlos über ' + einrichtung();
}
if (handyModus && handyModus.w) {
eingabe.style.display = 'block';
guthabenBanner.style.display = 'none';
emailTeil.style.display = 'none';
fehlerBox.style.display = 'none';
kaufBlock.style.display = 'none';
codeTeil.style.display = 'none';
return;
}
// Bezahlter Modus: Eingabe offen, Guthaben-Stand zeigen, Gratis-Sperre egal
if (hatGuthaben()) {
eingabe.style.display = 'block';
zeigeGuthabenBanner(guthaben());
emailTeil.style.display = 'none';
fehlerBox.style.display = 'none';
kaufBlock.style.display = 'none';
codeTeil.style.display = 'none';
return;
}
guthabenBanner.style.display = 'none';
codeTeil.style.display = 'block';
// Kauf ist IMMER möglich — auch sofort, ohne die Gratis-Analysen zu nutzen.
kaufBlock.style.display = 'block';
// 2. Versuch: E-Mail-Feld einblenden; alles verbraucht: Gratis-Weg sperren
if (benutzt() >= 2) {
eingabe.style.display = 'none';
fehlerBox.style.display = 'block';
fehlerBox.innerHTML = 'Ihre 2 Gratis-Analysen sind verbraucht. Haben Sie einen <strong>Code von einer Beratungsstelle</strong>? Dann geben Sie ihn direkt hier unten ein. Sonst können Sie unten <strong>Guthaben</strong> kaufen.';
werbung.style.display = 'block';
} else if (benutzt() === 1) {
emailTeil.style.display = 'block';
}
}
konsens.addEventListener('change', function () {
if (konsens.checked && fehlerBox.dataset.art === 'konsens') fehlerBox.style.display = 'none';
});
startKnopf.addEventListener('click', function () {
fehlerBox.style.display = 'none';
if (!konsens.checked) {
zeigeFehler('Bitte zuerst oben das Häkchen bei der Einwilligung setzen.');
fehlerBox.dataset.art = 'konsens';
konsens.closest('label').scrollIntoView({ behavior: 'smooth', block: 'center' });
konsens.focus();
return;
}
if (!walletFuerAnalyse() && benutzt() >= 1 && !emailFeld.value.trim()) {
emailTeil.style.display = 'block';
emailFeld.focus();
zeigeFehler('Bitte geben Sie zuerst Ihre E-Mail-Adresse ein — dann wählen Sie den Brief aus.');
return;
}
dateiFeld.click();
});
dateiFeld.addEventListener('change', function () {
var datei = dateiFeld.files && dateiFeld.files[0];
// Feld leeren: sonst meldet der Browser dieselbe Datei beim zweiten Versuch nicht
dateiFeld.value = '';
if (datei) starteAnalyse(datei);
});
/** Freundlicher Hinweis (kein Fehler) — nutzt das Feld über dem Upload. */
function zeigeHinweis(text) {
kaufOk.textContent = 'ℹ️ ' + text;
kaufOk.style.display = 'block';
}
function zeigeFertigBanner(angekommen, ohneUebersetzung, paket) {
verbundenBanner.innerHTML = '';
verbundenBanner.appendChild(document.createTextNode(angekommen
? '✅ Fertig. Die Erklärung steht hier und auf dem Computer. Für einen weiteren Brief tippt die Beratung am Computer noch einmal auf „Lieber mit dem Handy fotografieren".'
: '⚠️ Die Erklärung steht hier, ist aber am Computer nicht angekommen.'));
if (ohneUebersetzung) verbundenBanner.appendChild(document.createTextNode(' Die Übersetzung hat diesmal nicht geklappt, deshalb steht die Erklärung auf Deutsch.'));
if (!angekommen) {
var knopf = document.createElement('button');
knopf.type = 'button'; knopf.className = 'cta cta-zwei';
knopf.textContent = '🔁 Noch einmal an den Computer senden';
knopf.addEventListener('click', async function () {
knopf.disabled = true; knopf.textContent = 'Wird gesendet …';
var ok = await ergebnisAnPc(paket);
zeigeFertigBanner(ok, ohneUebersetzung, paket);
});
verbundenBanner.appendChild(knopf);
}
}
function zeigeFehler(text) {
fehlerBox.dataset.art = '';
fehlerBox.style.display = 'block';
fehlerBox.textContent = text;
}
function turnstileToken() {
var feld = document.querySelector('[name="cf-turnstile-response"]');
return feld ? feld.value : '';
}
/** Foto verkleinern (max 2000px, JPEG 60%) — wie in der App. */
function bildVerkleinern(datei) {
return new Promise(function (resolve, reject) {
var img = new Image();
var url = URL.createObjectURL(datei);
img.onload = function () {
URL.revokeObjectURL(url);
var f = Math.min(1, MAX_KANTE / Math.max(img.width, img.height));
var c = document.createElement('canvas');
c.width = Math.round(img.width * f);
c.height = Math.round(img.height * f);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
resolve(c.toDataURL('image/jpeg', 0.6).split(',')[1]);
};
img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Bild unlesbar')); };
img.src = url;
});
}
function pdfAlsBase64(datei) {
return new Promise(function (resolve, reject) {
var leser = new FileReader();
leser.onload = function () { resolve(String(leser.result).split(',')[1]); };
leser.onerror = function () { reject(new Error('Datei unlesbar')); };
leser.readAsDataURL(datei);
});
}
async function starteAnalyse(datei) {
fehlerBox.style.display = 'none';
if (datei.size > 5 * 1024 * 1024 && datei.type === 'application/pdf') {
zeigeFehler('Die PDF-Datei ist zu groß (max. 5 MB). Tipp: Fotografieren Sie den Brief stattdessen.');
return;
}
var token = turnstileToken();
if (!token) {
zeigeFehler('Die Sicherheitsprüfung lädt noch — bitte warten Sie 2 Sekunden und versuchen Sie es erneut.');
return;
}
eingabe.style.display = 'none';
status.querySelector('.gross').textContent = 'Ihr Brief wird analysiert…';
status.style.display = 'block';
try {
var istPdf = datei.type === 'application/pdf';
var base64 = istPdf ? await pdfAlsBase64(datei) : await bildVerkleinern(datei);
var wallet = walletFuerAnalyse();
var bezahlt = !!wallet;
var body = {
bild: base64,
mimeType: istPdf ? 'application/pdf' : 'image/jpeg',
turnstileToken: token,
};
var email = emailFeld.value.trim();
body.zielsprache = zielsprache();
if (bezahlt) body.walletToken = wallet;
else if (benutzt() >= 1 && email) body.email = email;
if (handyModus && handyModus.d) body.auchDeutsch = true;
meldeAnPc({ schritt: 'analyse' });
var antwort = await fetch(ENDPUNKT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
var daten = await antwort.json();
if (!antwort.ok) {
status.style.display = 'none';
eingabe.style.display = 'block';
if (window.turnstile) window.turnstile.reset();
meldeAnPc({ schritt: 'fehler', code: daten.code || '', meldung: daten.meldung || '' });
// Guthaben vom PC: Fehler nur anzeigen, am Handy nichts umstellen
if (handyModus && handyModus.w) {
zeigeFehler(daten.meldung || 'Etwas ist schiefgelaufen. Bitte erneut versuchen.');
return;
}
// Handy ohne Zugang vom PC: Kontingent-Fehler nicht als Kaufangebot zeigen
if (handyModus && /^(aufgebraucht|kein_guthaben|budget)$/.test(daten.code || '')) {
if (daten.code === 'aufgebraucht') setzeBenutzt(2);
eingabe.style.display = 'none';
fehlerBox.style.display = 'block';
fehlerBox.textContent = KEIN_ZUGANG_HANDY;
meldeAnPc({ schritt: 'fehler', code: 'kein_zugang', meldung: 'Am Handy sind die kostenlosen Analysen verbraucht.' });
return;
}
if (daten.code === 'email_noetig') {
// Nur das E-Mail-Feld zeigen — NICHT als verbraucht markieren.
// (Sonst würde ein serverseitiges "E-Mail nötig", z. B. bei
// geteiltem Anschluss, den Browser dauerhaft blockieren, obwohl
// gar keine Analyse gelungen ist.)
emailTeil.style.display = 'block';
emailFeld.focus();
}
if (daten.code === 'aufgebraucht') {
setzeBenutzt(2);
zeigeStand();
return;
}
if (daten.code === 'kontingent_leer' || daten.code === 'kontingent_abgelaufen') {
vergissHandySitzung();
setzeGuthaben(0);
zeigeStand();
return;
}
if (daten.code === 'kein_guthaben') {
setzeGuthaben(0);
guthabenBanner.style.display = 'none';
kaufBlock.style.display = 'block';
kaufBlock.scrollIntoView({ behavior: 'smooth' });
return;
}
zeigeFehler(daten.meldung || 'Etwas ist schiefgelaufen. Bitte erneut versuchen.');
return;
}
// Token erneuern — sonst würde die nächste Analyse ein "Seite neu laden" verlangen
if (window.turnstile) window.turnstile.reset();
// Die Einwilligung gilt für genau diesen Brief: für den nächsten neu fragen
konsens.checked = false;
// Handy fotografiert für den PC: Ergebnis verschlüsselt hinüberschicken
var paketFuerPc = {
analyse: daten.analyse, analyse_de: daten.analyse_de, sprache: daten.sprache,
guthaben: daten.guthaben, einrichtung: daten.einrichtung,
};
var amPcAngekommen = handyModus ? await ergebnisAnPc(paketFuerPc) : false;
var analyse = bereiteVersionen(daten, false);
if (daten.uebersetzung_fehlgeschlagen) {
zeigeHinweis('Die Übersetzung hat diesmal nicht geklappt. Die Erklärung steht hier auf Deutsch.');
}
// Handy hat für den PC fotografiert: Verbindung ist damit erledigt.
// (Der PC wartet nicht mehr — ein zweiter Brief käme dort nie an.)
if (handyModus) {
if (!handyModus.w && typeof daten.versuch === 'number') setzeBenutzt(Math.max(benutzt(), daten.versuch));
handyModus.fertig = true;
zeigeAnalyse(analyse);
eingabe.style.display = 'none';
kaufBlock.style.display = 'none';
codeTeil.style.display = 'none';
werbung.style.display = 'none';
vergissHandySitzung();
zeigeFertigBanner(amPcAngekommen, daten.uebersetzung_fehlgeschlagen, paketFuerPc);
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
return;
}
if (daten.versuch === 'bezahlt' || typeof daten.guthaben === 'number') {
setzeGuthaben(daten.guthaben);
zeigeErgebnis(analyse, null, daten.guthaben);
return;
}
setzeBenutzt(Math.max(benutzt(), daten.versuch));
if (email) document.getElementById('wlEmail').value = email;
zeigeErgebnis(analyse, daten.versuch);
} catch (e) {
status.style.display = 'none';
eingabe.style.display = 'block';
if (window.turnstile) window.turnstile.reset();
meldeAnPc({ schritt: 'fehler', code: 'verbindung', meldung: 'Verbindung fehlgeschlagen.' });
zeigeFehler('Verbindung fehlgeschlagen. Bitte prüfen Sie Ihr Internet und versuchen Sie es erneut.');
}
}
/** Ampel wie in der App: rot ≤ 7 Tage, gelb ≤ 21, sonst grün. */
/** Ganze Kalendertage von heute bis zum ISO-Datum (wie tageBis() in der App).
* Früher wurde bis 12 Uhr mittags gerechnet — dann zeigte ein Termin in
* 7 Tagen vormittags „8 Tage" und Gelb statt Rot. */
function kalenderTage(iso) {
var t = String(iso).split('-');
var heute = new Date();
var a = Date.UTC(heute.getFullYear(), heute.getMonth(), heute.getDate());
var b = Date.UTC(+t[0], +t[1] - 1, +t[2]);
return Math.round((b - a) / 86400000);
}
function ampel(analyse) {
var daten = [];
if (analyse.frist && isoGueltig(analyse.frist.datum)) daten.push({ iso: analyse.frist.datum, art: 'Frist' });
if (analyse.termin && isoGueltig(analyse.termin.datum)) daten.push({ iso: analyse.termin.datum, art: 'Termin' });
if (!daten.length) return null;
daten.sort(function (x, y) { return x.iso < y.iso ? -1 : x.iso > y.iso ? 1 : 0; });
// Das nächste kommende Datum zählt; nur wenn alles vorbei ist, das zuletzt vergangene
var kommend = daten.filter(function (d) { return kalenderTage(d.iso) >= 0; });
var erstes = kommend.length ? kommend[0] : daten[daten.length - 1];
var tage = kalenderTage(erstes.iso);
if (isNaN(tage)) return null;
var farbe = tage <= 7 ? 'var(--rot)' : tage <= 21 ? 'var(--gelb)' : 'var(--gruen)';
var punkt = tage <= 7 ? '🔴' : tage <= 21 ? '🟡' : '🟢';
var datum = deDatum(erstes.iso);
var wann = erstes.art === 'Termin' ? 'Termin am ' + datum : 'Frist bis ' + datum;
var text = tage < 0 ? (erstes.art === 'Termin' ? 'Der Termin war am ' + datum + '!' : 'Die Frist ist womöglich schon vorbei (' + datum + ')!') :
tage === 0 ? 'HEUTE: ' + wann + '!' :
tage === 1 ? 'Morgen: ' + wann :
'Noch ' + tage + ' Tage · ' + wann;
return { farbe: farbe, text: punkt + ' ' + text, istFrist: erstes.art === 'Frist' };
}
// Eine Analyse kann in zwei Sprachen vorliegen (PC Deutsch, Handy Muttersprache).
var versionen = [];
var aktiveVersion = 0;
var letzteDaten = null; // { analyse, analyse_de, sprache } — für „Mitnehmen"
function bereiteVersionen(d, deutschZuerst) {
letzteDaten = { analyse: d.analyse, analyse_de: d.analyse_de, sprache: d.sprache || 'de' };
versionen = [{ code: letzteDaten.sprache, analyse: d.analyse }];
// Zweite Fassung nur, wenn sie wirklich eine andere Sprache ist
if (d.analyse_de && letzteDaten.sprache !== 'de') {
versionen[deutschZuerst ? 'unshift' : 'push']({ code: 'de', analyse: d.analyse_de });
}
aktiveVersion = 0;
return versionen[0].analyse;
}
function zeichneUmschalter() {
var box = document.getElementById('sprachUmschalter');
box.innerHTML = '';
box.style.display = versionen.length > 1 ? 'flex' : 'none';
versionen.forEach(function (v, i) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = SPRACH_NAMEN[v.code] || v.code;
b.setAttribute('aria-pressed', i === aktiveVersion ? 'true' : 'false');
b.addEventListener('click', function () { aktiveVersion = i; zeigeAnalyse(v.analyse); });
box.appendChild(b);
});
}
/** Nur die Anzeige einer Analyse — ohne Guthaben/Gratis-Ablauf. */
function zeigeAnalyse(analyse) {
status.style.display = 'none';
// Optionale Karten zuruecksetzen. Sonst bleibt beim zweiten Brief z. B.
// die Checkliste des ERSTEN Briefs stehen — zwei Briefe vermischt.
['ergAmpel', 'ergKalenderKarte', 'ergChecklisteKarte', 'ergAnrufKarte', 'ergBegriffeKarte']
.forEach(function (id) { document.getElementById(id).style.display = 'none'; });
document.getElementById('ergBrieftyp').textContent = analyse.brieftyp || 'Ihr Brief';
document.getElementById('ergKern').textContent = analyse.kernaussage || '';
document.getElementById('ergErklaerung').textContent = analyse.erklaerung_einfach || '';
var a = ampel(analyse);
var ampelEl = document.getElementById('ergAmpel');
if (a) {
ampelEl.style.display = 'block';
ampelEl.style.color = a.farbe;
ampelEl.textContent = a.text;
// Die Aufgabe gehört zur Frist — nur anhängen, wenn die Frist das früheste Datum ist.
// KI-Text in eigenes <bdi>, damit Arabisch/Farsi richtig herum läuft.
if (a.istFrist && analyse.frist && analyse.frist.aktion) {
ampelEl.appendChild(document.createTextNode(' · '));
var bdi = document.createElement('bdi'); bdi.textContent = analyse.frist.aktion;
ampelEl.appendChild(bdi);
}
}
if (analyse.checkliste && analyse.checkliste.length) {
document.getElementById('ergChecklisteKarte').style.display = 'block';
document.getElementById('ergCheckliste').innerHTML = analyse.checkliste
.map(function (t) { var d = document.createElement('div'); d.className = 'todo'; d.setAttribute('dir', 'auto'); d.textContent = t; return d.outerHTML; })
.join('');
}
if (analyse.fachbegriffe && analyse.fachbegriffe.length) {
document.getElementById('ergBegriffeKarte').style.display = 'block';
document.getElementById('ergBegriffe').innerHTML = analyse.fachbegriffe
.map(function (b) {
var det = document.createElement('details');
var sum = document.createElement('summary');
sum.textContent = b.begriff;
var p = document.createElement('p');
p.setAttribute('dir', 'auto');
p.textContent = b.erklaerung;
det.appendChild(sum); det.appendChild(p);
return det.outerHTML;
})
.join('');
}
zeigeKalender(analyse);
zeigeAnruf(analyse);
zeichneUmschalter();
document.getElementById('mitnehmenTeil').style.display = istPc && !handyModus && !leseModus ? 'block' : 'none';
document.getElementById('mitnehmenBox').style.display = 'none';
document.getElementById('ergebnis').style.display = 'block';
}
function zeigeErgebnis(analyse, versuch, guthabenRest) {
zeigeAnalyse(analyse);
// Bezahlter Modus: Guthaben-Stand zeigen, Eingabe für den nächsten Brief öffnen
if (typeof guthabenRest === 'number') {
werbung.style.display = 'none';
kaufOk.style.display = 'none';
if (guthabenRest > 0) {
eingabe.style.display = 'block';
zeigeGuthabenBanner(guthabenRest);
emailTeil.style.display = 'none';
kaufBlock.style.display = 'none';
} else if (einrichtung()) {
zeigeStand(); // Leer-Hinweis mit Telefonnummer — kein Kaufangebot für Ratsuchende
} else {
eingabe.style.display = 'none';
guthabenBanner.style.display = 'none';
kaufBlock.style.display = 'block';
}
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
return;
}
var rest = 2 - benutzt();
document.getElementById('restZeile').textContent =
rest > 0 ? 'Noch ' + rest + ' Gratis-Analyse übrig' : 'Ihre 2 Gratis-Analysen sind verbraucht';
document.getElementById('wlTitel').textContent =
rest > 0 ? 'Das hat Ihnen geholfen?' : 'Weiter geht es in der App';
document.getElementById('werbung').style.display = 'block';
if (rest > 0) {
// Upload-Bereich für die 2. Analyse wieder anbieten (mit E-Mail-Feld)
eingabe.style.display = 'block';
emailTeil.style.display = 'block';
document.getElementById('nochmalZeile').style.display = 'block';
}
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
}
// ---- Kalender (.ics) --------------------------------------------------
// Die Datei entsteht im Browser und wird nirgends hochgeladen. Die
// Erinnerungen stellt danach der Kalender des Handys selbst.
var letzteAnalyse = null;
function icsText(t) {
return String(t || '').replace(/\\/g, '\\\\').replace(/;/g, '\;')
.replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
}
// RFC 5545: max. 75 BYTE pro Zeile. Nach Bytes falten, nicht nach Zeichen —
// arabische oder tigrinische Texte brauchen 2–4 Byte pro Zeichen.
function icsFalte(zeile) {
var enc = new TextEncoder(), teile = [], akt = '', bytes = 0;
for (var zeichen of zeile) {
var b = enc.encode(zeichen).length;
if (bytes + b > 75) { teile.push(akt); akt = ' ' + zeichen; bytes = 1 + b; }
else { akt += zeichen; bytes += b; }
}
teile.push(akt);
return teile.join('\r\n');
}
function isoGueltig(d) { return typeof d === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(d); }
function deDatum(iso) { return iso.split('-').reverse().join('.'); }
function zweistellig(x) { return ('0' + x).slice(-2); }
// "Wanduhrzeit" ohne Zeitzone: der Kalender nimmt die Ortszeit des Geraets.
// Richtig fuer einen Termin in Deutschland, egal wo der Server steht.
function wanduhr(dt) {
return dt.getUTCFullYear() + zweistellig(dt.getUTCMonth() + 1) + zweistellig(dt.getUTCDate()) +
'T' + zweistellig(dt.getUTCHours()) + zweistellig(dt.getUTCMinutes()) + '00';
}
function nurDatum(dt) { return wanduhr(dt).slice(0, 8); }
function ausIso(iso, h, m) { var t = iso.split('-'); return new Date(Date.UTC(+t[0], +t[1] - 1, +t[2], h || 0, m || 0)); }
function eindeutig() {
return (window.crypto && crypto.randomUUID ? crypto.randomUUID() : Date.now() + '-' + Math.random().toString(16).slice(2)) + '@behoerdenklar.de';
}
function alarm(ausloeser, text) {
return ['BEGIN:VALARM', 'ACTION:DISPLAY', 'TRIGGER:' + ausloeser, 'DESCRIPTION:' + icsText(text), 'END:VALARM'];
}
function baueIcs(a) {
var z = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Webklar GbR//BehoerdenKlar//DE',
'CALSCALE:GREGORIAN', 'METHOD:PUBLISH'];
var titel = a.brieftyp || 'Brief', stempel = wanduhr(new Date()) + 'Z', n = 0;
if (a.frist && isoGueltig(a.frist.datum)) {
var fTag = ausIso(a.frist.datum);
var fDanach = new Date(fTag.getTime() + 86400000);
// Titel und Hinweis ohne deutsche Worte: Datum und Symbol versteht jeder,
// "aktion" kommt bereits in der Sprache des Nutzers von der KI.
z.push('BEGIN:VEVENT', 'UID:' + eindeutig(), 'DTSTAMP:' + stempel,
'DTSTART;VALUE=DATE:' + nurDatum(fTag), 'DTEND;VALUE=DATE:' + nurDatum(fDanach),
'SUMMARY:' + icsText('⚠️ ' + titel),
'DESCRIPTION:' + icsText((a.frist.aktion || '') + '\n\nbehoerdenklar.de'));
// Ganztaegig beginnt um 00:00 — so fallen beide Erinnerungen auf 9 Uhr morgens.
z.push.apply(z, alarm('-P2DT15H', '⏰ ' + titel + ' — ' + deDatum(a.frist.datum)));
z.push.apply(z, alarm('-PT15H', '⏰ ' + titel + ' — ' + deDatum(a.frist.datum)));
z.push('END:VEVENT'); n++;
}
if (a.termin && isoGueltig(a.termin.datum)) {
var zeit = a.termin.uhrzeit && String(a.termin.uhrzeit).match(/(\d{1,2})[:.](\d{2})/);
var h = zeit ? +zeit[1] : -1, m = zeit ? +zeit[2] : -1;
z.push('BEGIN:VEVENT', 'UID:' + eindeutig(), 'DTSTAMP:' + stempel);
if (zeit && h <= 23 && m <= 59) {
var beginn = ausIso(a.termin.datum, h, m);
var ende = new Date(beginn.getTime() + 3600000); // eine Stunde, auch ueber Mitternacht korrekt
z.push('DTSTART:' + wanduhr(beginn), 'DTEND:' + wanduhr(ende));
z.push.apply(z, alarm('-P3D', '📅 ' + titel + ' — ' + deDatum(a.termin.datum) + ', ' + zweistellig(h) + ':' + zweistellig(m)));
z.push.apply(z, alarm('-P1D', '📅 ' + titel + ' — ' + deDatum(a.termin.datum) + ', ' + zweistellig(h) + ':' + zweistellig(m)));
} else {
var tTag = ausIso(a.termin.datum);
z.push('DTSTART;VALUE=DATE:' + nurDatum(tTag), 'DTEND;VALUE=DATE:' + nurDatum(new Date(tTag.getTime() + 86400000)));
z.push.apply(z, alarm('-P2DT15H', '📅 ' + titel + ' — ' + deDatum(a.termin.datum)));
z.push.apply(z, alarm('-PT15H', '📅 ' + titel + ' — ' + deDatum(a.termin.datum)));
}
z.push('SUMMARY:' + icsText('📅 ' + titel));
if (a.termin.ort) z.push('LOCATION:' + icsText(a.termin.ort));
z.push('DESCRIPTION:' + icsText('behoerdenklar.de'), 'END:VEVENT'); n++;
}
z.push('END:VCALENDAR');
return n ? z.map(icsFalte).join('\r\n') + '\r\n' : null;
}
function zeigeKalender(a) {
var karte = document.getElementById('ergKalenderKarte');
var knopf = document.getElementById('kalenderKnopf');
var hinweis = document.getElementById('ergKalenderHinweis');
var label = document.getElementById('ergKalenderLabel');
var frist = a.frist && isoGueltig(a.frist.datum) ? a.frist : null;
var termin = a.termin && isoGueltig(a.termin.datum) ? a.termin : null;
var box = document.getElementById('ergKalenderText');
box.innerHTML = '';
function zeile(text, kiText) {
var p = document.createElement('p'); p.setAttribute('dir', 'auto'); p.textContent = text;
if (kiText) { p.appendChild(document.createTextNode(' — ')); var b = document.createElement('bdi'); b.textContent = kiText; p.appendChild(b); }
box.appendChild(p);
}
// Kein Datum im Brief: trotzdem sichtbar sagen, dass gesucht wurde.
// Sonst weiss niemand, ob nichts da ist oder etwas kaputt ist — und
// "keine Frist" ist fuer einen aengstlichen Leser selbst eine Nachricht.
if (!frist && !termin) {
letzteAnalyse = null;
// NICHT "keine Frist" behaupten: Manche Briefe nennen wiederkehrende
// oder bedingte Fristen ("bis zum 10. Tag nach jedem Vierteljahr"),
// aus denen sich kein einzelnes Datum machen laesst. Ein Haekchen
// wuerde dann "alles erledigt" signalisieren — gefaehrlich falsch.
label.textContent = 'Kalender';
zeile('ℹ️ Kein festes Datum zum Eintragen gefunden.');
zeile('Wichtig: Manche Fristen haben kein festes Datum, zum Beispiel „jedes Vierteljahr" oder nur unter bestimmten Bedingungen. Lesen Sie dazu die Erklärung oben.');
knopf.style.display = 'none';
hinweis.style.display = 'none';
karte.style.border = '';
karte.style.display = 'block';
return;
}
letzteAnalyse = a;
label.textContent = 'Nicht vergessen';
knopf.style.display = '';
hinweis.style.display = '';
karte.style.border = '2px solid var(--amtsblau)';
if (frist) zeile('⚠️ Frist: ' + deDatum(frist.datum), frist.aktion);
if (termin) zeile('📅 Termin: ' + deDatum(termin.datum) + (termin.uhrzeit ? ', ' + termin.uhrzeit + ' Uhr' : ''), termin.ort);
karte.style.display = 'block';
}
document.getElementById('kalenderKnopf').addEventListener('click', function () {
if (!letzteAnalyse) return;
var ics = baueIcs(letzteAnalyse);
if (!ics) return;
var url = URL.createObjectURL(new Blob([ics], { type: 'text/calendar;charset=utf-8' }));
var link = document.createElement('a');
link.href = url; link.download = 'BehoerdenKlar-Termin.ics';
document.body.appendChild(link); link.click(); link.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
});
// ---- Anruf-Hilfe ------------------------------------------------------
function zeigeAnruf(a) {
var karte = document.getElementById('ergAnrufKarte');
var k = a.kontakt || {}, saetze = Array.isArray(a.anruf) ? a.anruf : [];
if (!k.telefon && !saetze.length) return;
var box = document.getElementById('ergAnrufKontakt');
box.innerHTML = '';
if (k.telefon) {
var knopf = document.createElement('a');
knopf.className = 'cta';
knopf.href = 'tel:' + String(k.telefon).replace(/[^\d+]/g, '');
knopf.textContent = '📞 ' + k.telefon;
box.appendChild(knopf);
}
function angabe(label, wert, hervorheben) {
if (!wert) return;
var p = document.createElement('p'); p.className = 'kontakt-zeile';
var b = document.createElement('strong'); b.textContent = label + ': ';
var w = document.createElement('span'); w.textContent = wert;
if (hervorheben) w.className = 'az-wert';
p.appendChild(b); p.appendChild(w); box.appendChild(p);
}
angabe('Ansprechpartner', k.ansprechpartner);
angabe('Erreichbar', k.sprechzeiten);
angabe('Danach werden Sie gefragt', k.aktenzeichen, true);
var liste = document.getElementById('ergAnrufSaetze');
liste.innerHTML = '';
if (saetze.length) {
var kopf = document.createElement('p');
kopf.style.marginTop = '12px';
kopf.innerHTML = '<strong>Das können Sie am Telefon sagen:</strong>';
liste.appendChild(kopf);
var angezeigt = versionen.length ? versionen[aktiveVersion].code : zielsprache();
var mitBedeutung = angezeigt !== 'de';
saetze.forEach(function (s) {
var d = document.createElement('div'); d.className = 'anruf-satz';
var de = document.createElement('p'); de.className = 'anruf-de';
de.textContent = '„' + s.deutsch + '“';
d.appendChild(de);
if (mitBedeutung && s.bedeutung && s.bedeutung !== s.deutsch) {
var be = document.createElement('p'); be.className = 'anruf-bed';
be.setAttribute('dir', 'auto');
be.textContent = s.bedeutung;
d.appendChild(be);
}
liste.appendChild(d);
});
}
karte.style.display = 'block';
}
// ---- Guthaben kaufen ------------------------------------------------
function rendrePakete() {
paketListe.innerHTML = '';
PAKETE.forEach(function (p) {
var b = document.createElement('button');
b.className = 'cta';
b.type = 'button';
b.style.width = '100%';
b.style.display = 'flex';
b.style.justifyContent = 'space-between';
b.style.alignItems = 'center';
var links = document.createElement('span');
links.textContent = (p.best ? '⭐ ' : '') + p.titel;
var rechts = document.createElement('strong');
rechts.textContent = p.preis;
b.appendChild(links);
b.appendChild(rechts);
b.addEventListener('click', function () { starteKauf(p.priceId, b); });
paketListe.appendChild(b);
});
}
function starteKauf(priceId, knopf) {
if (!kaufKonsens.checked) {
kaufFehler.style.display = 'block';
kaufFehler.textContent = 'Bitte bestätigen Sie zuerst das Häkchen, um fortzufahren.';
kaufKonsens.focus();
return;
}
kaufFehler.style.display = 'none';
var alt = knopf.innerHTML;
knopf.disabled = true;
knopf.textContent = 'Weiter zu Stripe …';
fetch(KAUFEN_ENDPUNKT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ priceId: priceId, sprache: zielsprache() }),
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d && d.url) { window.location.href = d.url; return; }
throw new Error('keine url');
})
.catch(function () {
knopf.disabled = false;
knopf.innerHTML = alt;
kaufFehler.style.display = 'block';
kaufFehler.textContent = 'Die Bezahlseite konnte nicht geöffnet werden. Bitte erneut versuchen.';
});
}
// ---- Rückkehr von Stripe / Wallet-Wiederherstellung -----------------
function reinigeUrl() {
if (window.history.replaceState) {
window.history.replaceState({}, document.title, window.location.pathname);
}
}
function holeKaufStatus(sid, n) {
fetch(STATUS_ENDPUNKT + '?session_id=' + encodeURIComponent(sid))
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d && res.d.token) {
setzeWallet(res.d.token);
setzeGuthaben(res.d.guthaben);
setzeEinrichtung(null);
status.style.display = 'none';
kaufOk.style.display = 'block';
zeigeStand();
document.getElementById('demo').scrollIntoView({ behavior: 'smooth' });
return;
}
if (n < 4) { setTimeout(function () { holeKaufStatus(sid, n + 1); }, 1500); return; }
status.style.display = 'none';
zeigeFehler('Ihre Zahlung wird noch bestätigt. Bitte laden Sie die Seite in einer Minute neu.');
})
.catch(function () {
status.style.display = 'none';
zeigeFehler('Die Zahlungsbestätigung konnte nicht geladen werden. Bitte Seite neu laden.');
});
}
// ---- Einrichtungs-Code einlösen ------------------------------------
/** „mbe k7qx", „MBEK7QX", „MBE-K7QX" → „MBE-K7QX" (wie im Worker). */
function normCode(c) {
var roh = String(c || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
return roh.length > 4 ? roh.slice(0, -4) + '-' + roh.slice(-4) : roh;
}
function loeseCodeEin(code) {
code = normCode(code);
if (!code) { codeFeld.focus(); return; }
if (walletToken() && guthaben() > 0 && !einrichtung() &&
!window.confirm('Sie haben noch gekauftes Guthaben. Wenn Sie den Code einlösen, nutzt diese Seite den Code. Ihr gekauftes Guthaben bleibt über den Link in Ihrer Kauf-E-Mail erhalten. Weiter?')) {
return;
}
codeKnopf.disabled = true;
fehlerBox.style.display = 'none';
fetch(BASIS + '/code', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ code: code }),
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
codeKnopf.disabled = false;
if (!res.ok || !res.d.token) {
codeTeil.open = true;
codeFeld.value = code;
zeigeFehler(res.d.meldung || 'Der Code konnte nicht eingelöst werden.');
return;
}
setzeWallet(res.d.token);
setzeGuthaben(res.d.guthaben);
setzeEinrichtung(res.d.einrichtung);
if (res.d.ablauf) speicher.set(ABLAUF_SCHLUESSEL, res.d.ablauf);
speicher.set(CODE_SCHLUESSEL, code);
zeichneHandyQr();
zeigeStand();
if (res.d.guthaben <= 0) return; // zeigeStand zeigt den Leer-Hinweis mit Telefonnummer
kaufOk.textContent = '✅ Code eingelöst. BehördenKlar ist für Sie kostenlos, über ' + res.d.einrichtung + '.';
kaufOk.style.display = 'block';
})
.catch(function () {
codeKnopf.disabled = false;
zeigeFehler('Verbindung fehlgeschlagen. Bitte prüfen Sie Ihr Internet und versuchen Sie es erneut.');
});
}
codeKnopf.addEventListener('click', function () { loeseCodeEin(codeFeld.value); });
codeFeld.addEventListener('keydown', function (e) { if (e.key === 'Enter') loeseCodeEin(codeFeld.value); });
// ---- Handy ↔ PC ------------------------------------------------------
var verbundenBanner = document.getElementById('verbundenBanner');
var handyKnopf = document.getElementById('handyKnopf');
var handyBox = document.getElementById('handyBox');
var handyQr = document.getElementById('handyQr');
var handyZustand = document.getElementById('handyZustand');
var handyDeutsch = document.getElementById('handyDeutsch');
var handyDeutschZeile = document.getElementById('handyDeutschZeile');
var UEBERGABE_MS = BKUebergabe.LEBENSDAUER_MIN * 60 * 1000;
function zeigeQr(el, adresse) {
el.innerHTML = BKUebergabe.qrSvg(adresse);
el.setAttribute('data-link', adresse); // zum Testen ohne Kamera
}
// PC: QR-Code zeigen und warten, bis das Handy fotografiert hat
var pcWarten = null; // { ue, bis, timer }
var pruefeLaeuft = false;
function zeichneHandyQr() {
if (!pcWarten) return;
var sprache = zielsprache();
// d=1 auch bei „Deutsch" am PC: Stellt die Person am Handy ihre eigene
// Sprache ein, bekommt die Beraterin trotzdem die deutsche Fassung.
zeigeQr(handyQr, BKUebergabe.adresse({
// „Deutsch" am PC heißt nur: nichts vorgeben — dann erkennt das Handy seine Sprache selbst
a: 'k', s: pcWarten.ue.id, k: pcWarten.ue.k, l: sprache !== 'de' ? sprache : '',
d: handyDeutsch.checked ? '1' : '',
w: hatGuthaben() ? walletToken() : '',
e: hatGuthaben() ? einrichtung() : '',
}));
}
window.addEventListener('beforeunload', function (ev) {
if (pcWarten) { ev.preventDefault(); ev.returnValue = ''; }
});
function stoppeWarten() {
if (pcWarten && pcWarten.timer) clearInterval(pcWarten.timer);
pcWarten = null;
}
function schliesseHandyBox() {
handyBox.style.display = 'none';
handyKnopf.style.display = istPc ? 'block' : 'none';
}
handyKnopf.addEventListener('click', async function () {
stoppeWarten();
fehlerBox.style.display = 'none';
try {
pcWarten = { ue: await BKUebergabe.neu(), bis: Date.now() + UEBERGABE_MS };
} catch (e) {
zeigeFehler('Dieser Browser kann die verschlüsselte Übergabe nicht. Bitte einen aktuellen Browser verwenden.');
return;
}
zeichneHandyQr();
handyZustand.textContent = '⏳ Warte auf das Handy …';
handyKnopf.style.display = 'none';
handyBox.style.display = 'block';
handyBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
pcWarten.timer = setInterval(pruefeHandy, 1500);
});
handyDeutsch.addEventListener('change', zeichneHandyQr);
document.getElementById('handyAbbrechen').addEventListener('click', function () {
if (pcWarten) BKUebergabe.loeschen(pcWarten.ue);
stoppeWarten();
schliesseHandyBox();
});
async function pruefeHandy() {
if (!pcWarten || pruefeLaeuft) return;
if (Date.now() > pcWarten.bis) {
stoppeWarten();
schliesseHandyBox();
zeigeFehler('Der QR-Code ist abgelaufen. Tippen Sie noch einmal auf „Lieber mit dem Handy fotografieren".');
return;
}
pruefeLaeuft = true;
try {
var ue = pcWarten.ue;
var f = await BKUebergabe.holen(ue);
if (!pcWarten || pcWarten.ue !== ue) return; // inzwischen abgebrochen
if ((pcWarten.fehlversuche || 0) >= 5 && !f.status) handyZustand.textContent = '⏳ Warte auf das Handy …';
pcWarten.fehlversuche = 0;
// Jede neue Meldung vom Handy verlängert das Warten (nicht ab QR-Anzeige rechnen)
var schritt = f.status ? f.status.schritt + '|' + (f.status.meldung || '') : '';
if (schritt && schritt !== pcWarten.letzterSchritt) {
pcWarten.letzterSchritt = schritt;
// Verbunden: großzügiger warten (Gespräch vor dem Foto kann dauern)
pcWarten.bis = Date.now() + 3 * UEBERGABE_MS;
if (f.status.schritt === 'analyse') pcWarten.analyseSeit = Date.now();
}
if (f.status && f.status.schritt === 'fehler' && /^kontingent_/.test(f.status.code || '')) {
// Kontingent leer/abgelaufen: Warten ist sinnlos
stoppeWarten();
BKUebergabe.loeschen(ue);
schliesseHandyBox();
setzeGuthaben(0);
zeigeStand();
return;
}
if (f.status && f.status.schritt === 'fehler' && f.status.code === 'kein_zugang') {
stoppeWarten();
BKUebergabe.loeschen(ue);
schliesseHandyBox();
zeigeFehler('Am Handy sind die kostenlosen Analysen verbraucht. Bitte hier unten den Code der Beratungsstelle einlösen und dann noch einmal auf „Lieber mit dem Handy fotografieren" tippen.');
codeTeil.style.display = 'block';
codeTeil.open = true;
return;
}
if (f.ergebnis) {
stoppeWarten();
BKUebergabe.loeschen(ue);
schliesseHandyBox();
zeigeVomHandy(f.ergebnis);
} else if (f.status) {
var langsam = f.status.schritt === 'analyse' && pcWarten.analyseSeit && Date.now() - pcWarten.analyseSeit > 3 * 60 * 1000;
handyZustand.textContent =
langsam ? '⚠️ Das dauert ungewöhnlich lange. Bitte am Handy nachsehen: Steht die Erklärung dort schon, auf „Noch einmal an den Computer senden" tippen. Sonst hier abbrechen und neu verbinden.' :
f.status.schritt === 'analyse' ? '⏳ Das Handy hat fotografiert. Der Brief wird erklärt (ein bis zwei Minuten) …' :
f.status.schritt === 'fehler' ? '⚠️ Am Handy hat es nicht geklappt: ' + (f.status.meldung || '') + ' Bitte dort noch einmal versuchen.' :
'✅ Handy verbunden. Jetzt am Handy den Brief fotografieren.';
}
} catch (e) {
// kurz keine Verbindung — beim nächsten Durchlauf erneut versuchen,
// aber nach mehreren Fehlschlägen sichtbar sagen, woran es liegt
if (pcWarten) {
pcWarten.fehlversuche = (pcWarten.fehlversuche || 0) + 1;
if (pcWarten.fehlversuche >= 5) handyZustand.textContent = '⚠️ Keine Verbindung zum Server. Bitte das Internet dieses Computers prüfen.';
}
} finally {
pruefeLaeuft = false;
}
}
function zeigeVomHandy(e) {
// Hat das Handy das Guthaben dieses PCs benutzt, den neuen Stand übernehmen
if (typeof e.guthaben === 'number' && hatGuthaben()) setzeGuthaben(e.guthaben);
zeigeAnalyse(bereiteVersionen(e, true));
werbung.style.display = 'none';
zeigeStand();
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
}
// PC: Ergebnis per QR-Code aufs Handy mitgeben
document.getElementById('mitnehmenKnopf').addEventListener('click', async function () {
var knopf = this;
if (!letzteDaten) return;
knopf.disabled = true;
try {
var ue = await BKUebergabe.neu();
await BKUebergabe.senden(ue, 'ergebnis', letzteDaten);
zeigeQr(document.getElementById('mitnehmenQr'), BKUebergabe.adresse({ a: 'm', s: ue.id, k: ue.k }));
document.getElementById('mitnehmenBox').style.display = 'block';
} catch (e) {
zeigeFehler('Das Ergebnis konnte nicht vorbereitet werden. Bitte prüfen Sie die Internetverbindung.');
}
knopf.disabled = false;
});
// PC: Guthaben bzw. Zugang der Beratungsstelle aufs Handy holen
document.getElementById('guthabenHandyKnopf').addEventListener('click', function () {
var box = document.getElementById('guthabenQrBox');
if (box.style.display === 'block') { box.style.display = 'none'; return; }
zeigeQr(document.getElementById('guthabenQr'), BKUebergabe.adresse({
a: 'g', w: walletToken(), n: guthaben(), e: einrichtung(),
}));
box.style.display = 'block';
});
/** Handy: Seite wurde über einen QR-Code vom PC geöffnet. */
async function pruefeUebergabe(h) {
if (!h || !h.a) return;
if (h.a === 'g') { uebernehmeGuthaben(h); return; }
var ue = await BKUebergabe.oeffne(h.s, h.k).catch(function () { return null; });
if (!ue) {
zeigeFehler('Dieser QR-Code ist ungültig. Bitte am Computer einen neuen erzeugen.');
return;
}
if (h.a === 'k') {
handyModus = { ue: ue, w: h.w || '', d: h.d === '1', e: h.e || '' };
if (h.l && SPRACH_NAMEN[h.l]) {
// nicht dauerhaft speichern: Es ist das Handy der Person, nicht der Beratung
sitzungsSprache = h.l;
document.getElementById('sprachWahl').value = h.l;
}
verbundenBanner.textContent = '🔗 Mit dem Computer verbunden. Setzen Sie das Häkchen und fotografieren Sie Ihren Brief. Die Erklärung erscheint hier und auf dem Bildschirm.';
verbundenBanner.style.display = 'block';
handyKnopf.style.display = 'none';
if (handyModus.e) document.querySelector('#demo .formular-kopf').textContent = 'Antrag auf Klartext · kostenlos über ' + handyModus.e;
// „verbunden" zuerst melden — zeigeStand meldet danach ggf. „kein Zugang",
// und das darf nicht von „verbunden" überschrieben werden
await meldeAnPc({ schritt: 'verbunden' });
zeigeStand();
if (handyModus.w) { kaufBlock.style.display = 'none'; codeTeil.style.display = 'none'; }
verbundenBanner.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (h.a === 'm') {
leseModus = true;
eingabe.style.display = 'none';
kaufBlock.style.display = 'none';
status.querySelector('.gross').textContent = 'Das Ergebnis wird geladen …';
status.style.display = 'block';
var f = null;
try { f = await BKUebergabe.holen(ue); } catch (e) {}
status.style.display = 'none';
if (!f || !f.ergebnis) {
leseModus = false;
zeigeStand();
zeigeFehler('Dieser QR-Code ist abgelaufen oder wurde schon benutzt (er gilt ' + BKUebergabe.LEBENSDAUER_MIN + ' Minuten und nur einmal). Bitte am Computer einen neuen erzeugen.');
return;
}
BKUebergabe.loeschen(ue);
codeTeil.style.display = 'none';
kaufBlock.style.display = 'none';
verbundenBanner.textContent = '📱 Vom Computer übernommen. Tragen Sie die Frist gleich in Ihren Kalender ein, dann vergessen Sie nichts.';
verbundenBanner.style.display = 'block';
zeigeAnalyse(bereiteVersionen(f.ergebnis, false));
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
}
}
function uebernehmeGuthaben(h) {
if (!h.w) return;
if (walletToken() && walletToken() !== h.w && guthaben() > 0 &&
!window.confirm('Auf diesem Handy ist schon Guthaben gespeichert. Durch das vom Computer ersetzen? Ihr bisheriges Guthaben bleibt über den Link in Ihrer Kauf-E-Mail erhalten.')) {
return;
}
setzeWallet(h.w);
setzeGuthaben(parseInt(h.n, 10) || 1);
setzeEinrichtung(h.e || null);
kaufOk.textContent = h.e ? '✅ Übernommen. BehördenKlar ist für Sie kostenlos, über ' + h.e + '.' : '✅ Guthaben übernommen.';
kaufOk.style.display = 'block';
zeigeStand();
}
function vergissHandySitzung() { try { sessionStorage.removeItem('bk_handy'); } catch (e) {} }
function pruefeRueckkehr() {
var p = new URLSearchParams(window.location.search);
var w = p.get('wallet');
if (w) { setzeWallet(w); setzeGuthaben(Math.max(guthaben(), 1)); setzeEinrichtung(null); reinigeUrl(); return; }
var c = p.get('code');
if (c) { reinigeUrl(); loeseCodeEin(c); return; }
var kauf = p.get('kauf');
if (kauf === 'abbruch') {
reinigeUrl();
fehlerBox.style.display = 'block';
fehlerBox.textContent = 'Kauf abgebrochen — es wurde nichts berechnet.';
return;
}
if (kauf === 'ok') {
var sid = p.get('session_id');
reinigeUrl();
if (!sid) return;
status.querySelector('.gross').textContent = 'Ihr Kauf wird bestätigt …';
eingabe.style.display = 'none';
status.style.display = 'block';
holeKaufStatus(sid, 0);
}
}
kaufKonsens.addEventListener('change', function () {
if (kaufKonsens.checked) kaufFehler.style.display = 'none';
});
// Sprachauswahl aufbauen
(function () {
var feld = document.getElementById('sprachWahl');
var aktiv = zielsprache();
Object.keys(SPRACH_NAMEN).forEach(function (c) {
var o = document.createElement('option');
o.value = c; o.textContent = SPRACH_NAMEN[c] + (SPRACH_DEUTSCH[c] ? ' – ' + SPRACH_DEUTSCH[c] : '');
if (c === aktiv) o.selected = true;
feld.appendChild(o);
});
feld.addEventListener('change', function () {
speicher.set(SPRACH_SCHLUESSEL, feld.value);
sitzungsSprache = null;
if (/[?&]sprache=/.test(window.location.search) && window.history.replaceState) {
var u = new URL(window.location.href);
u.searchParams.delete('sprache');
window.history.replaceState(null, document.title, u.pathname + u.search + u.hash);
}
zeichneHandyQr();
});
})();
rendrePakete();
var startHash = BKUebergabe.ausAdresse();
var startSuche = window.location.search; // vor pruefeRueckkehr, das die Adresse aufräumt
// Handy-Verbindung übersteht ein Neuladen (z. B. wenn Android den Tab beim
// Kamera-Wechsel neu lädt). sessionStorage gilt nur für diesen einen Tab.
try {
if (startHash && startHash.a === 'k') sessionStorage.setItem('bk_handy', JSON.stringify(startHash));
else if (!startHash) { var gemerkt = sessionStorage.getItem('bk_handy'); if (gemerkt) startHash = JSON.parse(gemerkt); }
} catch (e) {}
if (startHash && window.history.replaceState) {
// Schlüssel nicht im Browser-Verlauf stehen lassen
window.history.replaceState(null, document.title, window.location.pathname + window.location.search);
}
pruefeRueckkehr();
zeigeStand();
// Einrichtungs-Zugang leer? Einmal still nachfragen — vielleicht wurde
// inzwischen aufgeladen oder verlängert (genau ein Aufruf pro Seitenaufruf)
if (!startHash && einrichtung() && walletToken() && guthaben() <= 0 && speicher.get(CODE_SCHLUESSEL)
&& !/[?&](code|wallet|kauf)=/.test(startSuche)) {
loeseCodeEin(speicher.get(CODE_SCHLUESSEL));
}
handyKnopf.style.display = istPc ? 'block' : 'none';
document.getElementById('guthabenHandyKnopf').style.display = istPc ? '' : 'none';
pruefeUebergabe(startHash);
})();
</script>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "11f017e9a707466b9151921db6125dde"}'></script>
</body>
</html>