diff --git a/src/lib/dashboard/bewertung.ts b/src/lib/dashboard/bewertung.ts index a9cbcaa..0142e44 100644 --- a/src/lib/dashboard/bewertung.ts +++ b/src/lib/dashboard/bewertung.ts @@ -24,6 +24,42 @@ export const WARTEZEIT_DRINGEND_TAGE = 21 /** Wie viele Ausschlussfristen zeigt die Warnung hoechstens? */ const AUSSCHLUSSFRISTEN_MAX = 3 +/** + * Wie weit im Voraus eine Ausschlussfrist die Warnung ausloest. + * + * Frueher gab es diese Grenze nicht: Der Warnblock fragte nur, OB etwas + * eine Ausschlussfrist ist, nie WANN sie faellig wird. Auf dem Dashboard + * standen deshalb dauerhaft drei rote Kaesten mit Warndreieck - fuer + * Aufwandspauschalen, die 2027, 2028 und 2029 ablaufen. Die Pflichtfristen + * werden bewusst drei Jahre im Voraus angelegt (jahreVoraus in + * pflichtfristen.ts), und alle landeten ungefiltert in der Warnung. + * + * Das ist nicht nur laestig, sondern gefaehrlich: Eine Warnung, die + * monatelang dasteht und nichts von einem will, stumpft ab. Man uebersieht + * die vierte, die etwas will - und bei § 1878 BGB ist das die + * Jahrespauschale, die ersatzlos erlischt. + * + * 90 Tage, weil die kuerzeste hier vorkommende Ausschlussfrist sechs + * Monate Zeit laesst. Drei Monate Vorlauf sind reichlich, und solange + * nichts ansteht, bleibt der Kasten leer - dann bedeutet er etwas. + * + * Verschwinden tut nichts: Was ausserhalb des Horizonts liegt, steht + * weiterhin in der Fristenliste. + */ +const AUSSCHLUSSFRIST_HORIZONT_TAGE = 90 + +/** + * Gehoert diese Frist in den hervorgehobenen Warnblock? + * + * Abgelaufenes immer - eine versaeumte Ausschlussfrist bleibt eine + * schlechte Nachricht, auch wenn sie lange zurueckliegt. Sonst nur, was + * innerhalb des Horizonts faellig wird. + */ +function warntJetzt(eintrag: { ausschlussfrist: boolean; tage: number }): boolean { + if (!eintrag.ausschlussfrist) return false + return eintrag.tage < 0 || eintrag.tage <= AUSSCHLUSSFRIST_HORIZONT_TAGE +} + /** * Datumsfelder ohne Uhrzeit (@db.Date) kommen als UTC-Mitternacht aus der * Datenbank. Wer sie unbesehen mit einem lokalen "heute" vergleicht, @@ -126,7 +162,7 @@ export function fristenEinordnen(fristen: FristRoh[], heute: Date): FristenAmpel dieseWoche: eintraege.filter((e) => e.stufe === 'orange'), diesenMonat: eintraege.filter((e) => e.stufe === 'gelb'), weitereOffen: eintraege.filter((e) => e.stufe === 'gruen').length, - ausschlussfristen: eintraege.filter((e) => e.ausschlussfrist).slice(0, AUSSCHLUSSFRISTEN_MAX), + ausschlussfristen: eintraege.filter(warntJetzt).slice(0, AUSSCHLUSSFRISTEN_MAX), gesamtOffen: eintraege.length, } } diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts index 5310bd1..961f54b 100644 --- a/tests/dashboard.test.ts +++ b/tests/dashboard.test.ts @@ -92,9 +92,10 @@ describe('Fristen-Ampel', () => { [ frist({ id: 'normal', datum: dbTag('2026-08-20') }), frist({ + // Weit genug weg fuer "gruen", nah genug fuer die Warnung. id: 'pauschale', titel: 'Aufwandspauschale 2026', - datum: dbTag('2026-12-31'), + datum: dbTag('2026-10-15'), ausschlussfrist: true, rechtsgrundlage: '§ 1878 BGB', }), @@ -103,6 +104,81 @@ describe('Fristen-Ampel', () => { ) expect(daten.ausschlussfristen.map((e) => e.id)).toEqual(['pauschale']) + // Nicht in einem der Faelligkeitstoepfe - der Warnblock steht daneben. + expect(daten.weitereOffen).toBe(1) + }) + + describe('Ausschlussfristen: nur warnen, wenn es etwas zu tun gibt', () => { + /** + * Der Anlass: Auf dem Dashboard standen dauerhaft drei rote Kaesten mit + * Warndreieck - Aufwandspauschalen mit Verfall 2027, 2028 und 2029. Die + * Pflichtfristen werden bewusst drei Jahre im Voraus angelegt, und der + * Warnblock fragte nur, OB etwas eine Ausschlussfrist ist, nie WANN sie + * faellig wird. + * + * Eine Warnung, die monatelang dasteht und nichts von einem will, + * stumpft ab. Genau davor schuetzen diese Tests. + */ + function pauschale(id: string, datum: string) { + return frist({ + id, + titel: `Aufwandspauschale ${id}`, + datum: dbTag(datum), + ausschlussfrist: true, + rechtsgrundlage: '§ 1878 BGB', + }) + } + + it('lässt Fristen weit in der Zukunft aus der Warnung heraus', () => { + const daten = fristenEinordnen( + [ + pauschale('2027', '2027-05-06'), + pauschale('2028', '2028-05-06'), + pauschale('2029', '2029-05-06'), + ], + HEUTE, + ) + + expect(daten.ausschlussfristen).toEqual([]) + // Verschwunden sind sie deshalb nicht - sie stehen in der Liste. + expect(daten.weitereOffen).toBe(3) + expect(daten.gesamtOffen).toBe(3) + }) + + it('warnt, sobald eine in den Horizont rückt', () => { + // 90 Tage nach dem 19.08.2026 ist der 17.11.2026. + const daten = fristenEinordnen([pauschale('bald', '2026-11-17')], HEUTE) + + expect(daten.ausschlussfristen.map((e) => e.id)).toEqual(['bald']) + }) + + it('warnt einen Tag davor noch nicht', () => { + const daten = fristenEinordnen([pauschale('knapp-drueber', '2026-11-18')], HEUTE) + + expect(daten.ausschlussfristen).toEqual([]) + }) + + it('warnt bei einer versäumten Frist immer, egal wie lange sie zurückliegt', () => { + // Eine verfallene Aufwandspauschale bleibt eine schlechte Nachricht. + // Sie stillschweigend aus der Warnung zu nehmen, waere das Gegenteil + // von dem, was hier gewollt ist. + const daten = fristenEinordnen([pauschale('verpasst', '2024-05-06')], HEUTE) + + expect(daten.ausschlussfristen.map((e) => e.id)).toEqual(['verpasst']) + }) + + it('mischt Fälliges und Fernes richtig', () => { + const daten = fristenEinordnen( + [ + pauschale('verpasst', '2025-05-06'), + pauschale('bald', '2026-10-01'), + pauschale('fern', '2029-05-06'), + ], + HEUTE, + ) + + expect(daten.ausschlussfristen.map((e) => e.id)).toEqual(['verpasst', 'bald']) + }) }) it('warnt bei einer Ausschlussfrist ausdrücklich vor dem ersatzlosen Verfall', () => {