Anwesenheit: Fallback-Anker erster Zeiteintrag bei Auto-Tagen
ci / quality (push) Failing after 38s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 1m1s

Auto-Tage vom 6-Uhr-Schnitt haben keinen echten Lock-in — statt die
Anwesenheit ganz auszublenden, ankert sie jetzt am ersten Zeiteintrag
des Tages. teamToday/listToday liefern presenceStartedAt/presenceEndedAt
(echter Check-in hat Vorrang); Team-Karte, Heute-Balken und "Zeit heute"
nutzen die neuen Felder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Fable 5 committed 2026-06-12 12:20:18 +02:00
1 parent e049e0a88c
commit 94faf5e41f
4 files changed
+56 -17

No files matched your search

+36 -6
View File
@@ -180,11 +180,36 @@ export const timeRouter = router({
0,
);
// Anwesenheits-Anker wie in teamToday: echter Lock-in, sonst (Auto-Tag vom
// 6-Uhr-Schnitt oder kein Eintrag) der erste Zeiteintrag des Tages.
const ownDay = (
await ctx.db
.select({
checkedInAt: dayEntry.checkedInAt,
checkedOutAt: dayEntry.checkedOutAt,
autoCheckedIn: dayEntry.autoCheckedIn,
})
.from(dayEntry)
.where(and(eq(dayEntry.userId, ctx.user.id), eq(dayEntry.date, today)))
.limit(1)
)[0];
const firstStartMs = rows.length ? Math.min(...rows.map((r) => clampStart(r))) : null;
const anchorMs =
ownDay && !ownDay.autoCheckedIn && ownDay.checkedInAt
? ownDay.checkedInAt.getTime()
: firstStartMs;
const entries = rows.map((r) => ({
...r,
todoTitle: r.todoId ? (titles.get(r.todoId) ?? null) : null,
}));
return { entries, totalSeconds, trackedSeconds };
return {
entries,
totalSeconds,
trackedSeconds,
presenceStartedAt: anchorMs !== null ? new Date(anchorMs).toISOString() : null,
presenceEndedAt: ownDay?.checkedOutAt ? ownDay.checkedOutAt.toISOString() : null,
};
}),
/**
@@ -342,29 +367,34 @@ export const timeRouter = router({
}
// Anwesenheit (Lock-in → Feierabend) pro Person — reine Anzeige neben dem
// Tracking. Vom 6-Uhr-Schnitt auto-angelegte Tage zählen nicht: deren
// Check-in (06:00) ist kein echter Lock-in.
// Tracking. Anker = echter Lock-in; bei vom 6-Uhr-Schnitt auto-angelegten
// Tagen (Check-in 06:00 ist kein echter Lock-in) stattdessen der erste
// Zeiteintrag des Tages.
const dayRows = await ctx.db
.select({
userId: dayEntry.userId,
checkedInAt: dayEntry.checkedInAt,
checkedOutAt: dayEntry.checkedOutAt,
autoCheckedIn: dayEntry.autoCheckedIn,
})
.from(dayEntry)
.where(and(eq(dayEntry.date, today), eq(dayEntry.autoCheckedIn, false)));
.where(eq(dayEntry.date, today));
const presenceByUser = new Map(dayRows.map((d) => [d.userId, d]));
// „Gesamt" pro Person = Wall-Clock (Vereinigung der Intervalle).
return Array.from(perUser.values()).map((b) => {
const p = presenceByUser.get(b.userId);
const firstStartMs = b.intervals.length ? Math.min(...b.intervals.map((i) => i[0])) : null;
const anchorMs =
p && !p.autoCheckedIn && p.checkedInAt ? p.checkedInAt.getTime() : firstStartMs;
return {
userId: b.userId,
totalSeconds: unionSeconds(b.intervals),
trackedSeconds: b.trackedSeconds,
byCategory: b.byCategory,
active: b.active,
checkedInAt: p?.checkedInAt ? p.checkedInAt.toISOString() : null,
checkedOutAt: p?.checkedOutAt ? p.checkedOutAt.toISOString() : null,
presenceStartedAt: anchorMs !== null ? new Date(anchorMs).toISOString() : null,
presenceEndedAt: p?.checkedOutAt ? p.checkedOutAt.toISOString() : null,
};
});
}),