fix(streak): Feiertage, Wochenend-Edge & Lookback-Deckel
- 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:
1 parent
1f0c9efd59
commit
bcae44eeb3
7 files changed
+256
-47
No files matched your search
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user