diff --git a/apps/api/src/routers/time.ts b/apps/api/src/routers/time.ts index 814ccac..d45f3b5 100644 --- a/apps/api/src/routers/time.ts +++ b/apps/api/src/routers/time.ts @@ -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, }; }); }), diff --git a/apps/web/src/components/time/TeamTimeSection.tsx b/apps/web/src/components/time/TeamTimeSection.tsx index 15717de..d5b7ec4 100644 --- a/apps/web/src/components/time/TeamTimeSection.tsx +++ b/apps/web/src/components/time/TeamTimeSection.tsx @@ -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() { )} {presenceSecondsOf(it) !== null && ( - + {' · Anwesend: '} {formatDurationCompact(presenceSecondsOf(it)!)} diff --git a/apps/web/src/components/time/TimelineSection.tsx b/apps/web/src/components/time/TimelineSection.tsx index b072e05..cd30cbf 100644 --- a/apps/web/src/components/time/TimelineSection.tsx +++ b/apps/web/src/components/time/TimelineSection.tsx @@ -31,11 +31,16 @@ export function TimelineSection() { const todayQuery = trpc.dayEntry.today.useQuery(); const [manualOpen, setManualOpen] = useState(false); const [editing, setEditing] = useState(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; diff --git a/apps/web/src/routes/heute.tsx b/apps/web/src/routes/heute.tsx index 64f17ec..9e90b2a 100644 --- a/apps/web/src/routes/heute.tsx +++ b/apps/web/src/routes/heute.tsx @@ -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 ( <>