import { describe, expect, it } from 'vitest' import { KontaktSchema, KontaktprotokollSchema, kontaktFormularLesen, protokollFormularLesen, tagFuerFormular, } from '@/lib/validierung/kontakte' import { dauerAusgeschrieben, statistikBerechnen, textbausteinErzeugen, textkontextAusAnrede, type ProtokollFuerStatistik, } from '@/lib/kontakte/statistik' /** * Kontakte und Kontaktprotokoll. Die Protokolle sind die Grundlage des * Jahresberichts nach § 1863 BGB - was hier falsch gezaehlt wird, steht * spaeter falsch im Bericht ans Betreuungsgericht. */ function formular(werte: Record): FormData { const daten = new FormData() for (const [name, wert] of Object.entries(werte)) daten.set(name, wert) return daten } /** Ein Protokollzeitstempel liegt zur Mittagszeit in UTC. */ function tag(text: string): Date { return new Date(`${text}T12:00:00.000Z`) } describe('Kontakt erfassen', () => { it('braucht einen Namen und eine Rolle, alles andere ist freiwillig', () => { const ergebnis = KontaktSchema.safeParse({ name: 'Frau Krämer', rolle: 'RECHTSPFLEGER' }) expect(ergebnis.success).toBe(true) if (ergebnis.success) { expect(ergebnis.data.name).toBe('Frau Krämer') expect(ergebnis.data.telefon).toBeNull() } }) it('weist einen fehlenden Namen mit einer verständlichen Meldung ab', () => { const ergebnis = KontaktSchema.safeParse({ name: ' ', rolle: 'HEIMLEITUNG' }) expect(ergebnis.success).toBe(false) if (!ergebnis.success) { expect(ergebnis.error.flatten().fieldErrors.name?.[0]).toContain('Namen angeben') } }) it('macht aus leeren Feldern NULL, nicht aus Versehen einen leeren Text', () => { const ergebnis = KontaktSchema.parse({ name: 'Amtsgericht Bayreuth', rolle: 'RECHTSPFLEGER', organisation: '', telefon: '', aktenzeichen: '', }) expect(ergebnis.organisation).toBeNull() expect(ergebnis.telefon).toBeNull() expect(ergebnis.aktenzeichen).toBeNull() }) it('lässt das E-Mail-Feld leer, akzeptiert aber keine kaputte Adresse', () => { expect(KontaktSchema.safeParse({ name: 'Dr. Weber', rolle: 'HAUSARZT', email: '' }).success).toBe( true, ) const kaputt = KontaktSchema.safeParse({ name: 'Dr. Weber', rolle: 'HAUSARZT', email: 'weber(at)praxis.de', }) expect(kaputt.success).toBe(false) }) it('schreibt E-Mail-Adressen klein und schneidet Leerzeichen ab', () => { const ergebnis = KontaktSchema.parse({ name: 'Bezirk Oberfranken', rolle: 'SACHBEARBEITER_BEZIRK', email: ' Sachbearbeitung@Bezirk-Oberfranken.DE ', }) expect(ergebnis.email).toBe('sachbearbeitung@bezirk-oberfranken.de') }) it('nimmt eine unbekannte Rolle nicht an', () => { expect(KontaktSchema.safeParse({ name: 'X', rolle: 'HAUSMEISTER' }).success).toBe(false) }) it('liest ein Formular vollständig ein', () => { const roh = kontaktFormularLesen( formular({ name: 'Frau Krämer', rolle: 'RECHTSPFLEGER', telefon: '0921 504-0' }), ) const ergebnis = KontaktSchema.parse(roh) expect(ergebnis.telefon).toBe('0921 504-0') }) }) describe('Kontaktprotokoll erfassen', () => { const grundgeruest = { datum: '2026-08-19', art: 'BESUCH' } it('kommt mit Datum und Art aus – das ist die Schnellerfassung', () => { const ergebnis = KontaktprotokollSchema.safeParse(grundgeruest) expect(ergebnis.success).toBe(true) if (ergebnis.success) { expect(ergebnis.data.dauerMinuten).toBeNull() expect(ergebnis.data.mitBetreuter).toBe(false) expect(ergebnis.data.inhalt).toBeNull() } }) it('legt das Datum auf die Mittagszeit, damit keine Zeitzone den Tag kippt', () => { const ergebnis = KontaktprotokollSchema.parse(grundgeruest) expect(ergebnis.datum.toISOString()).toBe('2026-08-19T12:00:00.000Z') expect(tagFuerFormular(ergebnis.datum)).toBe('2026-08-19') }) it('besteht auf einem Datum', () => { const ergebnis = KontaktprotokollSchema.safeParse({ datum: '', art: 'TELEFONAT' }) expect(ergebnis.success).toBe(false) if (!ergebnis.success) { expect(ergebnis.error.flatten().fieldErrors.datum?.[0]).toContain('Datum') } }) it('nimmt die Dauer als Zahl an und lässt sie auch weg', () => { expect(KontaktprotokollSchema.parse({ ...grundgeruest, dauerMinuten: '45' }).dauerMinuten).toBe( 45, ) expect(KontaktprotokollSchema.parse({ ...grundgeruest, dauerMinuten: '' }).dauerMinuten).toBeNull() }) it('weist eine unsinnige Dauer ab', () => { expect( KontaktprotokollSchema.safeParse({ ...grundgeruest, dauerMinuten: '-10' }).success, ).toBe(false) expect( KontaktprotokollSchema.safeParse({ ...grundgeruest, dauerMinuten: '2000' }).success, ).toBe(false) expect( KontaktprotokollSchema.safeParse({ ...grundgeruest, dauerMinuten: 'lang' }).success, ).toBe(false) }) it('übersetzt das Häkchen für den persönlichen Kontakt in einen Wahrheitswert', () => { const angehakt = protokollFormularLesen( formular({ datum: '2026-08-19', art: 'BESUCH', mitBetreuter: 'on' }), ) expect(KontaktprotokollSchema.parse(angehakt).mitBetreuter).toBe(true) const leer = protokollFormularLesen(formular({ datum: '2026-08-19', art: 'BESUCH' })) expect(KontaktprotokollSchema.parse(leer).mitBetreuter).toBe(false) }) it('behandelt eine nicht gewählte Verknüpfung als "keine"', () => { const roh = protokollFormularLesen( formular({ datum: '2026-08-19', art: 'BESUCH', kontaktId: '', vorgangId: '' }), ) const ergebnis = KontaktprotokollSchema.parse(roh) expect(ergebnis.kontaktId).toBeNull() expect(ergebnis.vorgangId).toBeNull() }) it('nimmt eine Kontaktart, die es nicht gibt, nicht an', () => { expect(KontaktprotokollSchema.safeParse({ datum: '2026-08-19', art: 'TAUBENPOST' }).success).toBe( false, ) }) }) describe('Auswertung für den Jahresbericht', () => { const VON = new Date('2026-01-01T00:00:00.000Z') const BIS = new Date('2026-12-31T23:59:59.999Z') function eintrag(teil: Partial & { datum: Date }): ProtokollFuerStatistik { return { art: 'BESUCH', dauerMinuten: null, mitBetreuter: false, ...teil } } it('zählt die Kontakte je Monat und darunter die persönlichen', () => { const statistik = statistikBerechnen( [ eintrag({ datum: tag('2026-01-12'), mitBetreuter: true }), eintrag({ datum: tag('2026-01-26'), art: 'TELEFONAT' }), eintrag({ datum: tag('2026-02-03'), mitBetreuter: true }), ], VON, BIS, ) expect(statistik.jeMonat).toEqual([ { jahr: 2026, monat: 1, gesamt: 2, persoenlich: 1 }, { jahr: 2026, monat: 2, gesamt: 1, persoenlich: 1 }, ]) expect(statistik.gesamt).toBe(3) expect(statistik.persoenlich).toBe(2) }) it('zählt zwei Besuche am selben Tag als einen Besuchstag', () => { const statistik = statistikBerechnen( [ eintrag({ datum: tag('2026-03-04'), mitBetreuter: true }), eintrag({ datum: tag('2026-03-04'), mitBetreuter: true }), eintrag({ datum: tag('2026-03-05'), mitBetreuter: true }), ], VON, BIS, ) expect(statistik.persoenlich).toBe(3) expect(statistik.besuchstage).toBe(2) }) it('rechnet einen Anruf beim Bezirk nicht als persönlichen Kontakt zur Betreuten', () => { const statistik = statistikBerechnen( [eintrag({ datum: tag('2026-04-02'), art: 'TELEFONAT', mitBetreuter: false })], VON, BIS, ) expect(statistik.telefonate).toBe(1) expect(statistik.persoenlich).toBe(0) expect(statistik.besuchstage).toBe(0) }) it('summiert nur die tatsächlich erfassten Dauern', () => { const statistik = statistikBerechnen( [ eintrag({ datum: tag('2026-05-02'), dauerMinuten: 45 }), eintrag({ datum: tag('2026-05-09'), dauerMinuten: 90 }), eintrag({ datum: tag('2026-05-16'), dauerMinuten: null }), ], VON, BIS, ) expect(statistik.gesamtdauerMinuten).toBe(135) expect(statistik.mitDauerErfasst).toBe(2) expect(statistik.gesamt).toBe(3) }) it('zählt die Monate des Berichtszeitraums mit', () => { expect(statistikBerechnen([], VON, BIS).monateImZeitraum).toBe(12) expect( statistikBerechnen([], new Date('2026-03-01T00:00:00Z'), new Date('2026-05-31T00:00:00Z')) .monateImZeitraum, ).toBe(3) }) it('schreibt einen Textbaustein in ganzen Sätzen mit Zeitraum und Zahlen', () => { const protokolle: ProtokollFuerStatistik[] = [] // Ein Besuch je Monat, dazu zwei Telefonate. for (let monat = 1; monat <= 12; monat += 1) { const mm = String(monat).padStart(2, '0') protokolle.push( eintrag({ datum: tag(`2026-${mm}-10`), mitBetreuter: true, dauerMinuten: 60 }), ) } protokolle.push(eintrag({ datum: tag('2026-03-20'), art: 'TELEFONAT' })) protokolle.push(eintrag({ datum: tag('2026-09-08'), art: 'TELEFONAT' })) const statistik = statistikBerechnen(protokolle, VON, BIS) const text = textbausteinErzeugen( statistik, textkontextAusAnrede('Frau', 'Pflegeheim Sonnenhof'), ) expect(text).toContain('Im Berichtszeitraum vom 01.01.2026 bis 31.12.2026') expect(text).toContain('regelmäßiger persönlicher Kontakt zur Betreuten') expect(text).toContain('an 12 Tagen persönlich im Pflegeheim Sonnenhof besucht') expect(text).toContain('2 Telefonate') expect(text).toContain('12 Stunden') expect(text).toContain('12 von 12 Monaten') }) it('nennt es nicht "regelmäßig", wenn der Kontakt nur sporadisch war', () => { const statistik = statistikBerechnen( [ eintrag({ datum: tag('2026-02-10'), mitBetreuter: true }), eintrag({ datum: tag('2026-08-14'), mitBetreuter: true }), ], VON, BIS, ) const text = textbausteinErzeugen(statistik) expect(text).toContain('bestand persönlicher Kontakt') expect(text).not.toContain('regelmäßiger') }) it('beschönigt nichts, wenn kein persönlicher Kontakt dokumentiert ist', () => { const statistik = statistikBerechnen( [eintrag({ datum: tag('2026-06-02'), art: 'BRIEF' })], VON, BIS, ) const text = textbausteinErzeugen(statistik, textkontextAusAnrede('Herr', null)) expect(text).toContain('kein persönlicher Kontakt zum Betreuten dokumentiert') }) it('sagt bei einem leeren Zeitraum, dass nachzutragen ist', () => { const text = textbausteinErzeugen(statistikBerechnen([], VON, BIS)) expect(text).toContain('kein Kontakt dokumentiert') expect(text).toContain('nachzutragen') }) it('schreibt Dauern in Stunden und Minuten aus', () => { expect(dauerAusgeschrieben(45)).toBe('45 Minuten') expect(dauerAusgeschrieben(60)).toBe('1 Stunde') expect(dauerAusgeschrieben(135)).toBe('2 Stunden 15 Minuten') expect(dauerAusgeschrieben(0)).toBe('0 Minuten') }) it('wählt die Anrede passend zur betreuten Person', () => { expect(textkontextAusAnrede('Frau', null).kontaktZu).toBe('zur Betreuten') expect(textkontextAusAnrede('Herr', null).akkusativ).toBe('den Betreuten') expect(textkontextAusAnrede(null, null).kontaktZu).toBe('zur betreuten Person') }) })