feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen
Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Konventionen im Betreuungs-Cockpit
|
||||
|
||||
Verbindlich für jeden, der Code in dieses Projekt schreibt. Wer davon
|
||||
abweicht, erzeugt Reibung an der nächsten Schnittstelle.
|
||||
|
||||
## Sprache
|
||||
|
||||
- **Alles auf Deutsch**: Bezeichner, Kommentare, Commit-Nachrichten,
|
||||
Oberflächentexte, Fehlermeldungen. Ausnahmen sind Framework-Vorgaben
|
||||
(`page.tsx`, `layout.tsx`, `default`, `props`) und etablierte
|
||||
Fachbegriffe ohne gute Übersetzung (`Token`, `Hash`, `Cookie`).
|
||||
- Umlaute in **Oberflächentexten**: ja, korrekt geschrieben.
|
||||
Umlaute in **Bezeichnern und Dateinamen**: nein, umschrieben
|
||||
(`vermoegen`, `naechsterSchritt`, `zurueck`).
|
||||
- Du-Ansprache. Keine Juristensprache ohne Erklärung — steht ein
|
||||
Paragraph im Text, gehört eine Erklärung daneben.
|
||||
|
||||
## Verzeichnisse
|
||||
|
||||
```
|
||||
src/
|
||||
app/
|
||||
(auth)/ Anmeldung, Ersteinrichtung, Zweitfaktor
|
||||
(app)/ Alle Fachseiten, hinter Anmeldung + 2FA
|
||||
api/
|
||||
components/
|
||||
ui/ Basisbausteine (Button, Input, Card, …)
|
||||
<bereich>/ Fachkomponenten je Modul
|
||||
lib/
|
||||
crypto/ Feldverschlüsselung, Hashing, TOTP
|
||||
auth/ Sitzung, Rollen, Rate-Limiting
|
||||
validierung/ Zod-Schemata je Entität
|
||||
<bereich>/ Fachlogik ohne React
|
||||
server/
|
||||
actions/ Server Actions ("use server")
|
||||
prisma/
|
||||
tests/ Vitest (Logik)
|
||||
e2e/ Playwright (kritische Flows)
|
||||
```
|
||||
|
||||
Ein Modul bringt mit: Seite(n) unter `app/(app)/<modul>/`, Fachkomponenten
|
||||
unter `components/<modul>/`, Zod-Schema unter `lib/validierung/<modul>.ts`,
|
||||
Server Actions unter `server/actions/<modul>.ts`, reine Logik unter
|
||||
`lib/<modul>/`.
|
||||
|
||||
## Datenzugriff
|
||||
|
||||
```ts
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
```
|
||||
|
||||
- **Jede** Abfrage auf Fachdaten filtert `...nichtGeloescht` (Papierkorb).
|
||||
- **Jede** Abfrage ist auf `betreuteId` eingegrenzt.
|
||||
- Zugriff wird immer über `erfordereZugang(betreuteId, modul, art)` geprüft
|
||||
— auch in Server Actions, nicht nur in der Seite. Nie nur im UI ausblenden.
|
||||
|
||||
```ts
|
||||
const { betreute, benutzer, darfSchreiben } = await erfordereZugang(id, 'FRISTEN', 'schreiben')
|
||||
```
|
||||
|
||||
## Geldbeträge
|
||||
|
||||
Immer `BigInt` in **Cent**, Feldname endet auf `Cent`. Anzeige über
|
||||
`formatCent()`. Nie `number` für Beträge, nie `Decimal`, nie Fließkomma.
|
||||
Auf dem Weg zu Client Components über `centZuZahl()`.
|
||||
|
||||
## Datum
|
||||
|
||||
- Fristen, Stichtage, Bescheiddaten: `@db.Date`, ohne Uhrzeit.
|
||||
- Zeitstempel (`erstelltAm`, Protokolle): volles `DateTime`.
|
||||
- Anzeige über `formatDatum()` / `formatDatumZeit()` aus `@/lib/utils`.
|
||||
- Datumsrechnung immer über `date-fns`, nie mit `setDate` von Hand.
|
||||
|
||||
## Verschlüsselte Felder
|
||||
|
||||
Feldname endet auf `Verschluesselt`. Nie direkt schreiben:
|
||||
|
||||
```ts
|
||||
import { sealen, oeffnen } from '@/lib/crypto/feld'
|
||||
|
||||
const daten = { ...rest, ...sealen('Vorgang', { beschreibung: eingabe.beschreibung }) }
|
||||
const offen = oeffnen('Vorgang', datensatzAusDb) // offen.beschreibung ist Klartext
|
||||
```
|
||||
|
||||
Neues verschlüsseltes Feld? Erst in `VERSCHLUESSELTE_FELDER` in
|
||||
`src/lib/crypto/feld.ts` eintragen, sonst wirft `sealen()`.
|
||||
Verschlüsselte Felder lassen sich **nicht** filtern oder sortieren.
|
||||
|
||||
## Audit-Log
|
||||
|
||||
Jede schreibende Aktion protokolliert:
|
||||
|
||||
```ts
|
||||
await protokollieren({
|
||||
aktion: 'GEAENDERT',
|
||||
entitaet: 'Frist',
|
||||
entitaetId: frist.id,
|
||||
bezeichnung: frist.titel,
|
||||
betreuteId,
|
||||
userId: benutzer.id,
|
||||
userEmail: benutzer.email,
|
||||
userName: benutzer.name,
|
||||
aenderung: aenderungErmitteln(vorher, nachher),
|
||||
})
|
||||
```
|
||||
|
||||
## Server Actions
|
||||
|
||||
- Immer `'use server'` oben, immer Zod-Validierung, immer Zugriffsprüfung.
|
||||
- Rückgabe für Formulare: ein Zustandsobjekt
|
||||
`{ fehler?: string; felder?: Record<string, string>; werte?: … }`,
|
||||
benutzt mit `useActionState`. Keine geworfenen Fehler ans Formular.
|
||||
- Nach dem Schreiben `revalidatePath()` auf die betroffene Seite.
|
||||
- Löschen heißt: `geloeschtAm` setzen. Endgültiges Löschen nur im
|
||||
Papierkorb und nur nach `erfordereFrischeAnmeldung()`.
|
||||
|
||||
## Oberfläche
|
||||
|
||||
- Basisbausteine aus `@/components/ui/*`. Formularfelder immer in
|
||||
`<Feld id beschriftung erklaerung fehler>` einwickeln — das verdrahtet
|
||||
Label, Erklärung und Fehler barrierefrei.
|
||||
- Jede Seite beginnt mit `<SeitenKopf titel beschreibung pfad aktionen />`.
|
||||
- Leere Listen bekommen `<Leerzustand>` mit einem konkreten nächsten
|
||||
Schritt, nie nur "keine Daten".
|
||||
- Steht ein Paragraph auf der Seite, steht `<Rechtshinweis>` darunter.
|
||||
- Mindestens 44 px Touchziele, Schrift nie unter 16 px in Eingabefeldern.
|
||||
- Farbe ist nie die einzige Information — Ampeln immer mit Text.
|
||||
- Tabellen scrollen im eigenen Rahmen, nie die Seite.
|
||||
- Keine externen Fonts, keine CDNs, keine Tracker. Bilder als Data-URI
|
||||
oder aus dem eigenen Objektspeicher.
|
||||
|
||||
## Statusfarben (Ampel)
|
||||
|
||||
| Lage | Variante |
|
||||
|---|---|
|
||||
| überfällig | `rot` |
|
||||
| diese Woche fällig | `orange` |
|
||||
| dieser Monat fällig | `gelb` |
|
||||
| erledigt / im Plan | `gruen` |
|
||||
|
||||
## Tests
|
||||
|
||||
- Fachlogik (Fristberechnung, Beträge, Bewertungen): Vitest unter `tests/`.
|
||||
- Kritische Abläufe: Playwright unter `e2e/`.
|
||||
- Ein Test beschreibt auf Deutsch, was fachlich gelten soll — nicht, welche
|
||||
Funktion aufgerufen wird.
|
||||
|
||||
## Commits
|
||||
|
||||
- Deutsch, Imperativ, erste Zeile ≤ 72 Zeichen.
|
||||
- Körper erklärt **warum**, nicht was.
|
||||
- Trailer am Ende, ohne Leerzeile dazwischen:
|
||||
|
||||
```
|
||||
Etappe: 3
|
||||
Phase: Papier
|
||||
```
|
||||
|
||||
Diese Trailer wertet `npm run protokoll` aus — daraus entsteht das
|
||||
Bauprotokoll mit den Zeiten je Etappe.
|
||||
Reference in New Issue
Block a user