Anwesenheit: Fallback-Anker erster Zeiteintrag bei Auto-Tagen
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:
1 parent
e049e0a88c
commit
94faf5e41f
4 files changed
+56
-17
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user