From 0e7da42f27b0cd980b13fc5bae9e2e007eb174c3 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 22 Aug 2026 10:51:28 +0200 Subject: [PATCH] docs: Bedienungsanleitung und Roadmap Bis jetzt gab es Code und Betriebsdokumentation, aber nichts, was einem beim Arbeiten hilft. Die Anleitung ist kein Handbuch zum Durchlesen, sondern die zwoelf Handgriffe, die im Alltag vorkommen - vom Behoerdenschreiben aufnehmen bis zum Vertrag kuendigen. Dazu der Jahresrhythmus, die Berichte, was am Handy anders ist, und ein Abschnitt "wenn etwas schiefgeht" mit den Faellen, die wirklich passieren: Handy verloren, Code wird nicht angenommen, Frist stimmt nicht, Bericht zeigt eine Differenz. Ein eigener Abschnitt sagt, was die Anwendung NICHT tut. Wer sich auf etwas verlaesst, das sie nicht leistet, faellt sonst genau dann darauf herein, wenn es darauf ankommt. Sie ist unter /anleitung auch in der Anwendung lesbar - dieselbe Datei, kein zweiter Text, der auseinanderlaeuft. Wer im Heim steht und nicht weiterweiss, hat sie auf dem Telefon dabei, und der Druckknopf legt sie zu den Betreuungsunterlagen. Dafuer ein schlichter Markdown-Baustein statt einer Bibliothek: die Anwendung zeigt genau zwei solche Dokumente, beide schreibe ich selbst. Rohes HTML wird nicht durchgereicht. Die Datenschutzseite benutzt ihn jetzt ebenfalls, statt eine zweite Umsetzung zu pflegen. Beide Dateien werden ueber outputFileTracingIncludes in den Standalone-Build gezogen - der Pfad entsteht erst zur Laufzeit, die Dateiverfolgung von Next.js kann ihn nicht erkennen. Ohne den Eintrag waeren beide Seiten im Behaelter leer. Die Roadmap sortiert die 44 Schema-Wuensche der Etappen nach einer einzigen Frage zuerst: Kann daraus ein falscher Wert in einem Bericht ans Gericht werden? Was das kann, steht in 1.1. Dazu ein Abschnitt "bewusst nicht geplant" mit Begruendung - damit klar ist, worauf niemand wartet. Etappe: 8 Phase: Haertung --- docs/ANLEITUNG.md | 430 ++++++++++++++++++++++++++++++ docs/ROADMAP.md | 261 ++++++++++++++++++ next.config.mjs | 10 + src/app/(app)/anleitung/page.tsx | 55 ++++ src/app/datenschutz/page.tsx | 95 ++----- src/components/benutzer-menue.tsx | 17 +- src/components/druck-knopf.tsx | 20 ++ src/components/markdown-seite.tsx | 324 ++++++++++++++++++++++ 8 files changed, 1131 insertions(+), 81 deletions(-) create mode 100644 docs/ANLEITUNG.md create mode 100644 docs/ROADMAP.md create mode 100644 src/app/(app)/anleitung/page.tsx create mode 100644 src/components/druck-knopf.tsx create mode 100644 src/components/markdown-seite.tsx diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md new file mode 100644 index 0000000..5ad204e --- /dev/null +++ b/docs/ANLEITUNG.md @@ -0,0 +1,430 @@ +# Bedienungsanleitung + +Für dich als ehrenamtlichen Betreuer. Kein Handbuch zum Durchlesen — +sondern eine Sammlung der Handgriffe, die du tatsächlich brauchst, in der +Reihenfolge, in der sie im Alltag vorkommen. + +> Diese Anwendung ist ein **Arbeits- und Dokumentationswerkzeug**. Sie +> rechnet Fristen aus und sortiert Unterlagen. Sie erteilt **keine +> Rechtsberatung** und kennt deinen Einzelfall nicht. Wo ein Paragraph +> steht, steht daneben der Hinweis, dass er die Rückfrage bei Gericht, +> Bezirk oder Anwalt nicht ersetzt. Nimm den ernst. + +--- + +## Der Grundgedanke in drei Sätzen + +Du trägst einmal ein, was passiert ist — ein Schreiben, ein Besuch, eine +Überweisung. Die Anwendung merkt sich, wann daraus etwas folgt, und +erinnert dich rechtzeitig. Am Ende des Betreuungsjahres erzeugt sie aus +demselben Bestand die Berichte, die Gericht und Bezirk von dir wollen. + +Das heißt umgekehrt: **Was du nicht einträgst, steht später nicht im +Bericht.** Der Aufwand liegt vorn, der Gewinn hinten. + +--- + +## Der erste Tag + +Beim allerersten Aufruf führt dich ein Assistent durch drei Schritte. + +**Schritt 1 — Dein Zugang.** Name, E-Mail, Passwort. Nimm vier zufällige +Wörter statt eines kurzen Kunstworts: *Kastanie-Waschbär-Turm-Grün* ist +leichter zu merken und deutlich schwerer zu knacken. Noch besser ist ein +Passwortmanager. + +**Schritt 2 — Der zweite Faktor.** Installiere eine Authenticator-App — +**Aegis** (Android) oder **2FAS** (iOS) sind quelloffen und brauchen kein +Konto. Scanne den Code, tippe die sechs Ziffern ein. + +Danach bekommst du **acht Wiederherstellungscodes**. Die sind der einzige +Weg zurück, wenn dein Handy verloren geht. Druck sie aus und leg sie zu +deinen Betreuungsunterlagen — **nicht** auf dasselbe Gerät wie die App. +Sie werden dir nie wieder angezeigt. + +**Schritt 3 — Die betreute Person.** Nur das Nötigste. Ein Feld ist dabei +wichtiger als alle anderen: + +> **Das Bestellungsdatum.** Es steht in deiner Bestellungsurkunde. Daraus +> berechnet die Anwendung dein Betreuungsjahr — und damit, wann +> Anfangsbericht, Vermögensverzeichnis, Jahresbericht und die +> Aufwandspauschale fällig sind. Ist es falsch, ist alles falsch. + +Alles Weitere trägst du später unter **Stammdaten → Bearbeiten** in Ruhe +nach. Das lohnt sich: Was dort steht, taucht in jedem Bericht und in jedem +Anschreiben wieder auf. + +**Zum Schluss:** Geh auf **Fristen** und klicke einmal auf +*Pflichtfristen ableiten*. Danach stehen deine gesetzlichen Termine drin. + +--- + +## Der Morgen: die Seite „Heute" + +Diese eine Seite beantwortet die Frage, was ansteht. Von oben nach unten: + +| Kachel | Was sie dir sagt | +|---|---| +| **Fristen** | Was überfällig ist (rot), was diese Woche fällig wird (orange), was diesen Monat (gelb) | +| **Wartet auf andere** | Vorgänge, bei denen die Gegenstelle am Zug ist — mit Tagen seit dem letzten Kontakt | +| **Nachforderungen des Trägers** | Was der Bezirk noch von dir will, mit Fortschrittsbalken | +| **Ungeprüfter Posteingang** | Neu hochgeladene Schreiben, die noch niemand zugeordnet hat | +| **Unverbuchte Belege** | Rechnungen ohne Buchung und Buchungen ohne Beleg | +| **Wo stehen wir?** | Vermögen zum letzten Stichtag, Heimkosten gegen Deckung, Tage bis zum Jahresbericht | + +Jede Kachel endet in einer Handlung. Wenn irgendwo nichts steht, ist dort +auch nichts zu tun. + +**Ist eine Kachel leer, obwohl du Daten erwartest?** Dann fehlt meist ein +Eintrag: keine Fristen ohne Bestellungsdatum, keine Deckungslücke ohne +erfassten Heimkostenmonat. + +--- + +## Die zwölf häufigsten Handgriffe + +### 1. Ein Behördenschreiben aufnehmen + +**Dokumente → Aufnehmen**, oder unterwegs über den runden Knopf unten +rechts → *Beleg fotografieren*. + +1. Foto machen oder PDF wählen — mehrere auf einmal sind ausdrücklich + erwünscht. +2. Warten, bis die Datei angenommen ist. Geprüft wird der **Inhalt**, nicht + die Endung: eine Datei, die sich nur als PDF ausgibt, wird abgelehnt. +3. Titel eintragen (oder leer lassen — die Erkennung schlägt einen vor). +4. **Eingangsdatum** setzen. Das ist wichtiger, als es aussieht: Bei + Bescheiden läuft die Widerspruchsfrist ab dem Zugang, nicht ab dem + Datum auf dem Brief. +5. Vorgang auswählen. +6. *In die Akte übernehmen*. + +Danach liest der Server den Text (Texterkennung auf Deutsch) und schlägt +dir **Absender, Datum, Aktenzeichen, Dokumentart und eine im Text genannte +Frist** vor. Die Textstelle steht daneben — prüf sie, bevor du übernimmst. +Nichts davon wird automatisch angelegt. + +**Trag ein, wo das Papier liegt.** Es gibt ein Feld für Ordner und +Register. Wenn der Rechtspfleger das Original sehen will, findest du es +dann in zehn Sekunden. + +### 2. Aus einem Schreiben eine Frist machen + +Öffne das Dokument. Steht unter den Vorschlägen eine erkannte Frist, +genügt ein Klick auf *Als Frist übernehmen*. Sonst: **Fristen → Neu**, +Datum eintragen, Vorgang zuordnen. + +Setz bei behördlichen Fristen die Vorwarnung großzügig. Der Standard ist +60/30/7/1 Tage vorher. + +### 3. Einen Besuch festhalten — in zwanzig Sekunden + +Runder Knopf unten rechts → *Notiz / Kontakt festhalten*. + +Datum ist auf heute vorbelegt, die Art wählst du aus einer Knopfreihe, die +Dauer per Schnellwahl. Ein Freitextfeld, fertig. + +Zwei Felder lohnen die zusätzlichen Sekunden: + +- **„Persönlicher Kontakt zur betreuten Person"** — dieses Häkchen zählt + direkt in den Jahresbericht. Es ist bewusst **nicht** vorbelegt: ein + versehentlich gesetztes Häkchen wäre eine falsche Angabe gegenüber dem + Gericht. +- **Geäußerte Wünsche** — seit der Reform 2023 ist der Wille der betreuten + Person die Leitschnur deines Handelns (§ 1821 BGB). Was du hier + festhältst, landet im Jahresbericht und trägt deine Entscheidungen. + +Das ist der Handgriff, der sich am meisten auszahlt. Ein Jahresbericht aus +vierzig kurzen Notizen schreibt sich fast von selbst; einer aus der +Erinnerung ist eine Qual. + +### 4. Eine Buchung erfassen + +**Vermögen & Kasse → Buchungen → Neu**, oder über den runden Knopf. + +Wichtig ist die Trennung: **Betrifft die Buchung die betreute Person oder +ist es deine eigene Auslage?** Läuft eine Buchung auf ein als privat +markiertes Konto und ist trotzdem als „betrifft die Betreute" +gekennzeichnet, warnt die Anwendung. Das ist der Fehler, der einem in der +Rechnungslegung um die Ohren fliegt. + +**Belege.** Eine Buchung ohne Beleg wird in der Liste markiert. Das ist +kein Verbot — manchmal gibt es keinen. Dann trag eine Begründung ein, dann +ist die Zeile sauber und du weißt später noch, warum. + +### 5. Einen Vorgang anlegen + +**Vorgänge → Neuer Vorgang.** Nimm eine Vorlage, wenn eine passt: + +Heimeinzug · Antrag Hilfe zur Pflege · Wohnungsauflösung · Hausverkauf · +Darlehen § 91 SGB XII · Versicherungsverwertung · Pflegegrad-Höherstufung · +Steuererklärung · Widerspruch gegen Bescheid + +Jede bringt eine durchdachte Aufgabenliste in der richtigen Reihenfolge +mit. Bei den genehmigungspflichtigen erzwingt sie die Reihenfolge +**Mitteilung → Genehmigung → Rechtsgeschäft**. + +Ein Feld solltest du immer pflegen: **Nächster Schritt.** Es steht auf dem +Dashboard und beantwortet dir in vier Wochen die Frage, was du dir dabei +gedacht hast. + +### 6. Ein genehmigungspflichtiges Geschäft + +Hausverkauf, Wohnungskündigung, Kreditaufnahme, Verwertung einer +Lebensversicherung — dafür brauchst du das Betreuungsgericht. + +Markiere den Vorgang als genehmigungspflichtig und wähle die +Rechtsgrundlage. Die Anwendung zeigt dann eine feste Stufenfolge: + +**Mitteilung ans Gericht → Antrag gestellt → Anhörung → Beschluss → +Rechtskraft → Vollzug** + +> Die Stufe **Vollzug lässt sich erst setzen, wenn ein Beschlussdokument +> hinterlegt ist.** Das ist Absicht: Nach § 1858 BGB muss die Genehmigung +> **vor** dem Rechtsgeschäft vorliegen. Ein Vertrag, den du vorher +> unterschreibst, ist schwebend unwirksam. + +Sprich vorher mit dem Rechtspfleger. Die Anwendung ersetzt das Gespräch +nicht, sie erinnert dich nur daran. + +### 7. Eine Nachforderung des Bezirks abarbeiten + +**Sozialhilfe → Akte öffnen.** Jede angeforderte Unterlage steht als +eigene Zeile mit Status und Frist. Der Fortschrittsbalken zeigt „x von y +Unterlagen geliefert". + +Beim Anlegen des Antrags kannst du die **Checkliste der Standardunterlagen** +übernehmen — Rentenbescheide, Kontoauszüge, Versicherungsunterlagen, +notarielle Übertragungsverträge, Pflegegradbescheid, Heimvertrag, +Grundbuchauszug. Zu jeder steht dabei, **warum** der Träger sie will. Das +erspart Rückfragen. + +Gibt es eine Unterlage nicht, setz den Status auf *nicht vorhanden* und +schreib eine Begründung dazu. Die geht später mit ins Unterlagenpaket — +besser als kommentarloses Schweigen. + +Wenn alles beisammen ist: **Berichte → Unterlagenpaket**. Das macht aus +den markierten Dokumenten **eine** PDF mit Anschreiben, laufender Nummer +und Inhaltsverzeichnis. + +### 8. Einen Bescheid prüfen + +**Sozialhilfe → Akte → Bescheide → Neu.** + +Trag das **Zugangsdatum** ein, nicht das Datum auf dem Bescheid. Die +Widerspruchsfrist läuft ab Bekanntgabe. + +Die Anwendung rechnet die Frist aus — mit Wochenende, bayerischen +Feiertagen und dem Sonderfall, dass bei fehlender oder falscher +Rechtsbehelfsbelehrung ein Jahr statt eines Monats gilt (§ 66 SGG). Der +Rechenweg wird ausgegeben, damit du ihn nachvollziehen kannst. Aus dem +Ergebnis entsteht automatisch die Frist *Widerspruch möglich bis*. + +**Rechne im Zweifel nach oder frag beim Bezirk.** Eine verstrichene +Widerspruchsfrist bekommst du nicht zurück. + +### 9. Einen Rückkaufswert anfordern + +**Verträge → Vertrag öffnen → Rückkaufswert anfordern.** Es entsteht ein +Anschreiben zum Kopieren, und die Anwendung legt eine **Wiedervorlage in +vier Wochen** an. Versicherer antworten selten von selbst. + +### 10. Einen Vertrag kündigen + +**Verträge → Vertrag öffnen → Kündigen.** + +Der Rechner nennt dir den **spätesten Absendetag**, nicht den Fristablauf — +eine Kündigung wirkt mit dem Zugang, nicht mit dem Abschicken. Drei Tage +Postlauf sind eingerechnet. + +Der Assistent fragt dich nach der Genehmigung. Er beantwortet die Frage +nicht, und das soll er auch nicht: Ob die Verwertung einer +Lebensversicherung genehmigungspflichtig ist, entscheidet der +Rechtspfleger. Der **Versandnachweis** (Einschreiben, Faxprotokoll) ist +Pflichtfeld — ohne ihn geht die Kündigung nicht durch. + +### 11. Einen Wertstand nachtragen + +**Vermögen & Kasse → Vermögen → Posten öffnen → Wert eintragen.** + +Stichtag, Betrag und **Wertquelle** — letztere ist Pflicht. „Eigene +Schätzung" ist eine zulässige Antwort, „keine Angabe" nicht: Im +Vermögensverzeichnis steht die Quelle mit dabei, und das Gericht darf +wissen, woher die Zahl kommt. + +Alte Werte werden **nie überschrieben**. Jede Vermögensübersicht bleibt +später reproduzierbar. + +### 12. Etwas versehentlich Gelöschtes zurückholen + +**Einstellungen → Papierkorb.** Gelöschtes bleibt 30 Tage liegen und lässt +sich mit einem Klick zurückholen. Erst danach ist es wirklich weg. + +Endgültiges Löschen verlangt eine erneute Anmeldung. + +--- + +## Das Betreuungsjahr + +Dein Jahr läuft **ab dem Tag der Bestellung**, nicht ab dem 1. Januar. Wer +am 14. Februar bestellt wurde, hat sein erstes Betreuungsjahr bis zum +13. Februar des Folgejahres. + +| Wann | Was | Grundlage | +|---|---|---| +| 3 Monate nach Bestellung | Vermögensverzeichnis und Anfangsbericht | §§ 1835, 1863 BGB | +| Zum Ende jedes Betreuungsjahres | Jahresbericht und Vermögensübersicht | §§ 1859, 1863 BGB | +| Bis 6 Monate danach | **Aufwandspauschale beantragen** | § 1878 BGB | +| Jährlich bis 31.12. | Bayerisches Landespflegegeld | BayLPflGG | +| Jährlich | Zuzahlungsbefreiung der Krankenkasse | § 62 SGB V | +| Jährlich | Rückkaufswerte aller Versicherungen aktualisieren | — | + +> **Die Aufwandspauschale ist eine Ausschlussfrist.** Läuft sie ab, ist +> der Anspruch ersatzlos weg — keine Nachsicht, keine Wiedereinsetzung. +> Die Anwendung warnt deshalb schon 90 Tage vorher und markiert sie rot. + +--- + +## Die Berichte + +Alle unter **Berichte**, die beiden Finanzberichte unter **Vermögen & +Kasse → Berichte**. Jeder trägt Kopf mit Aktenzeichen, Fußzeile mit +Seitenzahl und ein Unterschriftenfeld. + +**Vermögensverzeichnis (§ 1835 BGB).** Stichtag wählen, Knopf drücken. +Aktiva nach Gruppen, Passiva, laufende Einnahmen und Ausgaben, +Belegverweise. Hat ein Posten zum Stichtag keinen Wert, erscheint er als +**Lücke** — nicht als 0,00 €. Das ist ehrlicher und fällt dir auf. + +**Vermögensübersicht / Rechnungslegung (§§ 1859, 1865 BGB).** +Betreuungsjahr wählen. Anfangsbestand, geordnete Einnahmen und Ausgaben, +Endbestand, Belegliste. + +Zwei Varianten: die knappe Übersicht für **befreite Betreuer** (§ 1859 BGB +— als Angehöriger bist du das in der Regel) und die vollständige +Rechnungslegung. + +> Geht die Rechenprobe nicht auf, wird die **Differenz im Bericht +> ausgewiesen** und jeder fehlende Saldostand benannt. Das ist Absicht. +> Eine still geglättete Rechnungslegung ist gegenüber dem Gericht wertlos — +> und meistens fehlt einfach ein Kontoauszug. + +**Jahresbericht (§ 1863 BGB).** Die Anwendung erzeugt einen **Entwurf** aus +deinen Kontaktprotokollen, Gesundheitsereignissen und Vorgängen, gegliedert +nach dem, was das Gericht sehen will. Du überarbeitest ihn im Browser +(wird automatisch zwischengespeichert) und gibst ihn dann als PDF oder DOCX +aus. + +> Es ist ein Entwurf, kein fertiger Bericht. Lies ihn durch, bevor du ihn +> abgibst. Was dort steht, verantwortest du. + +**Belegmappe.** Alle Dokumente eines Vorgangs oder Zeitraums als eine +chronologisch sortierte, paginierte PDF mit Deckblatt und +Inhaltsverzeichnis. Genau das, was Gericht und Bezirk haben wollen. + +**Vollexport.** Ein ZIP mit allen Originaldateien, allen Daten als +lesbarem JSON, den Berichten und Prüfsummen. Damit kannst du diese +Anwendung jederzeit verlassen. Verlangt eine erneute Anmeldung. + +--- + +## Am Handy + +Die Anwendung ist am Telefon vollwertig bedienbar — sie ist dafür gebaut, +nicht dafür angepasst. + +- Menü über das Symbol links oben, schließt mit Wischen oder Escape. +- Der **runde Knopf unten rechts** ist immer erreichbar: Beleg + fotografieren, Notiz festhalten, Frist anlegen, Buchung erfassen. +- Tabellen scrollen in ihrem eigenen Rahmen, nie die ganze Seite. +- Zoomen ist erlaubt und abgeschaltet wird es nie. + +Der typische Fall: Du sitzt im Heim, fotografierst die Heimrechnung, +ordnest sie dem Vorgang zu, hältst den Besuch fest — drei Minuten, und +abends musst du nichts nacharbeiten. + +--- + +## Wenn etwas schiefgeht + +**Ich habe mein Handy mit der Authenticator-App verloren.** +Melde dich mit einem deiner Wiederherstellungscodes an — statt der sechs +Ziffern gibst du den Code im Format `XXXX-XXXX-XXXX` ein. Jeder +funktioniert einmal. Danach unter **Konto → Zweiter Faktor** neu +einrichten und **neue Codes erzeugen**. + +**Ich habe auch die Wiederherstellungscodes verloren.** +Dann kommt nur noch jemand mit Zugriff auf den Server weiter, indem er den +zweiten Faktor in der Datenbank zurücksetzt. Genau deshalb: Codes +ausdrucken. + +**Der Code wird nicht angenommen.** +Fast immer geht die Uhr des Telefons falsch. TOTP-Codes hängen an der +Uhrzeit. Stell die automatische Zeitsynchronisierung ein. + +**„Zu viele Fehlversuche."** +Nach fünf Fehlversuchen wird für eine Viertelstunde gesperrt. Warte ab — +das gilt auch dann, wenn du selbst es warst. + +**Ich habe mein Passwort geändert und bin überall abgemeldet.** +So gewollt. Ein Passwortwechsel beendet alle Sitzungen, auch auf Geräten, +an denen du gerade nicht sitzt. + +**Eine Frist stimmt nicht.** +Prüf zuerst das Bestellungsdatum unter **Stammdaten**. Änderst du es, +werden die Pflichtfristen neu berechnet. Erledigte Fristen bleiben dabei +unangetastet. + +**Ein Bericht zeigt eine Differenz.** +Meistens fehlt ein Saldostand. Der Bericht sagt dir, welcher. Trag ihn +unter **Vermögen & Kasse → Konten** nach und erzeuge neu. + +**Die Texterkennung findet nichts.** +Sie braucht Tesseract auf dem Server. Fehlt es, funktioniert der Upload +trotzdem — nur ohne Volltextsuche. Titel und Absender kannst du von Hand +eintragen. + +--- + +## Was diese Anwendung nicht tut + +Damit du dich nicht darauf verlässt: + +- **Sie berät dich nicht.** Sie rechnet Fristen und schlägt Einordnungen + vor. Ob dein Fall eine Ausnahme ist, weiß sie nicht. +- **Sie entscheidet nicht, was Schonvermögen ist.** Sie gibt dir + Argumentationshilfen zu § 90 SGB XII zum Übernehmen. Entscheiden tun der + Träger und notfalls das Sozialgericht. +- **Sie schätzt keine Bewilligungschancen.** +- **Sie holt keine Kontoauszüge ab.** Kontobewegungen kommen per + CSV-Import oder von Hand. +- **Sie verschickt nichts.** Anschreiben entstehen als Text zum Kopieren, + Berichte als PDF. Absenden musst du selbst — und den Versandnachweis + hochladen. +- **Sie ersetzt deine Papierablage nicht.** Originale bleiben Originale. + Deshalb gibt es das Feld für den Ablageort. + +--- + +## Zwei Gewohnheiten, die sich lohnen + +**Jeden Kontakt festhalten, sofort.** Auch das kurze Telefonat. Zwanzig +Sekunden jetzt sparen dir im Januar einen halben Tag. + +**Einmal im Jahr die Wiederherstellung üben.** Ein Backup, das nie +zurückgespielt wurde, ist kein Backup. Die Anleitung steht in der README +unter *Sicherung und Wiederherstellung*. Zwanzig Minuten, einmal im Jahr. + +--- + +## Wo du weiterkommst + +- **Hinweise** in der Anwendung — 37 Karten zu den häufigen Fallstricken, + jede mit Rechtsgrundlage und Link zum Gesetzestext. +- **Protokoll** — jede Änderung, lückenlos und unveränderlich. Dein + Nachweis gegenüber Gericht und möglichen Miterben. +- Dein **Rechtspfleger** beim Betreuungsgericht. Er ist zuständig und + antwortet. Bei allem, was genehmigungspflichtig sein könnte: vorher + fragen, nicht nachher. +- Die örtliche **Betreuungsbehörde** und die Betreuungsvereine beraten + ehrenamtliche Betreuer kostenlos. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..0acb80c --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,261 @@ +# Roadmap + +Was als Nächstes drankommt, und warum in dieser Reihenfolge. + +**Maßstab ist der Alltag, nicht die Vollständigkeit.** Ein Punkt steht +weit oben, wenn er entweder etwas Falsches richtigstellt oder jeden Monat +Zeit spart. Er steht weit unten, wenn er nur schön wäre. + +Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb +als wichtiger herausstellt, rutscht nach oben. + +Stand: 22.08.2026 · Aktuelle Fassung: **1.0.0** ([CHANGELOG](../CHANGELOG.md)) + +--- + +## Woher die Punkte kommen + +Beim Bauen sind **44 Schema-Wünsche** aufgelaufen — Stellen, an denen das +Datenmodell etwas nicht abbildet, was die Fachlogik gebraucht hätte. Sie +liegen einzeln und begründet in [`schema-wuensche/`](schema-wuensche/), +nach Modul getrennt. Diese Roadmap sortiert sie nach Dringlichkeit und +bündelt sie zu Releases, die sich in einem Rutsch migrieren lassen. + +Dazu kommen die bekannten Einschränkungen aus dem Changelog und das, was +sich im Betrieb noch zeigen wird. + +--- + +## 1.1 — Was heute noch falsch sein kann + +**Ziel:** Kein Wert in der Anwendung soll unter ungünstigen Umständen +falsch sein, ohne dass man es merkt. Das ist die einzige Art von Fehler, +die man einem Betreuungsgericht nicht erklären kann. + +### Kostenkonto der Immobilie richtigstellen + +`Buchung` hat kein `immobilieId`. Das Kostenkonto wertet deshalb über +Buchungskategorien aus — bei **einem** Grundstück richtig, bei einem +zweiten still falsch. Die Oberfläche sagt das derzeit an der Auswertung +dazu; das ist ein Pflaster, keine Lösung. + +*Aufwand: klein. Migration: ein Feld, ein Index.* + +### Umbuchungen kennen ihr Gegenkonto + +Eine Umbuchung zwischen zwei Konten erscheint heute als zwei unverbundene +Buchungen. In der Rechnungslegung sieht das aus wie eine Ausgabe und eine +Einnahme — und wer nachrechnet, fragt zu Recht nach. + +*Aufwand: klein. Migration: ein selbstbezüglicher Fremdschlüssel.* + +### Dublettenschutz beim Import ohne Konto + +Der Schutz vor Doppelbuchungen greift über `(kontoId, importReferenz)`. +Ist kein Konto zugeordnet, greift er nicht — ein zweimal eingelesener +Auszug erzeugt dann doppelte Buchungen. + +*Aufwand: klein. Migration: partieller eindeutiger Index.* + +### Rückkaufswert am Vertrag + +Der Wert hängt heute am Vermögensposten. In der Vertragsübersicht steht +deshalb kein Betrag — ausgerechnet dort, wo man ihn beim Telefonat mit dem +Versicherer braucht. + +*Aufwand: klein.* + +### Genau eine Zielspalte je Dokumentverknüpfung + +`DokumentVerknuepfung` hat zwölf mögliche Zielspalten; dass genau eine +gesetzt ist, erzwingt bisher nur die Anwendung. Ein CHECK-Constraint macht +daraus eine Zusage, die auch bei direktem Datenbankzugriff hält. + +*Aufwand: klein. Migration: ein Constraint.* + +### Papierkorb für Saldo- und Wertstände + +Beide Modelle haben kein `geloeschtAm`. Ein versehentlich eingetragener +Kontostand lässt sich nur endgültig löschen — was der Zusage widerspricht, +dass Löschen 30 Tage rückgängig zu machen ist. + +*Aufwand: klein.* + +### Aufgaben aus Vorlagen entfernen können + +Passt eine Aufgabe aus einer Vorgangs-Vorlage im Einzelfall nicht, bleibt +sie dauerhaft offen stehen und verfälscht jede Fortschrittsanzeige. + +*Aufwand: klein.* + +--- + +## 1.2 — Weniger tippen + +**Ziel:** Die Handgriffe, die sich jeden Monat wiederholen, kürzer machen. + +### Nachforderungen mit stabilem Schlüssel + +Der Dublettenschutz beim Übernehmen der Standardcheckliste läuft über die +Bezeichnung. Wer eine Zeile umbenennt, bekommt sie beim nächsten Mal +doppelt. + +### Bescheid und Frist verbinden + +Aus einem Bescheid entsteht die Widerspruchsfrist heute über einen +Textschlüssel statt über einen Fremdschlüssel. Ändert sich das +Zugangsdatum, wandert die Frist nicht mit. + +### Statuswechsel eines Vorgangs als eigene Tabelle + +Der Zeitstrahl liest Statuswechsel aus dem entschlüsselten Audit-Log. Das +funktioniert, ist aber langsam und hängt an einem Format, das eigentlich +nur Nachweis sein soll. + +### Wer hat was erfasst + +Fristen, Saldo- und Wertstände tragen kein Feld für den erfassenden +Benutzer. Bei einer Betreuung zu zweit ist das die erste Frage. + +### Ansprechpartner am Vorgang + +Die Sachstandsanfrage kann namentlich anreden, bekommt den Ansprechpartner +mangels Feld aber nicht übergeben. + +### Inventarliste als eigenes Modell + +Wertgegenstände hängen heute an Freitext und Fotos. Für die +Wohnungsauflösung wäre eine Liste mit Bezeichnung, Raum, geschätztem Wert +und Verbleib das richtige Werkzeug. + +### Pflegegrad-Historie + +Der Pflegegrad steht als einzelnes Feld in den Stammdaten. Frühere Grade +mit ihren Zeiträumen gehören in eine Historie — sie werden im +Jahresbericht und bei einer Höherstufung gebraucht. + +--- + +## 1.3 — Belastbarkeit + +**Ziel:** Die Stellen absichern, die bisher nur unter Laborbedingungen +geprüft sind. + +### Texterkennung gegen echtes Tesseract + +Bisher ist nur der Weg „nicht verfügbar, Upload funktioniert trotzdem" +getestet. Ein Test im Container gegen echte Schreiben — auch schlechte +Handyfotos, auch mehrseitige Bescheide — fehlt. + +### CAMT.053 gegen einen echten Bankauszug + +Der Leser ist gegen selbst gebaute Beispiele geprüft. Ein echter Auszug +der Sparkasse ist der eigentliche Test. + +### Suche und Belegmappe gegen Postgres + +Beide sind nur in Einzelteilen geprüft, nicht end-to-end gegen die +Datenbank mit einigen hundert Dokumenten. + +### Wiederherstellung als Test + +Die Anleitung dafür steht in der README. Ein Skript, das sie einmal im +Monat automatisch durchspielt — Sicherung zurückspielen, Diagnosen im +Klartext prüfen, Testdatenbank verwerfen — wäre der Beweis statt der +Absichtserklärung. + +### Barrierefreiheits-Durchgang mit einem Screenreader + +Semantik, Fokusreihenfolge und Kontraste sind beim Bauen beachtet und in +Teilen getestet. Ein Durchgang mit NVDA oder VoiceOver ist etwas anderes +als eine Testzusicherung. + +### `@react-pdf/renderer` ohne Krücke + +`src/lib/pdf/standardschrift.ts` gleicht aus, dass die Unterabhängigkeiten +für Standardschriften kein Schriftobjekt mehr liefern. Ein `overrides`- +Eintrag in der `package.json` oder eine neuere Fassung macht die Datei +überflüssig — sie baut sich dann selbst ab. + +--- + +## 2.0 — Mehr als eine Betreuung + +**Ziel:** Was sich nur mit einem Bruch lösen lässt. Deshalb eine eigene +Hauptversion — und deshalb nicht vorher. + +### Zweite betreute Person im Alltag erprobt + +Das Datenmodell trägt es von Anfang an: Alles hängt an einer `betreuteId`, +der Zugriff wird gegen sie geprüft, die Auswahl steht in der Seitenleiste. +**Gelaufen ist es bisher nur mit einer.** Was fehlt, ist Erfahrung — und +vermutlich ein Dashboard, das über beide hinwegsieht. + +### Heimkostenmodell entwirren + +Der einrichtungseinheitliche Eigenanteil auf der Heimrechnung ist bereits +um die Leistung der Pflegekasse gekürzt. Das Modul rechnet deshalb +ausdrücklich nach der Lesart `EIGENANTEIL_NETTO` und warnt, wenn die +Pflegekassenleistung zusätzlich erfasst wird. + +Sauberer wäre, die beiden Lesarten im Modell zu trennen, statt sie im Kopf +auseinanderzuhalten. Das ändert bestehende Datensätze — also 2.0. + +### Mehrere Fassungen je Bericht + +Heute verdrängt die DOCX-Ausgabe die abgegebene PDF-Fassung. Was einmal +beim Gericht lag, sollte unverändert nachweisbar bleiben. + +### Verschlüsselte Ablage auch bei S3 + +Beim Dateitreiber liegen Dokumente zusätzlich AES-256-GCM-verschlüsselt. +Bei S3 geht das nicht, weil signierte URLs am Server vorbei ausgeliefert +werden. Der Ausweg wäre, Dateien durch die Anwendung zu reichen — das +kostet Durchsatz und ist eine bewusste Abwägung, keine Selbstverständlichkeit. + +--- + +## Später, ohne Datum + +Sinnvoll, aber nicht dringend: + +- **Kalendarische Wiedervorlage per E-Mail als Wochenübersicht** statt + einzelner Erinnerungen. +- **Vorlagen für eigene Anschreiben**, über die mitgelieferten hinaus. +- **Import eines Grundbuchauszugs** als strukturierte Daten statt als PDF. +- **Mehrere Betreuungsgerichte** je betreuter Person (bei Umzug). +- **Zwei-Personen-Prinzip** für besonders sensible Aktionen — technisch + vorbereitet durch die erneute Anmeldung, fachlich noch nicht gebraucht. +- **Statistik über mehrere Betreuungsjahre** für den eigenen Überblick. + +--- + +## Bewusst nicht geplant + +Damit klar ist, worauf niemand warten muss: + +| Was | Warum nicht | +|---|---| +| **Automatische Rechtsberatung** | Die Anwendung kennt den Einzelfall nicht. Sie rechnet und dokumentiert; entscheiden tun Gericht, Träger und im Zweifel das Sozialgericht. | +| **Prognosen zu Bewilligungschancen** | Eine Zahl, die niemand belegen kann, wird trotzdem geglaubt. Das ist schlimmer als keine Zahl. | +| **Direkte Bankanbindung** | Ein dauerhafter Kontozugriff ist ein Risiko, das der Bequemlichkeit nicht wert ist. CSV und CAMT reichen. | +| **Mehrmandantenfähigkeit für fremde Betreuungen** | Sobald mehrere Betreuer mit fremden Fällen auf derselben Installation arbeiten, ändert sich die datenschutzrechtliche Lage grundlegend. Mehrere Betreute **einer** Person bleiben möglich. | +| **Native App für Handy und Tablet** | Die Web-Anwendung ist am Telefon vollwertig bedienbar. Eine zweite Oberfläche wäre eine zweite Oberfläche, die veraltet. | +| **Cloud-Angebot für andere Betreuer** | Dann wäre ich Auftragsverarbeiter für fremde Gesundheitsdaten. Eine andere Aufgabe als die, für die das hier gebaut wurde. | +| **KI-Auswertung der Akte** | Gesundheitsdaten einer nicht entscheidungsfähigen Person verlassen diesen Server nicht. Die Texterkennung läuft lokal, und dabei bleibt es. | + +--- + +## Wie hier priorisiert wird + +Drei Fragen, in dieser Reihenfolge: + +1. **Kann daraus ein falscher Wert in einem Bericht ans Gericht werden?** + Dann sofort — auch wenn es unbequem ist. +2. **Kostet es jeden Monat Zeit?** Dann bald. +3. **Wäre es schön?** Dann irgendwann, und ehrlich gesagt vielleicht nie. + +Fällt ein Punkt in Kategorie 1 und wird trotzdem nicht sofort behoben, +steht der Grund dabei — und die Oberfläche sagt an der betroffenen Stelle, +worauf man achten muss. Ein stillschweigend falscher Wert ist das +Schlimmste, was diese Anwendung produzieren kann. diff --git a/next.config.mjs b/next.config.mjs index bf43217..3182a72 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,6 +6,16 @@ const nextConfig = { poweredByHeader: false, // Argon2 ist eine native Bibliothek und darf nicht gebundelt werden. serverExternalPackages: ['@node-rs/argon2'], + + // Zwei Seiten lesen zur Laufzeit eine Markdown-Datei aus docs/: + // die Datenschutzhinweise und die Bedienungsanleitung. Der Pfad wird + // erst zur Laufzeit gebaut, die Dateiverfolgung von Next.js kann ihn + // also nicht erkennen - ohne diesen Eintrag fehlten beide Dateien im + // Standalone-Build, und im Behaelter waeren die Seiten leer. + outputFileTracingIncludes: { + '/datenschutz': ['./docs/datenschutzhinweise.md'], + '/anleitung': ['./docs/ANLEITUNG.md'], + }, eslint: { ignoreDuringBuilds: false, }, diff --git a/src/app/(app)/anleitung/page.tsx b/src/app/(app)/anleitung/page.tsx new file mode 100644 index 0000000..204894b --- /dev/null +++ b/src/app/(app)/anleitung/page.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from 'next' +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { BookOpen } from 'lucide-react' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { SeitenKopf } from '@/components/seiten-kopf' +import { MarkdownSeite } from '@/components/markdown-seite' +import { DruckKnopf } from '@/components/druck-knopf' +import { erfordereBenutzer } from '@/lib/auth/sitzung' + +export const metadata: Metadata = { title: 'Anleitung' } +export const dynamic = 'force-dynamic' + +/** + * Die Bedienungsanleitung, gelesen aus docs/ANLEITUNG.md. + * + * Dieselbe Datei, die auch im Repository liegt und sich ausdrucken laesst - + * zwei Fassungen desselben Textes waeren zwei Fassungen, die auseinander- + * laufen. Wer im Heim steht und nicht weiterweiss, hat sie damit auf dem + * Telefon dabei. + */ +export default async function AnleitungSeite() { + await erfordereBenutzer() + + const pfad = path.join(process.cwd(), 'docs', 'ANLEITUNG.md') + let inhalt: string + + try { + inhalt = await readFile(pfad, 'utf8') + } catch { + inhalt = '' + } + + return ( +
+ } + /> + + {inhalt === '' ? ( + + + ) : ( + + )} +
+ ) +} diff --git a/src/app/datenschutz/page.tsx b/src/app/datenschutz/page.tsx index e966e29..e127624 100644 --- a/src/app/datenschutz/page.tsx +++ b/src/app/datenschutz/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { readFile } from 'node:fs/promises' import path from 'node:path' +import { MarkdownSeite } from '@/components/markdown-seite' export const metadata: Metadata = { title: 'Datenschutz' } @@ -11,83 +12,7 @@ export const metadata: Metadata = { title: 'Datenschutz' } * Der Text kommt aus docs/datenschutzhinweise.md - dieselbe Datei, die man * auch ausdrucken und der betreuten Person aushaendigen kann. Zwei Quellen * fuer denselben Text waeren zwei Quellen fuer Fehler. - * - * Die Umsetzung ist bewusst schlicht: keine Markdown-Bibliothek, nur die - * wenigen Auszeichnungen, die im Dokument wirklich vorkommen. */ -function alsAbsaetze(markdown: string) { - const zeilen = markdown.split(/\r?\n/) - const bloecke: React.ReactNode[] = [] - let absatz: string[] = [] - - const absatzSchliessen = (schluessel: number) => { - if (absatz.length === 0) return - bloecke.push( -

- {absatz.join(' ')} -

, - ) - absatz = [] - } - - zeilen.forEach((zeile, i) => { - const roh = zeile.trim() - - // Der Hinweisblock fuer den Ausfuellenden gehoert nicht auf die Seite. - if (roh.startsWith('>')) return - - if (roh === '' || roh === '---') { - absatzSchliessen(i) - return - } - if (roh.startsWith('## ')) { - absatzSchliessen(i) - bloecke.push( -

- {roh.slice(3)} -

, - ) - return - } - if (roh.startsWith('# ')) { - absatzSchliessen(i) - bloecke.push( -

- {roh.slice(2)} -

, - ) - return - } - if (roh.startsWith('- ')) { - absatzSchliessen(i) - bloecke.push( -
  • - {roh.slice(2)} -
  • , - ) - return - } - // Tabellenzeilen werden als einfache Aufzaehlung dargestellt. - if (roh.startsWith('|')) { - absatzSchliessen(i) - const spalten = roh.split('|').map((s) => s.trim()).filter(Boolean) - if (spalten.every((s) => /^-+$/.test(s))) return - bloecke.push( -
    - {spalten[0]} - {spalten.slice(1).join(' · ')} -
    , - ) - return - } - - absatz.push(roh) - }) - - absatzSchliessen(zeilen.length) - return bloecke -} - export default async function DatenschutzSeite() { const pfad = path.join(process.cwd(), 'docs', 'datenschutzhinweise.md') let inhalt: string @@ -95,14 +20,24 @@ export default async function DatenschutzSeite() { try { inhalt = await readFile(pfad, 'utf8') } catch { - inhalt = - '# Datenschutzhinweise\n\nDer Text konnte nicht geladen werden. ' + - 'Er liegt in docs/datenschutzhinweise.md.' + inhalt = '' } return (
    -
    {alsAbsaetze(inhalt)}
    +

    Datenschutzhinweise

    +

    nach Art. 13 und 14 DSGVO

    + +
    + {inhalt === '' ? ( +

    + Der Text konnte nicht geladen werden. Er liegt im Projekt unter{' '} + docs/datenschutzhinweise.md. +

    + ) : ( + + )} +

    diff --git a/src/components/benutzer-menue.tsx b/src/components/benutzer-menue.tsx index fe2c3a2..792f66a 100644 --- a/src/components/benutzer-menue.tsx +++ b/src/components/benutzer-menue.tsx @@ -3,7 +3,16 @@ import * as React from 'react' import Link from 'next/link' import { signOut } from 'next-auth/react' -import { LogOut, Monitor, Moon, Settings, ShieldCheck, Sun, User as UserIcon } from 'lucide-react' +import { + BookOpen, + LogOut, + Monitor, + Moon, + Settings, + ShieldCheck, + Sun, + User as UserIcon, +} from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -96,6 +105,12 @@ export function BenutzerMenue({ Einstellungen + + + + Anleitung + + diff --git a/src/components/druck-knopf.tsx b/src/components/druck-knopf.tsx new file mode 100644 index 0000000..d815ba4 --- /dev/null +++ b/src/components/druck-knopf.tsx @@ -0,0 +1,20 @@ +'use client' + +import { Printer } from 'lucide-react' +import { Button } from '@/components/ui/button' + +/** + * Druckt die aktuelle Seite. + * + * Muss eine Client-Komponente sein - window.print() gibt es auf dem + * Server nicht. Der Knopf selbst traegt "no-print", damit er auf dem + * Ausdruck nicht auftaucht. + */ +export function DruckKnopf({ beschriftung = 'Drucken' }: { beschriftung?: string }) { + return ( + + ) +} diff --git a/src/components/markdown-seite.tsx b/src/components/markdown-seite.tsx new file mode 100644 index 0000000..831f493 --- /dev/null +++ b/src/components/markdown-seite.tsx @@ -0,0 +1,324 @@ +import * as React from 'react' + +/** + * Anzeige einer Markdown-Datei aus dem docs-Verzeichnis. + * + * Bewusst ohne Markdown-Bibliothek: Die Anwendung zeigt genau zwei solche + * Dokumente an - die Datenschutzhinweise und die Anleitung. Beide schreibe + * ich selbst, ich weiss also, welche Auszeichnungen darin vorkommen. Eine + * Bibliothek waere hier vor allem zusaetzliche Abhaengigkeit und + * zusaetzliche Angriffsflaeche. + * + * Unterstuetzt: Ueberschriften, Absaetze, Aufzaehlungen (auch nummeriert), + * Tabellen, Zitatbloecke, Codebloecke, Trennlinien sowie **fett**, + * `Code` und [Links](ziel) im Fliesstext. + * + * Nicht unterstuetzt und auch nicht noetig: eingebettetes HTML, Bilder, + * Fussnoten. Rohes HTML wird NICHT durchgereicht - der Text wird immer als + * Text ausgegeben, nie als Markup. + */ + +type Block = + | { art: 'ueberschrift'; ebene: 2 | 3 | 4; text: string } + | { art: 'absatz'; text: string } + | { art: 'liste'; nummeriert: boolean; punkte: string[] } + | { art: 'tabelle'; kopf: string[]; zeilen: string[][] } + | { art: 'zitat'; zeilen: string[] } + | { art: 'code'; zeilen: string[] } + | { art: 'trenner' } + +/** Zerlegt den Fliesstext in fett, Code und Links. */ +function inline(text: string, schluessel: string): React.ReactNode[] { + const teile: React.ReactNode[] = [] + // Reihenfolge zaehlt: Code zuerst, damit Sternchen darin nicht greifen. + const muster = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)]+\))/g + + let letzte = 0 + let treffer: RegExpExecArray | null + let nummer = 0 + + while ((treffer = muster.exec(text)) !== null) { + if (treffer.index > letzte) { + teile.push(text.slice(letzte, treffer.index)) + } + + const stueck = treffer[0] + const id = `${schluessel}-${nummer++}` + + if (stueck.startsWith('`')) { + teile.push( + + {stueck.slice(1, -1)} + , + ) + } else if (stueck.startsWith('**')) { + teile.push( + + {stueck.slice(2, -2)} + , + ) + } else { + const beschriftung = stueck.slice(1, stueck.indexOf(']')) + const ziel = stueck.slice(stueck.indexOf('](') + 2, -1) + const extern = /^https?:\/\//.test(ziel) + teile.push( + + {beschriftung} + , + ) + } + + letzte = treffer.index + stueck.length + } + + if (letzte < text.length) teile.push(text.slice(letzte)) + return teile +} + +function zerlegen(markdown: string, ohneTitel: boolean): Block[] { + const zeilen = markdown.split(/\r?\n/) + const bloecke: Block[] = [] + let absatz: string[] = [] + + const absatzSchliessen = () => { + if (absatz.length === 0) return + bloecke.push({ art: 'absatz', text: absatz.join(' ') }) + absatz = [] + } + + for (let i = 0; i < zeilen.length; i++) { + const roh = zeilen[i] ?? '' + const zeile = roh.trimEnd() + const gekuerzt = zeile.trim() + + // --- Codeblock --------------------------------------------------------- + if (gekuerzt.startsWith('```')) { + absatzSchliessen() + const inhalt: string[] = [] + i += 1 + while (i < zeilen.length && !(zeilen[i] ?? '').trim().startsWith('```')) { + inhalt.push(zeilen[i] ?? '') + i += 1 + } + bloecke.push({ art: 'code', zeilen: inhalt }) + continue + } + + if (gekuerzt === '') { + absatzSchliessen() + continue + } + + if (/^-{3,}$/.test(gekuerzt) || /^\*{3,}$/.test(gekuerzt)) { + absatzSchliessen() + bloecke.push({ art: 'trenner' }) + continue + } + + // --- Ueberschriften ---------------------------------------------------- + const ueberschrift = gekuerzt.match(/^(#{1,4})\s+(.*)$/) + if (ueberschrift) { + absatzSchliessen() + const ebene = ueberschrift[1]!.length + // Die H1 ist der Dokumenttitel - den setzt die Seite selbst. + if (ebene === 1 && ohneTitel) continue + bloecke.push({ + art: 'ueberschrift', + ebene: (Math.min(Math.max(ebene, 2), 4) as 2 | 3 | 4), + text: ueberschrift[2]!, + }) + continue + } + + // --- Zitatblock -------------------------------------------------------- + if (gekuerzt.startsWith('>')) { + absatzSchliessen() + const inhalt: string[] = [] + while (i < zeilen.length && (zeilen[i] ?? '').trim().startsWith('>')) { + inhalt.push((zeilen[i] ?? '').trim().replace(/^>\s?/, '')) + i += 1 + } + i -= 1 + bloecke.push({ art: 'zitat', zeilen: [inhalt.join(' ')] }) + continue + } + + // --- Tabelle ----------------------------------------------------------- + if (gekuerzt.startsWith('|')) { + absatzSchliessen() + const spalten = (z: string) => + z + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((s) => s.trim()) + + const kopf = spalten(gekuerzt) + i += 1 // Trennzeile ueberspringen + const zeilenInhalt: string[][] = [] + while (i + 1 < zeilen.length && (zeilen[i + 1] ?? '').trim().startsWith('|')) { + i += 1 + zeilenInhalt.push(spalten(zeilen[i] ?? '')) + } + bloecke.push({ art: 'tabelle', kopf, zeilen: zeilenInhalt }) + continue + } + + // --- Aufzaehlung ------------------------------------------------------- + const punkt = gekuerzt.match(/^(?:[-*]|\d+\.)\s+(.*)$/) + if (punkt) { + absatzSchliessen() + const nummeriert = /^\d+\./.test(gekuerzt) + const punkte: string[] = [punkt[1]!] + + while (i + 1 < zeilen.length) { + const naechste = (zeilen[i + 1] ?? '').trim() + const weiterer = naechste.match(/^(?:[-*]|\d+\.)\s+(.*)$/) + if (weiterer && /^\d+\./.test(naechste) === nummeriert) { + punkte.push(weiterer[1]!) + i += 1 + continue + } + // Eingerueckte Fortsetzung gehoert zum letzten Punkt. + if (naechste !== '' && /^\s{2,}\S/.test(zeilen[i + 1] ?? '')) { + punkte[punkte.length - 1] += ` ${naechste}` + i += 1 + continue + } + break + } + + bloecke.push({ art: 'liste', nummeriert, punkte }) + continue + } + + absatz.push(gekuerzt) + } + + absatzSchliessen() + return bloecke +} + +export function MarkdownSeite({ + inhalt, + ohneTitel = true, + className, +}: { + inhalt: string + /** Die erste H1 weglassen - die Seite setzt ihre Ueberschrift selbst. */ + ohneTitel?: boolean + className?: string +}) { + const bloecke = zerlegen(inhalt, ohneTitel) + + return ( +

    + {bloecke.map((block, i) => { + switch (block.art) { + case 'ueberschrift': { + const Tag = `h${block.ebene}` as 'h2' | 'h3' | 'h4' + const groesse = + block.ebene === 2 + ? 'mt-10 text-xl font-semibold' + : block.ebene === 3 + ? 'mt-7 text-lg font-semibold' + : 'mt-5 text-base font-semibold' + return ( + + {block.text} + + ) + } + + case 'absatz': + return ( +

    + {inline(block.text, String(i))} +

    + ) + + case 'liste': { + const Tag = block.nummeriert ? 'ol' : 'ul' + return ( + + {block.punkte.map((punkt, j) => ( +
  • + {inline(punkt, `${i}-${j}`)} +
  • + ))} +
    + ) + } + + case 'tabelle': + return ( + // Tabellen scrollen im eigenen Rahmen - am Handy darf nie die + // ganze Seite seitlich wandern. +
    + + + + {block.kopf.map((spalte, j) => ( + + ))} + + + + {block.zeilen.map((zeile, j) => ( + + {zeile.map((zelle, k) => ( + + ))} + + ))} + +
    + {inline(spalte, `${i}-k-${j}`)} +
    + {inline(zelle, `${i}-${j}-${k}`)} +
    +
    + ) + + case 'zitat': + return ( +
    + {block.zeilen.map((zeile, j) => ( +

    + {inline(zeile, `${i}-${j}`)} +

    + ))} +
    + ) + + case 'code': + return ( +
    +                {block.zeilen.join('\n')}
    +              
    + ) + + case 'trenner': + return
    + } + })} +
    + ) +}