fix(zeit): Gesamt-Zeit als Wall-Clock statt Summe paralleler Tasks
"Zeit im Team heute" (und "Zeit heute") summierten alle Task-Dauern — wer 5 Tasks parallel trackt, hatte 10h statt 2h Anwesenheit. Gesamt wird jetzt als Vereinigung überlappender Intervalle berechnet (unionSeconds). Pro-Aufgabe/Kategorie bleibt separat als Summe erhalten und wird als "getrackt X" angezeigt; der Verteilungsbalken nutzt diese Summe als Nenner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
43eeabaea2
commit
b7b54fcec5
2 files changed
+61
-10
No files matched your search
@@ -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<readonly [number, number]>): 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<string, number>;
|
||||
active: Active | null;
|
||||
intervals: Array<readonly [number, number]>;
|
||||
};
|
||||
|
||||
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,
|
||||
}));
|
||||
}),
|
||||
});
|
||||
@@ -79,13 +79,22 @@ export function TeamTimeSection() {
|
||||
<p className="text-xs text-[var(--color-ink-muted)]">
|
||||
Gesamt:{' '}
|
||||
<span className="font-mono">{formatDurationCompact(it.totalSeconds)}</span>
|
||||
{it.trackedSeconds > it.totalSeconds + 60 && (
|
||||
<span
|
||||
className="text-[var(--color-ink-faint)]"
|
||||
title="Über parallele Aufgaben getrackte Summe (zählt überlappende Zeit mehrfach)."
|
||||
>
|
||||
{' · getrackt '}
|
||||
<span className="font-mono">{formatDurationCompact(it.trackedSeconds)}</span>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{it.active ? <ActiveLine active={it.active} /> : <IdleLine />}
|
||||
|
||||
<Breakdown byCategory={it.byCategory} total={it.totalSeconds} />
|
||||
<Breakdown byCategory={it.byCategory} total={it.trackedSeconds} />
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user