diff --git a/apps/api/src/routers/time.ts b/apps/api/src/routers/time.ts index 400fe55..10efbaf 100644 --- a/apps/api/src/routers/time.ts +++ b/apps/api/src/routers/time.ts @@ -8,6 +8,31 @@ import { todayIso } from '../lib/werktag.js'; const idInput = z.object({ id: z.string().uuid() }); +/** + * Wall-Clock-Sekunden: Vereinigung überlappender Zeitintervalle. Parallel + * laufende Einträge (mehrere Tasks gleichzeitig) zählen nur einmal — damit + * „Gesamt heute" die tatsächlich anwesende Zeit zeigt, nicht die Summe. + */ +function unionSeconds(intervals: Array): number { + const valid = intervals.filter(([s, e]) => e > s).sort((a, b) => a[0] - b[0]); + if (valid.length === 0) return 0; + let total = 0; + let curStart = valid[0]![0]; + let curEnd = valid[0]![1]; + for (let i = 1; i < valid.length; i++) { + const [s, e] = valid[i]!; + if (s <= curEnd) { + if (e > curEnd) curEnd = e; + } else { + total += curEnd - curStart; + curStart = s; + curEnd = e; + } + } + total += curEnd - curStart; + return Math.round(total / 1000); +} + async function stopCurrent( tx: import('@cockpit/db').Db, userId: string, @@ -117,12 +142,19 @@ export const timeRouter = router({ ), ) .orderBy(desc(timeEntry.startedAt)); - const totalSeconds = rows.reduce((acc, r) => { - if (r.endedAt && r.durationSeconds) return acc + r.durationSeconds; - if (!r.endedAt) return acc + Math.round((Date.now() - r.startedAt.getTime()) / 1000); - return acc; + const now = Date.now(); + // „Gesamt" = Wall-Clock (überlappende Tasks zählen einmal). + const totalSeconds = unionSeconds( + rows.map((r) => [r.startedAt.getTime(), r.endedAt ? r.endedAt.getTime() : now] as const), + ); + // „getrackt" = Summe aller Task-Zeiten (kann bei parallelem Tracken größer sein). + const trackedSeconds = rows.reduce((acc, r) => { + if (r.endedAt) { + return acc + (r.durationSeconds ?? Math.round((r.endedAt.getTime() - r.startedAt.getTime()) / 1000)); + } + return acc + Math.round((now - r.startedAt.getTime()) / 1000); }, 0); - return { entries: rows, totalSeconds }; + return { entries: rows, totalSeconds, trackedSeconds }; }), /** @@ -244,9 +276,10 @@ export const timeRouter = router({ }; type Bucket = { userId: string; - totalSeconds: number; + trackedSeconds: number; // Summe aller Task-Zeiten (Parallel-Tracking) byCategory: Record; active: Active | null; + intervals: Array; }; const now = Date.now(); @@ -255,16 +288,18 @@ export const timeRouter = router({ for (const r of rows) { const bucket: Bucket = perUser.get(r.userId) ?? { userId: r.userId, - totalSeconds: 0, + trackedSeconds: 0, byCategory: {}, active: null, + intervals: [], }; const duration = r.endedAt ? (r.durationSeconds ?? Math.round((r.endedAt.getTime() - r.startedAt.getTime()) / 1000)) : Math.round((now - r.startedAt.getTime()) / 1000); - bucket.totalSeconds += duration; + bucket.trackedSeconds += duration; bucket.byCategory[r.category] = (bucket.byCategory[r.category] ?? 0) + duration; + bucket.intervals.push([r.startedAt.getTime(), r.endedAt ? r.endedAt.getTime() : now]); if (!r.endedAt) { bucket.active = { startedAt: r.startedAt.toISOString(), @@ -277,6 +312,13 @@ export const timeRouter = router({ perUser.set(r.userId, bucket); } - return Array.from(perUser.values()); + // „Gesamt" pro Person = Wall-Clock (Vereinigung der Intervalle). + return Array.from(perUser.values()).map((b) => ({ + userId: b.userId, + totalSeconds: unionSeconds(b.intervals), + trackedSeconds: b.trackedSeconds, + byCategory: b.byCategory, + active: b.active, + })); }), }); diff --git a/apps/web/src/components/time/TeamTimeSection.tsx b/apps/web/src/components/time/TeamTimeSection.tsx index 53aeb58..433611c 100644 --- a/apps/web/src/components/time/TeamTimeSection.tsx +++ b/apps/web/src/components/time/TeamTimeSection.tsx @@ -79,13 +79,22 @@ export function TeamTimeSection() {

Gesamt:{' '} {formatDurationCompact(it.totalSeconds)} + {it.trackedSeconds > it.totalSeconds + 60 && ( + + {' · getrackt '} + {formatDurationCompact(it.trackedSeconds)} + + )}

{it.active ? : } - + ))}