Files
beh-rde-brief/webseite/demo.html
T
JUSNandClaude Opus 5.5 82235dc529 Handy-Bedienung in 27 Sprachen, Kopf im Einrichtungs-Modus, schmale Handys
- ui-texte.js: 74 Bedientexte je Sprache (uebersetzt + lektoriert), t() in
  demo.html; am PC bleibt alles deutsch, am Handy Sprache der Person,
  RTL fuer ar/fa/ur/ps, verbundene Schriften ohne Sperrsatz
- Servermeldungen am Handy nach Code in der Bediensprache
- Seitenkopf "Fuer Sie kostenlos ueber ..." statt "2 Analysen gratis"
- Turnstile passt auf schmale Handys (Abstaende, compact unter 350 px)
- Knopf fuer Mitarbeitende eindeutig: "Zugang dauerhaft auf ein Handy geben"

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

1838 lines
89 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; }
/* Schriften mit verbundenen Buchstaben (Arabisch, Devanagari …): kein
Buchstabenabstand und keine Schreibmaschine — sonst reißen Wörter auseinander */
html.verbundene-schrift .formular-kopf, html.verbundene-schrift .betreff-label,
html.verbundene-schrift h2, html.verbundene-schrift .karten-label {
font-family: inherit; letter-spacing: 0; text-transform: none;
}
@media (max-width: 420px) {
/* Cloudflare-Prüfung ist fest 300 px breit — sonst ragt sie aus dem Kasten */
.wrap { padding: 0 14px 56px; }
.antrag { padding: 22px 14px; }
}
.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" id="kopfAz">Formular D-01 · Gratis-Demo</span>
</div>
<header>
<p class="betreff-label" data-t="betreff">Betreff</p>
<h1><span data-t="titel1">Probieren Sie es aus —</span><br><span class="markiert" data-t="titel2">mit Ihrem eigenen Brief.</span></h1>
<p class="anrede">
<span data-t="anrede">Kein Konto. Keine App-Installation. Laden Sie ein Foto Ihres
Behördenbriefs hoch — nach ein bis zwei Minuten lesen Sie ihn in einfacher
Sprache.</span> <strong id="anredeAngebot" data-t="anredeGratis">2 Analysen gratis.</strong>
</p>
</header>
<section id="demo">
<h2><span class="nr">§ 1</span> <span data-t="abschnittHochladen">Brief hochladen</span></h2>
<div class="antrag">
<p class="formular-kopf" data-t="formKopfGratis">Brief erklären lassen · 2 kostenlose 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> <span data-t="bannerRest">Analysen — laden Sie den nächsten Brief hoch.</span>
<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" data-t="spracheLabel">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>
<span data-t="konsens">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.</span>
<a href="datenschutz.html" target="_blank" data-t="datenschutzLink">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" data-t="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');
}
// Sehr schmale Handys (< 350 px): kompakte Cloudflare-Prüfung
if (window.innerWidth < 350) document.querySelector('.cf-turnstile').setAttribute('data-size', 'compact');
</script>
<p class="klein" data-t="tippFoto">
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 data-t="tippHandyTitel">📱 Am Handy:</strong> <span data-t="tippHandy">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. Diese Abfrage kommt von
Ihrem Handy, nicht von uns: Diese Seite greift weder auf
Kamera noch auf Mikrofon zu.</span>
</p>
</div>
<div class="status" id="status">
<span class="sanduhr">⏳</span>
<p class="gross">Ihr Brief wird erklärt …</p>
<p class="sub" data-t="statusDauer">Das dauert ein bis zwei Minuten. 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 data-t="codeFrage">🤝 Sie haben einen Code von einer Beratungsstelle?</summary>
<p data-t="codeText">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" data-t="codeKnopf">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> <span data-t="ergebnisTitel">Ihr Brief in Klartext</span></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;" data-t="kalenderKnopf">📅 In meinen Kalender eintragen</button>
<p class="klein" id="ergKalenderHinweis" data-t="kalenderHinweis">
Ihr Handy erinnert Sie 3 Tage und 1 Tag vorher.
Der Eintrag landet nur in Ihrem eigenen Kalender — wir speichern nichts.
</p>
</div>
<div class="brief-karte">
<span class="karten-label" data-t="labelEinfach">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" data-t="labelTun">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" data-t="labelAnrufen">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" data-t="labelWoerter">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" data-t="rechtHinweis">
⚖️ BehördenKlar erklärt Briefe, ersetzt aber keine Rechtsberatung. Bei
wichtigen Entscheidungen hilft eine Beratungsstelle oder eine Anwältin bzw. 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 src="ui-texte.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); }
}
// ── Bediensprache ─────────────────────────────────────────────────
// Am PC (Beratung) bleibt alles deutsch. Am Handy richtet sich die
// Bedienung nach der gewählten Erklärungssprache (Texte: ui-texte.js).
var UI = window.BK_UI_TEXTE || { de: {} };
var RTL = { ar: 1, fa: 1, ur: 1, ps: 1 };
var VERBUNDEN = { ar: 1, fa: 1, ur: 1, ps: 1, hi: 1, ti: 1, zh: 1 };
var uiSprache = 'de';
function t(schluessel, werte) {
var text = (UI[uiSprache] && UI[uiSprache][schluessel]) || (UI.de && UI.de[schluessel]) || schluessel;
if (werte) for (var k in werte) text = text.split('{' + k + '}').join(werte[k]);
return text;
}
function wendeUiSpracheAn() {
var amPc = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
var gewuenscht = amPc ? 'de' : zielsprache();
uiSprache = UI[gewuenscht] ? gewuenscht : 'de';
document.documentElement.lang = uiSprache;
document.documentElement.dir = RTL[uiSprache] ? 'rtl' : 'ltr';
document.documentElement.classList.toggle('verbundene-schrift', !!VERBUNDEN[uiSprache]);
var elemente = document.querySelectorAll('[data-t]');
for (var i = 0; i < elemente.length; i++) {
var el = elemente[i], key = el.getAttribute('data-t');
if (UI.de && UI.de[key]) el.textContent = t(key);
}
// Was gerade dynamisch angezeigt wird, in der neuen Sprache neu setzen
var stelle = (typeof handyModus !== 'undefined' && handyModus && handyModus.e) || (einrichtung() && hatGuthaben() ? einrichtung() : '');
if (stelle) zeigeEinrichtungImKopf(stelle);
if (typeof handyModus !== 'undefined' && handyModus && !handyModus.fertig) verbundenBanner.textContent = t('verbunden');
}
/** Meldung des Servers: am Handy in der Bediensprache, sonst die genaue deutsche. */
function serverMeldung(daten) {
var zuordnung = { turnstile: 'srvTurnstile', ki: 'srvKi', ki_konfig: 'srvDienst', dienst: 'srvDienst', datei: 'srvDatei', inhalt: 'srvInhalt', aufgebraucht: 'srvAufgebraucht' };
if (uiSprache !== 'de' && zuordnung[daten.code]) return t(zuordnung[daten.code]);
return daten.meldung || t('srvKi');
}
// 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();
document.getElementById('guthabenHandyKnopf').textContent = einrichtung()
? '🔑 Zugang dauerhaft auf ein Handy geben (nur für Mitarbeitende)'
: '📱 Auch auf dem Handy nutzen';
guthabenText.textContent = einrichtung()
? t('bannerKostenlos', { name: einrichtung() + (bis ? ' ' + t('bannerBis', { datum: 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)); }
/** Text mit {name}: der Name kommt in ein <bdi>, damit „(Test)." in arabischem Text richtig steht. */
function mitName(el, vorlage, name) {
var teile = vorlage.split('{name}');
el.textContent = teile[0];
var b = document.createElement('bdi'); b.textContent = name; el.appendChild(b);
if (teile[1]) el.appendChild(document.createTextNode(teile[1]));
}
/** Einrichtungs-Zugang: oben nicht mehr „2 Analysen gratis" anpreisen. */
function zeigeEinrichtungImKopf(name) {
mitName(document.querySelector('#demo .formular-kopf'), t('formKopfKostenlos'), name);
mitName(document.getElementById('anredeAngebot'), t('anredeKostenlos'), name);
document.getElementById('kopfAz').textContent = 'Formular D-01 · ' + name;
}
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 = t('keinZugangHandy');
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 = t('leerHinweis');
return;
}
if (einrichtung() && hatGuthaben()) zeigeEinrichtungImKopf(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(t('fehlerKonsens'));
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 ? t('fertig') : t('nichtAngekommen')));
if (ohneUebersetzung) verbundenBanner.appendChild(document.createTextNode(' ' + t('ohneUebersetzung')));
if (!angekommen) {
var knopf = document.createElement('button');
knopf.type = 'button'; knopf.className = 'cta cta-zwei';
knopf.textContent = t('nochmalSenden');
knopf.addEventListener('click', async function () {
knopf.disabled = true; knopf.textContent = t('wirdGesendet');
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(t('fehlerPdfGross'));
return;
}
var token = turnstileToken();
if (!token) {
zeigeFehler(t('fehlerTurnstileLaedt'));
return;
}
eingabe.style.display = 'none';
status.querySelector('.gross').textContent = t('statusLaeuft');
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(serverMeldung(daten));
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 = t('keinZugangHandy');
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(serverMeldung(daten));
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(t('fehlerVerbindung'));
}
}
/** 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' ? t('ampelTerminAm', { datum: datum }) : t('ampelFristBis', { datum: datum });
var text = tage < 0 ? (erstes.art === 'Termin' ? t('ampelTerminWar', { datum: datum }) : t('ampelFristVorbei', { datum: datum })) :
tage === 0 ? t('ampelHeute') + ': ' + wann + '!' :
tage === 1 ? t('ampelMorgen') + ': ' + wann :
t('ampelNoch', { n: 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 = t('labelKalender');
zeile(t('keinDatum1'));
zeile(t('keinDatum2'));
knopf.style.display = 'none';
hinweis.style.display = 'none';
karte.style.border = '';
karte.style.display = 'block';
return;
}
letzteAnalyse = a;
label.textContent = t('labelNichtVergessen');
knopf.style.display = '';
hinweis.style.display = '';
karte.style.border = '2px solid var(--amtsblau)';
if (frist) zeile(t('fristZeile', { datum: deDatum(frist.datum) }), frist.aktion);
if (termin) zeile(t('terminZeile', { datum: deDatum(termin.datum) }) + (termin.uhrzeit ? ', ' + t('uhrzeit', { zeit: termin.uhrzeit }) : ''), 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(t('ansprechpartner'), k.ansprechpartner);
angabe(t('erreichbar'), k.sprechzeiten);
angabe(t('gefragt'), k.aktenzeichen, true);
var liste = document.getElementById('ergAnrufSaetze');
liste.innerHTML = '';
if (saetze.length) {
var kopf = document.createElement('p');
kopf.style.marginTop = '12px';
var stark = document.createElement('strong'); stark.textContent = t('amTelefon'); kopf.appendChild(stark);
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.setAttribute('dir', 'ltr'); // wird auf Deutsch gesagt — auch auf arabischer Seite links-nach-rechts
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 = t('codeEingeloest', { name: 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(t('qrUngueltig'));
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;
}
wendeUiSpracheAn();
verbundenBanner.textContent = t('verbunden');
verbundenBanner.style.display = 'block';
handyKnopf.style.display = 'none';
if (handyModus.e) zeigeEinrichtungImKopf(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 = t('statusLaden');
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(t('qrAbgelaufen', { min: BKUebergabe.LEBENSDAUER_MIN }));
return;
}
BKUebergabe.loeschen(ue);
codeTeil.style.display = 'none';
kaufBlock.style.display = 'none';
verbundenBanner.textContent = t('mitgenommen');
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 ? t('uebernommen', { name: 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;
wendeUiSpracheAn();
zeigeStand();
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();
wendeUiSpracheAn();
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>