Fix #40: Ausschlussfristen nur bei Fälligkeit hervorheben #47

Merged
florian.wilde merged 1 commits from fix/40-ausschlussfristen-horizont into main 2026-09-01 13:51:00 +00:00
2 changed files with 114 additions and 2 deletions
Showing only changes of commit b3823094d6 - Show all commits
+37 -1
View File
@@ -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,
}
}
+77 -1
View File
@@ -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', () => {