Absenderanschrift in jedem Schreiben
Jeder erzeugte Brief ging bisher ohne Ruecksendeadresse hinaus: Im Absenderblock stand nur der Name, in der Datumszeile fehlte der Ort. Bei einer Versicherung oder Behoerde wirkt das unserioes und macht es der Gegenstelle unnoetig schwer zu antworten. Neu unter "Mein Konto" ein Feld fuer Strasse, PLZ und Ort. Ab da stehen sie im Absenderblock jedes Schreibens, und die Datumszeile lautet "Bayreuth, den 23.08.2026". Die Anschrift haengt am Benutzer, nicht an der betreuten Person: Sie gehoert zu mir und gilt fuer alle Akten. Nicht die Logik fehlte, sondern die Datenquelle - absenderBlock() und datumsZeile() konnten die Felder schon, briefkopfBauen() hat sie mit einem Kommentar auf null gesetzt, weil es sie im Datenmodell nicht gab. Aufbau: - Drei Felder am Benutzer, Migration ueber den Wrapper aus #24. Das war sein erster echter Einsatz: erzeugte Datei ohne Handarbeit brauchbar, DROP-INDEX-Zeilen bereits heraus. - Ein Absender-Typ statt eines blossen Namens, durchgereicht bis in den Serienlauf - der hat bisher {name} gebaut und haette die Anschrift sonst still verloren. - Zwei Platzhalter fuer eigene Vorlagen: betreuer_anschrift, betreuer_ort. Was fehlt, bleibt weg: Jede der drei Angaben ist einzeln freiwillig. Nur ein Ort ergibt eine Datumszeile mit Ort und einen Absenderblock, der weiterhin nur aus dem Namen besteht. Eine halb ausgefuellte Adresse sieht nach Fehler aus, ein blosser Name nach Absicht. Geprueft: sieben Einheitstests fuer den Absenderblock inklusive jeder Teilbefuellung und der Gegenprobe, dass nie die Heimadresse der betreuten Person im Absender landet; vier End-to-End-Tests vom leeren Zustand ueber das Speichern bis zum fertigen Brief. Refs #21
This commit is contained in:
+44
-1
@@ -26,6 +26,48 @@ Wiederherstellung*.
|
|||||||
|
|
||||||
_Noch nichts._
|
_Noch nichts._
|
||||||
|
|
||||||
|
## [1.10.0] – 2026-08-23
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Deine Anschrift steht jetzt in jedem Schreiben**
|
||||||
|
([#21](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/21)).
|
||||||
|
Bisher ging jeder Brief ohne Rücksendeadresse hinaus — im
|
||||||
|
Absenderblock stand nur dein Name, und in der Datumszeile fehlte der
|
||||||
|
Ort. Bei einer Versicherung oder Behörde wirkt das unseriös und macht
|
||||||
|
es der Gegenstelle unnötig schwer zu antworten.
|
||||||
|
|
||||||
|
Unter **Mein Konto → Meine Anschrift** einmal Straße, PLZ und Ort
|
||||||
|
hinterlegen. Ab da stehen sie im Absenderblock jedes erzeugten
|
||||||
|
Schreibens, und die Datumszeile lautet „Bayreuth, den 23.08.2026".
|
||||||
|
Betroffen ist alles: Rückkaufswert-Anfrage, Serienabfrage, Kündigung,
|
||||||
|
Sachstandsanfrage und die eigenen Vorlagen.
|
||||||
|
|
||||||
|
- **Zwei neue Platzhalter** für eigene Vorlagen: `{{betreuer_anschrift}}`
|
||||||
|
und `{{betreuer_ort}}`.
|
||||||
|
|
||||||
|
### Eine Zusage
|
||||||
|
|
||||||
|
**Was fehlt, bleibt weg.** Jede der drei Angaben ist einzeln freiwillig.
|
||||||
|
Hinterlegst du nur den Ort, bekommst du die Datumszeile mit Ort und einen
|
||||||
|
Absenderblock, der weiterhin nur aus deinem Namen besteht — keine leeren
|
||||||
|
Zeilen, kein Komma ohne Ort. Eine halb ausgefüllte Adresse sieht nach
|
||||||
|
Fehler aus, ein bloßer Name nach Absicht.
|
||||||
|
|
||||||
|
### Datenbank
|
||||||
|
|
||||||
|
Eine Migration: drei Felder am Benutzer. Die Anschrift hängt an dir und
|
||||||
|
nicht an der betreuten Person — sie gilt für alle Akten.
|
||||||
|
|
||||||
|
### Geprüft
|
||||||
|
|
||||||
|
- Sieben Einheitstests für den Absenderblock, darunter jede
|
||||||
|
Teilbefüllung und die Gegenprobe, dass nie die Heimadresse der
|
||||||
|
betreuten Person im Absender landet.
|
||||||
|
- Vier End-to-End-Tests: der Brief ohne hinterlegte Anschrift, das
|
||||||
|
Speichern unter *Mein Konto*, der Brief danach und die Gegenprobe am
|
||||||
|
Rückkaufswert-Schreiben.
|
||||||
|
|
||||||
## [1.9.0] – 2026-08-23
|
## [1.9.0] – 2026-08-23
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
@@ -1036,7 +1078,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.9.0...HEAD
|
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...HEAD
|
||||||
|
[1.10.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.9.0...v1.10.0
|
||||||
[1.9.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.8.0...v1.9.0
|
[1.9.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.8.0...v1.9.0
|
||||||
[1.8.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.7.0...v1.8.0
|
[1.8.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.7.0...v1.8.0
|
||||||
[1.7.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.6.0...v1.7.0
|
[1.7.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.6.0...v1.7.0
|
||||||
|
|||||||
@@ -131,14 +131,14 @@ cd /opt/betreuungs-cockpit
|
|||||||
|
|
||||||
# Auf die letzte veröffentlichte Fassung wechseln,
|
# Auf die letzte veröffentlichte Fassung wechseln,
|
||||||
# nicht auf den laufenden Entwicklungsstand
|
# nicht auf den laufenden Entwicklungsstand
|
||||||
git checkout v1.9.0
|
git checkout v1.10.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kontrolle
|
### Kontrolle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
||||||
git describe --tags # v1.9.0
|
git describe --tags # v1.10.0
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -299,7 +299,7 @@ betreuungs-cockpit-sicherung-1 Up
|
|||||||
```bash
|
```bash
|
||||||
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
|
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
|
||||||
curl -s http://127.0.0.1:3000/api/gesundheit
|
curl -s http://127.0.0.1:3000/api/gesundheit
|
||||||
# {"status":"bereit","version":"1.9.0","commit":"..."}
|
# {"status":"bereit","version":"1.10.0","commit":"..."}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kommt hier nichts, sieh in die Logs:
|
Kommt hier nichts, sieh in die Logs:
|
||||||
@@ -649,7 +649,7 @@ Erst **vom Proxy-Server aus** — kommt er überhaupt an?
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s http://10.0.0.5:3000/api/gesundheit
|
curl -s http://10.0.0.5:3000/api/gesundheit
|
||||||
# {"status":"bereit","version":"1.9.0",...}
|
# {"status":"bereit","version":"1.10.0",...}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der
|
Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der
|
||||||
@@ -700,7 +700,7 @@ Zum Schluss: **Fristen → Pflichtfristen ableiten**.
|
|||||||
- Du kommst mit E-Mail, Passwort und dem Code aus der App herein.
|
- Du kommst mit E-Mail, Passwort und dem Code aus der App herein.
|
||||||
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
|
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
|
||||||
- Unter **Protokoll** siehst du deine eigene Anmeldung.
|
- Unter **Protokoll** siehst du deine eigene Anmeldung.
|
||||||
- Unten links in der Seitenleiste steht `Version 1.9.0 · <commit>`.
|
- Unten links in der Seitenleiste steht `Version 1.10.0 · <commit>`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@ alles so vor wie zuvor.
|
|||||||
| Aufruf | Wirkung |
|
| Aufruf | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `./scripts/update.sh` | auf die neueste veröffentlichte Fassung |
|
| `./scripts/update.sh` | auf die neueste veröffentlichte Fassung |
|
||||||
| `./scripts/update.sh v1.9.0` | auf eine bestimmte Fassung |
|
| `./scripts/update.sh v1.10.0` | auf eine bestimmte Fassung |
|
||||||
| `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern |
|
| `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern |
|
||||||
| `./scripts/update.sh --ja` | ohne Rückfrage (für cron) |
|
| `./scripts/update.sh --ja` | ohne Rückfrage (für cron) |
|
||||||
| `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen |
|
| `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen |
|
||||||
@@ -913,7 +913,7 @@ git fetch --tags
|
|||||||
git log --oneline HEAD..origin/main
|
git log --oneline HEAD..origin/main
|
||||||
|
|
||||||
# 3. Umschalten
|
# 3. Umschalten
|
||||||
git checkout v1.9.0
|
git checkout v1.10.0
|
||||||
|
|
||||||
# 4. Neu bauen und starten
|
# 4. Neu bauen und starten
|
||||||
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ 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
|
Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb
|
||||||
als wichtiger herausstellt, rutscht nach oben.
|
als wichtiger herausstellt, rutscht nach oben.
|
||||||
|
|
||||||
Stand: 23.08.2026 · Aktuelle Fassung: **1.9.0** ([CHANGELOG](../CHANGELOG.md))
|
Stand: 23.08.2026 · Aktuelle Fassung: **1.10.0** ([CHANGELOG](../CHANGELOG.md))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { anmelden } from './hilfen'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die eigene Anschrift im Schreiben — gegen die echte Anwendung.
|
||||||
|
*
|
||||||
|
* Der Weg, um den es geht: einmal unter „Mein Konto" hinterlegen, und
|
||||||
|
* ab da steht sie im Absenderblock und der Ort in der Datumszeile —
|
||||||
|
* ohne dass man beim Schreiben daran denken muss.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function mitDb<T>(arbeit: (db: PrismaClient) => Promise<T>): Promise<T> {
|
||||||
|
const db = new PrismaClient()
|
||||||
|
try {
|
||||||
|
return await arbeit(db)
|
||||||
|
} finally {
|
||||||
|
await db.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zurück auf „nichts hinterlegt", damit der Fall auch prüfbar bleibt. */
|
||||||
|
async function anschriftLeeren(): Promise<void> {
|
||||||
|
await mitDb(async (db) => {
|
||||||
|
await db.user.updateMany({ data: { strasse: null, plz: null, ort: null } })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Absenderanschrift', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await anschriftLeeren()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await anschriftLeeren()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ohne hinterlegte Anschrift steht nur der Name im Brief', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/schreiben/neu?art=FREI')
|
||||||
|
|
||||||
|
const text = await page.locator('textarea#text').inputValue()
|
||||||
|
const zeilen = text.split('\n')
|
||||||
|
|
||||||
|
// Erste Zeile ist der Absender, danach kommt gleich die Leerzeile.
|
||||||
|
expect(zeilen[0]).toBe('Florian Wilde')
|
||||||
|
expect(zeilen[1]).toBe('')
|
||||||
|
// Kein Ort in der Datumszeile, also auch kein Komma davor.
|
||||||
|
expect(text).not.toContain(', den ')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('speichert die Anschrift unter Mein Konto', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/konto')
|
||||||
|
|
||||||
|
await page.locator('input#strasse').fill('Cottenbacher Str. 23')
|
||||||
|
await page.locator('input#plz').fill('95445')
|
||||||
|
await page.locator('input#ort').fill('Bayreuth')
|
||||||
|
await page.getByRole('button', { name: 'Anschrift speichern' }).click()
|
||||||
|
|
||||||
|
await expect(page.getByText('Deine Anschrift ist gespeichert.')).toBeVisible()
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
await expect(page.locator('input#strasse')).toHaveValue('Cottenbacher Str. 23')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('trägt sie in Absenderblock und Datumszeile des Schreibens', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/schreiben/neu?art=FREI')
|
||||||
|
|
||||||
|
const text = await page.locator('textarea#text').inputValue()
|
||||||
|
const zeilen = text.split('\n')
|
||||||
|
|
||||||
|
expect(zeilen[0]).toBe('Florian Wilde')
|
||||||
|
expect(zeilen[1]).toBe('Cottenbacher Str. 23')
|
||||||
|
expect(zeilen[2]).toBe('95445 Bayreuth')
|
||||||
|
expect(text).toContain('Bayreuth, den ')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('steht auch im Rückkaufswert-Schreiben zum Vertrag', async ({ page }) => {
|
||||||
|
const vertragId = await mitDb(async (db) => {
|
||||||
|
const vertrag = await db.vertrag.findFirst({
|
||||||
|
where: { art: 'LEBENSVERSICHERUNG', geloeschtAm: null },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
return vertrag?.id ?? ''
|
||||||
|
})
|
||||||
|
test.skip(vertragId === '', 'Kein Lebensversicherungsvertrag im Bestand.')
|
||||||
|
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto(`/schreiben/neu?art=RUECKKAUFSWERT&vertragId=${vertragId}`)
|
||||||
|
|
||||||
|
const text = await page.locator('textarea#text').inputValue()
|
||||||
|
expect(text).toContain('Cottenbacher Str. 23')
|
||||||
|
expect(text).toContain('Bayreuth, den ')
|
||||||
|
})
|
||||||
|
})
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betreuungs-cockpit",
|
"name": "betreuungs-cockpit",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- betreuer_anschrift
|
||||||
|
--
|
||||||
|
-- Erzeugt mit "npm run db:migration". Entfernt wurden die
|
||||||
|
-- DROP-INDEX-Anweisungen zu diesen Indizes:
|
||||||
|
-- dokument_absender_trgm
|
||||||
|
-- vorgang_titel_trgm
|
||||||
|
--
|
||||||
|
-- Sie stammen aus prisma/sql/volltextsuche.sql und sind Prisma unbekannt;
|
||||||
|
-- ohne sie waere die Volltextsuche still langsam geworden.
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "benutzer" ADD COLUMN "ort" TEXT,
|
||||||
|
ADD COLUMN "plz" TEXT,
|
||||||
|
ADD COLUMN "strasse" TEXT;
|
||||||
@@ -81,6 +81,13 @@ model User {
|
|||||||
// (Passwortwechsel, Rollenaenderung, Verdacht auf Kompromittierung).
|
// (Passwortwechsel, Rollenaenderung, Verdacht auf Kompromittierung).
|
||||||
sitzungsGeneration Int @default(0)
|
sitzungsGeneration Int @default(0)
|
||||||
|
|
||||||
|
/// Die eigene Anschrift - Absenderblock und Ort in der Datumszeile
|
||||||
|
/// jedes erzeugten Schreibens. Sie gehoert zum Betreuer und nicht zur
|
||||||
|
/// betreuten Person: Sie gilt fuer alle Akten.
|
||||||
|
strasse String?
|
||||||
|
plz String?
|
||||||
|
ort String?
|
||||||
|
|
||||||
// Oberflaechen-Einstellungen
|
// Oberflaechen-Einstellungen
|
||||||
theme String @default("system") // "system" | "hell" | "dunkel"
|
theme String @default("system") // "system" | "hell" | "dunkel"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { useFormStatus } from 'react-dom'
|
||||||
|
import { AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'
|
||||||
|
import { anschriftSpeichern, type AnschriftZustand } from '@/server/actions/konto'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Feld } from '@/components/feld'
|
||||||
|
|
||||||
|
function Absenden() {
|
||||||
|
const { pending } = useFormStatus()
|
||||||
|
return (
|
||||||
|
<Button type="submit" disabled={pending}>
|
||||||
|
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
||||||
|
{pending ? 'Wird gespeichert …' : 'Anschrift speichern'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnschriftFormular({
|
||||||
|
strasse,
|
||||||
|
plz,
|
||||||
|
ort,
|
||||||
|
}: {
|
||||||
|
strasse: string | null
|
||||||
|
plz: string | null
|
||||||
|
ort: string | null
|
||||||
|
}) {
|
||||||
|
const [zustand, aktion] = useActionState<AnschriftZustand, FormData>(anschriftSpeichern, {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={aktion} className="space-y-4" noValidate>
|
||||||
|
{zustand.fehler && (
|
||||||
|
<Alert variant="fehler">
|
||||||
|
<AlertCircle aria-hidden="true" />
|
||||||
|
<AlertDescription>{zustand.fehler}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{zustand.erfolg && (
|
||||||
|
<Alert variant="erfolg">
|
||||||
|
<CheckCircle2 aria-hidden="true" />
|
||||||
|
<AlertDescription>{zustand.erfolg}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Feld id="strasse" beschriftung="Straße und Hausnummer" fehler={zustand.felder?.strasse}>
|
||||||
|
<Input
|
||||||
|
id="strasse"
|
||||||
|
name="strasse"
|
||||||
|
defaultValue={strasse ?? ''}
|
||||||
|
maxLength={150}
|
||||||
|
autoComplete="street-address"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-[8rem_1fr]">
|
||||||
|
<Feld id="plz" beschriftung="PLZ" fehler={zustand.felder?.plz}>
|
||||||
|
<Input
|
||||||
|
id="plz"
|
||||||
|
name="plz"
|
||||||
|
defaultValue={plz ?? ''}
|
||||||
|
maxLength={10}
|
||||||
|
autoComplete="postal-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
|
||||||
|
<Feld
|
||||||
|
id="ort"
|
||||||
|
beschriftung="Ort"
|
||||||
|
erklaerung="Steht außerdem in der Datumszeile: „Bayreuth, den 23.08.2026“."
|
||||||
|
fehler={zustand.felder?.ort}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="ort"
|
||||||
|
name="ort"
|
||||||
|
defaultValue={ort ?? ''}
|
||||||
|
maxLength={100}
|
||||||
|
autoComplete="address-level2"
|
||||||
|
/>
|
||||||
|
</Feld>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Absenden />
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Fingerprint, KeyRound, ShieldCheck } from 'lucide-react'
|
import { Fingerprint, KeyRound, MapPin, ShieldCheck } from 'lucide-react'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
@@ -11,6 +11,7 @@ import { PasskeyVerwaltung } from '@/components/konto/passkey-verwaltung'
|
|||||||
import { ROLLEN_BESCHRIFTUNG, ROLLEN_ERKLAERUNG } from '@/lib/auth/rollen'
|
import { ROLLEN_BESCHRIFTUNG, ROLLEN_ERKLAERUNG } from '@/lib/auth/rollen'
|
||||||
import { formatDatumZeit } from '@/lib/utils'
|
import { formatDatumZeit } from '@/lib/utils'
|
||||||
import { PasswortFormular } from './passwort-formular'
|
import { PasswortFormular } from './passwort-formular'
|
||||||
|
import { AnschriftFormular } from './anschrift-formular'
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Mein Konto' }
|
export const metadata: Metadata = { title: 'Mein Konto' }
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -22,7 +23,10 @@ export default async function KontoSeite() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl space-y-6">
|
<div className="max-w-2xl space-y-6">
|
||||||
<SeitenKopf titel="Mein Konto" beschreibung="Zugangsdaten, zweiter Faktor und Passkeys." />
|
<SeitenKopf
|
||||||
|
titel="Mein Konto"
|
||||||
|
beschreibung="Zugangsdaten, eigene Anschrift, zweiter Faktor und Passkeys."
|
||||||
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -55,6 +59,27 @@ export default async function KontoSeite() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<MapPin className="size-5" aria-hidden="true" />
|
||||||
|
Meine Anschrift
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Steht im Absenderblock jedes Schreibens, das hier entsteht, und liefert den Ort für
|
||||||
|
die Datumszeile. Ohne sie geht ein förmlicher Brief ohne Rücksendeadresse hinaus.
|
||||||
|
Jede Angabe ist einzeln freiwillig — was fehlt, bleibt im Brief einfach weg.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<AnschriftFormular
|
||||||
|
strasse={benutzer.strasse}
|
||||||
|
plz={benutzer.plz}
|
||||||
|
ort={benutzer.ort}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default async function SchreibenSeite({
|
|||||||
try {
|
try {
|
||||||
entwurf = await entwurfBauen({
|
entwurf = await entwurfBauen({
|
||||||
betreuteId: aktive.id,
|
betreuteId: aktive.id,
|
||||||
benutzerName: benutzer.name,
|
benutzer,
|
||||||
art,
|
art,
|
||||||
vertragId: text('vertragId') || null,
|
vertragId: text('vertragId') || null,
|
||||||
vorgangId: text('vorgangId') || null,
|
vorgangId: text('vorgangId') || null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'server-only'
|
import 'server-only'
|
||||||
import { db, nichtGeloescht } from '@/lib/db'
|
import { db, nichtGeloescht } from '@/lib/db'
|
||||||
import { oeffnen } from '@/lib/crypto/feld'
|
import { oeffnen } from '@/lib/crypto/feld'
|
||||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
import { briefkopfBauen, type Absender } from '@/lib/vertraege/briefkopf'
|
||||||
import {
|
import {
|
||||||
absenderBlock,
|
absenderBlock,
|
||||||
anschriftBlock,
|
anschriftBlock,
|
||||||
@@ -107,7 +107,8 @@ export function betreffAusText(text: string): string {
|
|||||||
|
|
||||||
export type EntwurfEingabe = {
|
export type EntwurfEingabe = {
|
||||||
betreuteId: string
|
betreuteId: string
|
||||||
benutzerName: string
|
/** Wer schreibt - mit Anschrift, wenn hinterlegt. */
|
||||||
|
benutzer: Absender
|
||||||
art: SchreibenArt
|
art: SchreibenArt
|
||||||
vertragId?: string | null
|
vertragId?: string | null
|
||||||
vorgangId?: string | null
|
vorgangId?: string | null
|
||||||
@@ -127,7 +128,7 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
|||||||
|
|
||||||
const kopf = briefkopfBauen({
|
const kopf = briefkopfBauen({
|
||||||
betreute: oeffnen('Betreute', betreute),
|
betreute: oeffnen('Betreute', betreute),
|
||||||
benutzer: { name: eingabe.benutzerName },
|
benutzer: eingabe.benutzer,
|
||||||
})
|
})
|
||||||
|
|
||||||
const gemeinsam = {
|
const gemeinsam = {
|
||||||
@@ -229,7 +230,7 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
|||||||
geburtsdatum: betreute.geburtsdatum,
|
geburtsdatum: betreute.geburtsdatum,
|
||||||
},
|
},
|
||||||
betreuer: {
|
betreuer: {
|
||||||
name: eingabe.benutzerName,
|
name: eingabe.benutzer.name,
|
||||||
aktenzeichenGericht: betreute.aktenzeichenGericht,
|
aktenzeichenGericht: betreute.aktenzeichenGericht,
|
||||||
betreuungsgericht: betreute.betreuungsgericht,
|
betreuungsgericht: betreute.betreuungsgericht,
|
||||||
},
|
},
|
||||||
@@ -278,6 +279,11 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
|||||||
: null,
|
: null,
|
||||||
betreute_anschrift: kopf.betreuteAnschrift,
|
betreute_anschrift: kopf.betreuteAnschrift,
|
||||||
betreuer_name: kopf.betreuerName,
|
betreuer_name: kopf.betreuerName,
|
||||||
|
betreuer_anschrift:
|
||||||
|
[kopf.betreuerStrasse, [kopf.betreuerPlz, kopf.betreuerOrt].filter(Boolean).join(' ')]
|
||||||
|
.filter((zeile) => zeile && zeile.trim() !== '')
|
||||||
|
.join('\n') || null,
|
||||||
|
betreuer_ort: kopf.betreuerOrt,
|
||||||
betreuungsgericht: kopf.betreuungsgericht,
|
betreuungsgericht: kopf.betreuungsgericht,
|
||||||
aktenzeichen_gericht: kopf.aktenzeichenGericht,
|
aktenzeichen_gericht: kopf.aktenzeichenGericht,
|
||||||
bestellung_datum: kopf.bestellungAm ? formatDatum(kopf.bestellungAm) : null,
|
bestellung_datum: kopf.bestellungAm ? formatDatum(kopf.bestellungAm) : null,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export type PlatzhalterName =
|
|||||||
| 'betreute_geburtsdatum'
|
| 'betreute_geburtsdatum'
|
||||||
| 'betreute_anschrift'
|
| 'betreute_anschrift'
|
||||||
| 'betreuer_name'
|
| 'betreuer_name'
|
||||||
|
| 'betreuer_anschrift'
|
||||||
|
| 'betreuer_ort'
|
||||||
| 'betreuungsgericht'
|
| 'betreuungsgericht'
|
||||||
| 'aktenzeichen_gericht'
|
| 'aktenzeichen_gericht'
|
||||||
| 'bestellung_datum'
|
| 'bestellung_datum'
|
||||||
@@ -49,6 +51,12 @@ export const PLATZHALTER: readonly PlatzhalterBeschreibung[] = [
|
|||||||
{ name: 'betreute_geburtsdatum', beschriftung: 'Geburtsdatum', quelle: 'Stammdaten' },
|
{ name: 'betreute_geburtsdatum', beschriftung: 'Geburtsdatum', quelle: 'Stammdaten' },
|
||||||
{ name: 'betreute_anschrift', beschriftung: 'Anschrift (Heim)', quelle: 'Stammdaten' },
|
{ name: 'betreute_anschrift', beschriftung: 'Anschrift (Heim)', quelle: 'Stammdaten' },
|
||||||
{ name: 'betreuer_name', beschriftung: 'Dein Name', quelle: 'Dein Zugang' },
|
{ name: 'betreuer_name', beschriftung: 'Dein Name', quelle: 'Dein Zugang' },
|
||||||
|
{
|
||||||
|
name: 'betreuer_anschrift',
|
||||||
|
beschriftung: 'Deine Anschrift',
|
||||||
|
quelle: 'Mein Konto',
|
||||||
|
},
|
||||||
|
{ name: 'betreuer_ort', beschriftung: 'Dein Ort', quelle: 'Mein Konto' },
|
||||||
{ name: 'betreuungsgericht', beschriftung: 'Betreuungsgericht', quelle: 'Stammdaten' },
|
{ name: 'betreuungsgericht', beschriftung: 'Betreuungsgericht', quelle: 'Stammdaten' },
|
||||||
{ name: 'aktenzeichen_gericht', beschriftung: 'Aktenzeichen des Gerichts', quelle: 'Stammdaten' },
|
{ name: 'aktenzeichen_gericht', beschriftung: 'Aktenzeichen des Gerichts', quelle: 'Stammdaten' },
|
||||||
{ name: 'bestellung_datum', beschriftung: 'Datum der Bestellung', quelle: 'Stammdaten' },
|
{ name: 'bestellung_datum', beschriftung: 'Datum der Bestellung', quelle: 'Stammdaten' },
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Muss zur "version" in der package.json passen. */
|
/** Muss zur "version" in der package.json passen. */
|
||||||
export const VERSION = '1.9.0'
|
export const VERSION = '1.10.0'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ import type { Briefkopf } from '@/lib/vertraege/anschreiben'
|
|||||||
/**
|
/**
|
||||||
* Baut den Briefkopf aus Stammdaten und angemeldetem Benutzer.
|
* Baut den Briefkopf aus Stammdaten und angemeldetem Benutzer.
|
||||||
*
|
*
|
||||||
* Die Anschrift des Betreuers steht im Datenmodell nicht - der Benutzer
|
* Die Anschrift des Betreuers haengt am Benutzer, nicht an der betreuten
|
||||||
* hat nur Name und E-Mail. Sie bleibt deshalb leer; im erzeugten Text
|
* Person: Sie gehoert zu mir und gilt fuer alle Akten. Ist sie nicht
|
||||||
* fehlen dann schlicht die beiden Zeilen, und wer den Entwurf kopiert,
|
* hinterlegt, bleiben die Zeilen weg - ein Absenderblock mit leeren
|
||||||
* ergaenzt sie im Briefkopf seines Textverarbeitungsprogramms ohnehin.
|
* Zeilen sieht schlimmer aus als einer, der nur aus dem Namen besteht.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Wer schreibt - Name und, wenn hinterlegt, die eigene Anschrift. */
|
||||||
|
export type Absender = {
|
||||||
|
name: string
|
||||||
|
strasse?: string | null
|
||||||
|
plz?: string | null
|
||||||
|
ort?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type BriefkopfQuelle = {
|
export type BriefkopfQuelle = {
|
||||||
betreute: {
|
betreute: {
|
||||||
vorname: string
|
vorname: string
|
||||||
@@ -22,7 +31,7 @@ export type BriefkopfQuelle = {
|
|||||||
aktenzeichenGericht: string | null
|
aktenzeichenGericht: string | null
|
||||||
bestellungAm: Date | null
|
bestellungAm: Date | null
|
||||||
}
|
}
|
||||||
benutzer: { name: string }
|
benutzer: Absender
|
||||||
}
|
}
|
||||||
|
|
||||||
export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
|
export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
|
||||||
@@ -37,11 +46,11 @@ export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
betreuerName: quelle.benutzer.name,
|
betreuerName: quelle.benutzer.name,
|
||||||
betreuerStrasse: null,
|
betreuerStrasse: quelle.benutzer.strasse ?? null,
|
||||||
betreuerPlz: null,
|
betreuerPlz: quelle.benutzer.plz ?? null,
|
||||||
// Der Ort in der Datumszeile ist der Ort des Betreuers, nicht der des
|
// Der Ort in der Datumszeile ist der Ort des Betreuers, nicht der
|
||||||
// Heims - er steht nirgends im Datenmodell und bleibt deshalb leer.
|
// des Heims.
|
||||||
betreuerOrt: null,
|
betreuerOrt: quelle.benutzer.ort ?? null,
|
||||||
betreuteName: `${b.anrede ? `${b.anrede} ` : ''}${b.vorname} ${b.nachname}`.trim(),
|
betreuteName: `${b.anrede ? `${b.anrede} ` : ''}${b.vorname} ${b.nachname}`.trim(),
|
||||||
betreuteGeburtsdatum: b.geburtsdatum,
|
betreuteGeburtsdatum: b.geburtsdatum,
|
||||||
betreuteAnschrift: anschrift || null,
|
betreuteAnschrift: anschrift || null,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { db, nichtGeloescht } from '@/lib/db'
|
|||||||
import { oeffnen } from '@/lib/crypto/feld'
|
import { oeffnen } from '@/lib/crypto/feld'
|
||||||
import { protokollieren } from '@/lib/audit'
|
import { protokollieren } from '@/lib/audit'
|
||||||
import { ablegenMitTyp } from '@/lib/speicher'
|
import { ablegenMitTyp } from '@/lib/speicher'
|
||||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
import { briefkopfBauen, type Absender } from '@/lib/vertraege/briefkopf'
|
||||||
import {
|
import {
|
||||||
RUECKKAUFSWERT_WIEDERVORLAGE_TAGE,
|
RUECKKAUFSWERT_WIEDERVORLAGE_TAGE,
|
||||||
rueckkaufswertAnschreiben,
|
rueckkaufswertAnschreiben,
|
||||||
@@ -53,7 +53,7 @@ export async function serienlaufDurchfuehren(eingabe: {
|
|||||||
betreuteId: string
|
betreuteId: string
|
||||||
vertragIds: string[]
|
vertragIds: string[]
|
||||||
stichtag: Date
|
stichtag: Date
|
||||||
benutzer: { id: string; name: string; email: string }
|
benutzer: { id: string; name: string; email: string } & Absender
|
||||||
}): Promise<SerienErgebnis> {
|
}): Promise<SerienErgebnis> {
|
||||||
const heute = heuteAlsTag()
|
const heute = heuteAlsTag()
|
||||||
const { betreuteId, benutzer } = eingabe
|
const { betreuteId, benutzer } = eingabe
|
||||||
@@ -65,7 +65,7 @@ export async function serienlaufDurchfuehren(eingabe: {
|
|||||||
|
|
||||||
const kopf = briefkopfBauen({
|
const kopf = briefkopfBauen({
|
||||||
betreute: oeffnen('Betreute', betreute),
|
betreute: oeffnen('Betreute', betreute),
|
||||||
benutzer: { name: benutzer.name },
|
benutzer,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Nur Vertraege dieser betreuten Person - fremde IDs laufen ins Leere.
|
// Nur Vertraege dieser betreuten Person - fremde IDs laufen ins Leere.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { erfordereBenutzer } from '@/lib/auth/sitzung'
|
import { erfordereBenutzer } from '@/lib/auth/sitzung'
|
||||||
@@ -102,3 +103,82 @@ export async function passwortAendern(
|
|||||||
await aufMindestdauerWarten(begonnen)
|
await aufMindestdauerWarten(begonnen)
|
||||||
return { erfolg: true }
|
return { erfolg: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die eigene Anschrift.
|
||||||
|
*
|
||||||
|
* Sie steht im Absenderblock jedes erzeugten Schreibens und liefert den
|
||||||
|
* Ort fuer die Datumszeile. Angaben sind einzeln freiwillig: Wer nur den
|
||||||
|
* Ort hinterlegt, bekommt "Bayreuth, den 23.08.2026" und keinen
|
||||||
|
* Absenderblock mit leeren Zeilen.
|
||||||
|
*/
|
||||||
|
const AnschriftSchema = z.object({
|
||||||
|
strasse: z.string().trim().max(150, 'Bitte höchstens 150 Zeichen.'),
|
||||||
|
plz: z.string().trim().max(10, 'Bitte höchstens 10 Zeichen.'),
|
||||||
|
ort: z.string().trim().max(100, 'Bitte höchstens 100 Zeichen.'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AnschriftZustand = {
|
||||||
|
fehler?: string
|
||||||
|
felder?: Partial<Record<'strasse' | 'plz' | 'ort', string>>
|
||||||
|
erfolg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function anschriftSpeichern(
|
||||||
|
_vorher: AnschriftZustand,
|
||||||
|
formular: FormData,
|
||||||
|
): Promise<AnschriftZustand> {
|
||||||
|
const benutzer = await erfordereBenutzer()
|
||||||
|
|
||||||
|
const gelesen = AnschriftSchema.safeParse({
|
||||||
|
strasse: formular.get('strasse') ?? '',
|
||||||
|
plz: formular.get('plz') ?? '',
|
||||||
|
ort: formular.get('ort') ?? '',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!gelesen.success) {
|
||||||
|
const felder = gelesen.error.flatten().fieldErrors
|
||||||
|
return {
|
||||||
|
fehler: 'Bitte prüfe die Eingaben.',
|
||||||
|
felder: {
|
||||||
|
strasse: felder.strasse?.[0],
|
||||||
|
plz: felder.plz?.[0],
|
||||||
|
ort: felder.ort?.[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer heisst "nicht hinterlegt", nicht "leere Zeichenkette" - sonst
|
||||||
|
// stuende im Brief eine Zeile aus Leerzeichen.
|
||||||
|
const leerZuNull = (wert: string) => (wert === '' ? null : wert)
|
||||||
|
|
||||||
|
await db.user.update({
|
||||||
|
where: { id: benutzer.id },
|
||||||
|
data: {
|
||||||
|
strasse: leerZuNull(gelesen.data.strasse),
|
||||||
|
plz: leerZuNull(gelesen.data.plz),
|
||||||
|
ort: leerZuNull(gelesen.data.ort),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await protokollieren({
|
||||||
|
aktion: 'GEAENDERT',
|
||||||
|
entitaet: 'User',
|
||||||
|
entitaetId: benutzer.id,
|
||||||
|
bezeichnung: benutzer.name,
|
||||||
|
userId: benutzer.id,
|
||||||
|
userEmail: benutzer.email,
|
||||||
|
userName: benutzer.name,
|
||||||
|
aenderung: {
|
||||||
|
anschrift: {
|
||||||
|
alt: [benutzer.strasse, benutzer.plz, benutzer.ort].filter(Boolean).join(', ') || null,
|
||||||
|
neu:
|
||||||
|
[gelesen.data.strasse, gelesen.data.plz, gelesen.data.ort].filter(Boolean).join(', ') ||
|
||||||
|
null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
revalidatePath('/konto')
|
||||||
|
return { erfolg: 'Deine Anschrift ist gespeichert.' }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
||||||
|
import { absenderBlock, datumsZeile } from '@/lib/vertraege/anschreiben'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Absenderblock.
|
||||||
|
*
|
||||||
|
* Ein foermliches Schreiben ohne Ruecksendeadresse wirkt unserioes und
|
||||||
|
* macht es der Gegenstelle schwer zu antworten. Umgekehrt gilt: Lieber
|
||||||
|
* nur der Name als ein Block mit leeren Zeilen - eine halb ausgefuellte
|
||||||
|
* Adresse sieht nach Fehler aus, ein blosser Name nach Absicht.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BETREUTE = {
|
||||||
|
vorname: 'Erika',
|
||||||
|
nachname: 'Musterfrau',
|
||||||
|
anrede: 'Frau',
|
||||||
|
geburtsdatum: new Date(Date.UTC(1943, 3, 17)),
|
||||||
|
heimName: 'Seniorenheim Sonnenhof',
|
||||||
|
heimStrasse: 'Heimweg 3',
|
||||||
|
heimPlz: '95445',
|
||||||
|
heimOrt: 'Bayreuth',
|
||||||
|
betreuungsgericht: 'Amtsgericht Bayreuth',
|
||||||
|
aktenzeichenGericht: 'XVII 412/25',
|
||||||
|
bestellungAm: new Date(Date.UTC(2026, 1, 14)),
|
||||||
|
}
|
||||||
|
|
||||||
|
const VOLLSTAENDIG = {
|
||||||
|
name: 'Florian Wilde',
|
||||||
|
strasse: 'Cottenbacher Str. 23',
|
||||||
|
plz: '95445',
|
||||||
|
ort: 'Bayreuth',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Absenderblock', () => {
|
||||||
|
it('nennt Name, Straße und Ort', () => {
|
||||||
|
const kopf = briefkopfBauen({ betreute: BETREUTE, benutzer: VOLLSTAENDIG })
|
||||||
|
expect(absenderBlock(kopf)).toBe('Florian Wilde\nCottenbacher Str. 23\n95445 Bayreuth')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bleibt beim Namen, wenn keine Anschrift hinterlegt ist', () => {
|
||||||
|
// Kein Block mit leeren Zeilen - das sieht nach Fehler aus.
|
||||||
|
const kopf = briefkopfBauen({ betreute: BETREUTE, benutzer: { name: 'Florian Wilde' } })
|
||||||
|
expect(absenderBlock(kopf)).toBe('Florian Wilde')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt die Straße weg, wenn nur der Ort bekannt ist', () => {
|
||||||
|
const kopf = briefkopfBauen({
|
||||||
|
betreute: BETREUTE,
|
||||||
|
benutzer: { name: 'Florian Wilde', plz: '95445', ort: 'Bayreuth' },
|
||||||
|
})
|
||||||
|
expect(absenderBlock(kopf)).toBe('Florian Wilde\n95445 Bayreuth')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verträgt einen Ort ohne PLZ', () => {
|
||||||
|
const kopf = briefkopfBauen({
|
||||||
|
betreute: BETREUTE,
|
||||||
|
benutzer: { name: 'Florian Wilde', ort: 'Bayreuth' },
|
||||||
|
})
|
||||||
|
expect(absenderBlock(kopf)).toBe('Florian Wilde\nBayreuth')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('nimmt die Anschrift vom Betreuer, nicht vom Heim', () => {
|
||||||
|
// Die Heimadresse ist die der betreuten Person und gehört in den
|
||||||
|
// Anschriftenblock, nie in den Absender.
|
||||||
|
const kopf = briefkopfBauen({ betreute: BETREUTE, benutzer: VOLLSTAENDIG })
|
||||||
|
expect(absenderBlock(kopf)).not.toContain('Heimweg')
|
||||||
|
expect(kopf.betreuteAnschrift).toContain('Heimweg 3')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Datumszeile', () => {
|
||||||
|
const heute = new Date(Date.UTC(2026, 7, 23))
|
||||||
|
|
||||||
|
it('stellt den Ort des Betreuers voran', () => {
|
||||||
|
const kopf = briefkopfBauen({ betreute: BETREUTE, benutzer: VOLLSTAENDIG })
|
||||||
|
expect(datumsZeile(kopf.betreuerOrt, heute)).toBe('Bayreuth, den 23.08.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt ohne Ort das Komma weg', () => {
|
||||||
|
const kopf = briefkopfBauen({ betreute: BETREUTE, benutzer: { name: 'Florian Wilde' } })
|
||||||
|
expect(datumsZeile(kopf.betreuerOrt, heute)).toBe('23.08.2026')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user