Anwesenheit ignoriert Auto-Check-in vom 6-Uhr-Schnitt
Der 6-Uhr-Schnitt legt Folgetage mit checkedInAt=06:00 an — das ist kein echter Lock-in, die Anwesenheitsanzeige lief dadurch fälschlich seit 06:00 mit (z. B. "6h 6m" mittags trotz spätem Arbeitsbeginn). Neues Flag day_entry.auto_checked_in (Backfill: bestehende 06:00:00-Check-ins); teamToday, Heute-Balken und "Zeit heute" zeigen Anwesenheit nur noch bei echtem Check-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0fbc386fa
commit
e049e0a88c
8 files changed
+3394
-7
No files matched your search
@@ -62,7 +62,14 @@ export async function applyWorkdayBoundary(db: Db, userId: string, now: Date): P
|
||||
|
||||
const [created] = await tx
|
||||
.insert(dayEntry)
|
||||
.values({ userId, date: nextDate, status: 'begun', checkedInAt: cutoff })
|
||||
.values({
|
||||
userId,
|
||||
date: nextDate,
|
||||
status: 'begun',
|
||||
checkedInAt: cutoff,
|
||||
// Kein echter Lock-in — Anwesenheits-Anzeigen ignorieren diesen Check-in.
|
||||
autoCheckedIn: true,
|
||||
})
|
||||
.returning();
|
||||
if (!created) throw new Error('Folge-Tag konnte nicht angelegt werden.');
|
||||
|
||||
|
||||
@@ -341,7 +341,9 @@ export const timeRouter = router({
|
||||
perUser.set(r.userId, bucket);
|
||||
}
|
||||
|
||||
// Anwesenheit (Lock-in → Feierabend) pro Person — reine Anzeige neben dem Tracking.
|
||||
// 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.
|
||||
const dayRows = await ctx.db
|
||||
.select({
|
||||
userId: dayEntry.userId,
|
||||
@@ -349,7 +351,7 @@ export const timeRouter = router({
|
||||
checkedOutAt: dayEntry.checkedOutAt,
|
||||
})
|
||||
.from(dayEntry)
|
||||
.where(eq(dayEntry.date, today));
|
||||
.where(and(eq(dayEntry.date, today), eq(dayEntry.autoCheckedIn, false)));
|
||||
const presenceByUser = new Map(dayRows.map((d) => [d.userId, d]));
|
||||
|
||||
// „Gesamt" pro Person = Wall-Clock (Vereinigung der Intervalle).
|
||||
|
||||
@@ -31,9 +31,11 @@ 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.
|
||||
const todayEntry = todayQuery.data?.entry;
|
||||
const presenceSeconds = usePresenceSeconds(
|
||||
todayQuery.data?.entry?.checkedInAt,
|
||||
todayQuery.data?.entry?.checkedOutAt,
|
||||
todayEntry?.autoCheckedIn ? null : todayEntry?.checkedInAt,
|
||||
todayEntry?.checkedOutAt,
|
||||
);
|
||||
|
||||
if (listQuery.isPending) return null;
|
||||
|
||||
@@ -320,11 +320,13 @@ function AddTodoInline() {
|
||||
function CheckInOutBar({
|
||||
checkedInAt,
|
||||
checkedOutAt,
|
||||
autoCheckedIn,
|
||||
todos,
|
||||
reflected,
|
||||
}: {
|
||||
checkedInAt: string | Date | null;
|
||||
checkedOutAt: string | Date | null;
|
||||
autoCheckedIn: boolean;
|
||||
todos: Array<{ id: string; title: string; status: 'open' | 'done' | 'dropped' }>;
|
||||
reflected: boolean;
|
||||
}) {
|
||||
@@ -355,7 +357,8 @@ function CheckInOutBar({
|
||||
}).format(new Date(d))
|
||||
: '—';
|
||||
// Anwesenheit (Lock-in → Feierabend bzw. jetzt) — reine Anzeige, kein Tracking.
|
||||
const presenceSeconds = usePresenceSeconds(checkedInAt, checkedOutAt);
|
||||
// Auto-Check-in vom 6-Uhr-Schnitt ist kein echter Lock-in → keine Anwesenheit.
|
||||
const presenceSeconds = usePresenceSeconds(autoCheckedIn ? null : checkedInAt, checkedOutAt);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -555,6 +558,7 @@ function EinstempeltView({
|
||||
status: 'begun' | 'full' | 'absent';
|
||||
checkedInAt: string | Date | null;
|
||||
checkedOutAt: string | Date | null;
|
||||
autoCheckedIn: boolean;
|
||||
};
|
||||
}) {
|
||||
// Reflexion ist „fällig", wenn nach 18:00 Berlin und noch nicht abgeschlossen.
|
||||
@@ -576,6 +580,7 @@ function EinstempeltView({
|
||||
<CheckInOutBar
|
||||
checkedInAt={entry.checkedInAt}
|
||||
checkedOutAt={entry.checkedOutAt}
|
||||
autoCheckedIn={entry.autoCheckedIn}
|
||||
todos={todos}
|
||||
reflected={entry.status === 'full'}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "day_entry" ADD COLUMN "auto_checked_in" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
UPDATE "day_entry" SET "auto_checked_in" = true
|
||||
WHERE "checked_in_at" IS NOT NULL
|
||||
AND ("checked_in_at" AT TIME ZONE 'Europe/Berlin')::time = TIME '06:00:00';
|
||||
File diff suppressed because it is too large.
Load diff
@@ -134,6 +134,13 @@
|
||||
"when": 1780828837324,
|
||||
"tag": "0018_last_lake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1781259091924,
|
||||
"tag": "0019_shocking_starbolt",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,9 @@ export const dayEntry = pgTable(
|
||||
date: date('date').notNull(), // YYYY-MM-DD im Europe/Berlin
|
||||
status: text('status', { enum: ['begun', 'full', 'absent'] }).notNull(),
|
||||
checkedInAt: timestamp('checked_in_at', { withTimezone: true }),
|
||||
// True, wenn der Eintrag vom 6-Uhr-Schnitt automatisch angelegt wurde (kein
|
||||
// echter Lock-in um 06:00) — Anwesenheits-Anzeigen ignorieren solche Check-ins.
|
||||
autoCheckedIn: boolean('auto_checked_in').notNull().default(false),
|
||||
// Arbeitsende (Check-Out) — markiert das Ende der Arbeits-Session des Tages.
|
||||
checkedOutAt: timestamp('checked_out_at', { withTimezone: true }),
|
||||
// Kimai-Sync: ID des bei Check-In gestarteten Timesheets + Sync-Status.
|
||||
|
||||
Reference in new issue
Block a user