fix(streak): Feiertage, Wochenend-Edge & Lookback-Deckel
ci / quality (push) Failing after 38s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 55s

- Feiertage: isWerktag berücksichtigt jetzt die 9 bundesweiten gesetzlichen
  Feiertage (Oster-Computus + feste Tage) → ein Feiertag bricht die Streak
  nicht mehr (zählt wie ein Wochenende neutral). Landesspezifische Feiertage
  bewusst (noch) nicht enthalten.
- Wochenend-Edge: Reflexion an Sa/So setzte die angezeigte Streak auf 0
  (Schleife brach am Nicht-Werktag ab). Jetzt wird übersprungen statt gebrochen.
- Lookback von 120 auf 400 Tage erhöht, damit lange Streaks nicht
  abgeschnitten werden.
- Streak-Zähllogik in reine, getestete countStreakDays() ausgelagert
  (8 Streak- + 5 Feiertags-Tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 15:42:25 +02:00
1 parent 1f0c9efd59
commit bcae44eeb3
7 files changed
+256 -47

No files matched your search

+11 -35
View File
@@ -1,23 +1,20 @@
import { and, eq, gte, lte } from 'drizzle-orm';
import { TRPCError } from '@trpc/server';
import { user, dayEntry, absenceDay } from '@cockpit/db';
import type { Db } from '@cockpit/db';
import { DayEntrySchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
import {
todayIso,
isWerktag,
previousWerktag,
parseIsoDate,
formatIsoDate,
} from '../lib/werktag.js';
import { todayIso, isWerktag, parseIsoDate, formatIsoDate } from '../lib/werktag.js';
import { countStreakDays } from '../lib/streak.js';
async function computeStreak(db: import('@cockpit/db').Db, userId: string): Promise<number> {
async function computeStreak(db: Db, userId: string): Promise<number> {
const today = todayIso();
// Hole alle relevanten DayEntries und Abwesenheiten der letzten 90 Tage.
// DayEntries + Abwesenheiten der letzten 400 Tage (deckt > 1 Jahr Werktage ab,
// damit lange Streaks nicht am Lookback-Fenster abgeschnitten werden).
const endIso = today;
const start = parseIsoDate(today);
start.setUTCDate(start.getUTCDate() - 120);
start.setUTCDate(start.getUTCDate() - 400);
const startIso = formatIsoDate(start);
const [entries, absences] = await Promise.all([
@@ -39,31 +36,11 @@ async function computeStreak(db: import('@cockpit/db').Db, userId: string): Prom
),
]);
const fullByDate = new Map<string, boolean>();
for (const e of entries) fullByDate.set(e.date, e.status === 'full');
const absSet = new Set(absences.map((a) => a.date));
const fullDates = new Set<string>();
for (const e of entries) if (e.status === 'full') fullDates.add(e.date);
const absenceDates = new Set(absences.map((a) => a.date));
// Zähle aufeinanderfolgende volle Werktage rückwärts ab heute.
let cursor = today;
let streak = 0;
// Wenn heute noch nicht voll: starten beim vorherigen Werktag.
if (!fullByDate.get(today)) {
cursor = previousWerktag(today);
}
while (true) {
if (!isWerktag(cursor)) break;
if (absSet.has(cursor)) {
cursor = previousWerktag(cursor);
continue;
}
if (fullByDate.get(cursor)) {
streak++;
cursor = previousWerktag(cursor);
continue;
}
break;
}
return streak;
return countStreakDays({ today, fullDates, absenceDates });
}
export const userRouter = router({
@@ -95,7 +72,6 @@ export const userRouter = router({
let cursor = input.from;
const toInsert: { userId: string; date: string; reason: 'urlaub' | 'krank' | 'sonstiges' }[] =
[];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (cursor <= input.to) {
if (isWerktag(cursor)) {
toInsert.push({ userId: ctx.user.id, date: cursor, reason: input.reason });