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,
};
});
}),
@@ -30,12 +30,12 @@ function initials(name: string) {
/** Anwesenheit (Lock-in → Feierabend bzw. jetzt) — reine Anzeige, kein Tracking. */
function presenceSecondsOf(it: {
checkedInAt: string | null;
checkedOutAt: string | null;
presenceStartedAt: string | null;
presenceEndedAt: string | null;
}): number | null {
if (!it.checkedInAt) return null;
const endMs = it.checkedOutAt ? new Date(it.checkedOutAt).getTime() : Date.now();
return Math.max(0, Math.round((endMs - new Date(it.checkedInAt).getTime()) / 1000));
if (!it.presenceStartedAt) return null;
const endMs = it.presenceEndedAt ? new Date(it.presenceEndedAt).getTime() : Date.now();
return Math.max(0, Math.round((endMs - new Date(it.presenceStartedAt).getTime()) / 1000));
}
export function TeamTimeSection() {
@@ -101,7 +101,7 @@ export function TeamTimeSection() {
</span>
)}
{presenceSecondsOf(it) !== null && (
<span title="Anwesenheit: Lock-in bis Feierabend (bzw. jetzt) — unabhängig vom Tracking.">
<span title="Anwesenheit: ab Lock-in (bzw. erstem Zeiteintrag des Tages) bis Feierabend bzw. jetzt — unabhängig vom Tracking.">
{' · Anwesend: '}
<span className="font-mono">
{formatDurationCompact(presenceSecondsOf(it)!)}
@@ -31,11 +31,16 @@ export function TimelineSection() {
const todayQuery = trpc.dayEntry.today.useQuery();
const [manualOpen, setManualOpen] = useState(false);
const [editing, setEditing] = useState<string | null>(null);
// Auto-Check-in vom 6-Uhr-Schnitt ist kein echter Lock-in → keine Anwesenheit.
// Anwesenheits-Anker: echter Lock-in; bei Auto-Tagen (6-Uhr-Schnitt) liefert
// listToday den ersten Zeiteintrag des Tages als Fallback.
const todayEntry = todayQuery.data?.entry;
const presenceFrom =
todayEntry && !todayEntry.autoCheckedIn && todayEntry.checkedInAt
? todayEntry.checkedInAt
: (listQuery.data?.presenceStartedAt ?? null);
const presenceSeconds = usePresenceSeconds(
todayEntry?.autoCheckedIn ? null : todayEntry?.checkedInAt,
todayEntry?.checkedOutAt,
presenceFrom,
todayEntry?.checkedOutAt ?? listQuery.data?.presenceEndedAt ?? null,
);
if (listQuery.isPending) return null;
+6 -2
View File
@@ -357,8 +357,12 @@ function CheckInOutBar({
}).format(new Date(d))
: '—';
// Anwesenheit (Lock-in → Feierabend bzw. jetzt) — reine Anzeige, kein Tracking.
// Auto-Check-in vom 6-Uhr-Schnitt ist kein echter Lock-in → keine Anwesenheit.
const presenceSeconds = usePresenceSeconds(autoCheckedIn ? null : checkedInAt, checkedOutAt);
// Auto-Check-in vom 6-Uhr-Schnitt ist kein echter Lock-in → Fallback-Anker
// (erster Zeiteintrag des Tages) aus listToday.
const listToday = trpc.time.listToday.useQuery(undefined, { refetchInterval: 30_000 });
const presenceFrom =
!autoCheckedIn && checkedInAt ? checkedInAt : (listToday.data?.presenceStartedAt ?? null);
const presenceSeconds = usePresenceSeconds(presenceFrom, checkedOutAt);
return (
<>