Anwesenheitszeit (Lock-in bis Feierabend) als reine Anzeige ergänzen
ci / quality (push) Failing after 36s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 1m19s

Neben der getrackten Task-Zeit zeigt der "Zeit heute"-Header jetzt
"Getrackt · Anwesend"; der Check-In/Out-Balken zeigt die Anwesenheit
live ("anwesend seit …") und nach Check-Out die finale Dauer. Kein
Eingriff ins Task-Tracking — Wert wird nur aus checkedInAt/checkedOutAt
berechnet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Fable 5 committed 2026-06-11 19:24:56 +02:00
1 parent a1cf6f3883
commit 687325ad0d
3 files changed
+54 -19

No files matched your search

@@ -7,6 +7,7 @@ import { Card } from '../ui/Card';
import { Button } from '../ui/Button';
import { Modal } from '../ui/Modal';
import { formatDuration, formatDurationCompact } from '../../lib/format-duration';
import { usePresenceSeconds } from '../../lib/use-presence-seconds';
import { TIME_CATEGORIES, type TimeCategory } from '../../lib/time-categories';
const CATEGORY_LABEL: Record<string, string> = Object.fromEntries(
@@ -30,6 +31,10 @@ export function TimelineSection() {
const todayQuery = trpc.dayEntry.today.useQuery();
const [manualOpen, setManualOpen] = useState(false);
const [editing, setEditing] = useState<string | null>(null);
const presenceSeconds = usePresenceSeconds(
todayQuery.data?.entry?.checkedInAt,
todayQuery.data?.entry?.checkedOutAt,
);
if (listQuery.isPending) return null;
// Aufsteigend sortieren: Vormittag oben, Abend unten.
@@ -45,7 +50,13 @@ export function TimelineSection() {
<div>
<h2 className="text-lg font-semibold">Zeit heute</h2>
<p className="text-xs text-[var(--color-ink-muted)]">
Gesamt: <span className="font-mono">{formatDurationCompact(totalSeconds)}</span>
Getrackt: <span className="font-mono">{formatDurationCompact(totalSeconds)}</span>
{presenceSeconds !== null && (
<>
{' · '}Anwesend:{' '}
<span className="font-mono">{formatDurationCompact(presenceSeconds)}</span>
</>
)}
</p>
</div>
<Button size="sm" variant="secondary" onClick={() => setManualOpen(true)}>
+22
View File
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
/**
* Anwesenheits-Sekunden von Lock-in bis Feierabend — bzw. bis jetzt, solange
* noch kein Check-Out vorliegt. Reine Anzeige, unabhängig vom Task-Tracking.
* Tickt minütlich, solange der Tag noch läuft.
*/
export function usePresenceSeconds(
checkedInAt: string | Date | null | undefined,
checkedOutAt: string | Date | null | undefined,
): number | null {
const running = Boolean(checkedInAt) && !checkedOutAt;
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
if (!running) return;
const id = setInterval(() => setNowMs(Date.now()), 60_000);
return () => clearInterval(id);
}, [running]);
if (!checkedInAt) return null;
const endMs = checkedOutAt ? new Date(checkedOutAt).getTime() : nowMs;
return Math.max(0, Math.round((endMs - new Date(checkedInAt).getTime()) / 1000));
}
+20 -18
View File
@@ -10,6 +10,7 @@ import { toast } from 'sonner';
import { X, Plus, RotateCw, BellRing, Play, Clock, Check, LogOut } from 'lucide-react';
import { TimelineSection, ManualDialog } from '../components/time/TimelineSection';
import { formatDurationCompact } from '../lib/format-duration';
import { usePresenceSeconds } from '../lib/use-presence-seconds';
/** Aktuelle Stunde in Europe/Berlin (0–23). */
function berlinHourNow(): number {
@@ -353,15 +354,8 @@ function CheckInOutBar({
minute: '2-digit',
}).format(new Date(d))
: '—';
const duration =
checkedInAt && checkedOutAt
? formatDurationCompact(
Math.max(
0,
Math.round((new Date(checkedOutAt).getTime() - new Date(checkedInAt).getTime()) / 1000),
),
)
: null;
// Anwesenheit (Lock-in → Feierabend bzw. jetzt) — reine Anzeige, kein Tracking.
const presenceSeconds = usePresenceSeconds(checkedInAt, checkedOutAt);
return (
<>
@@ -373,7 +367,7 @@ function CheckInOutBar({
<>
<span className="text-[var(--color-ink-muted)]">
· Feierabend <span className="font-mono">{hhmm(checkedOutAt)}</span>
{duration && ` · ${duration}`}
{presenceSeconds !== null && ` · ${formatDurationCompact(presenceSeconds)} anwesend`}
</span>
<Button
size="sm"
@@ -386,14 +380,22 @@ function CheckInOutBar({
</Button>
</>
) : (
<Button
size="sm"
variant="secondary"
className="ml-auto"
onClick={() => setConfirmOpen(true)}
>
<LogOut size={14} /> Feierabend / Check-Out
</Button>
<>
{presenceSeconds !== null && (
<span className="text-[var(--color-ink-muted)]">
· anwesend seit{' '}
<span className="font-mono">{formatDurationCompact(presenceSeconds)}</span>
</span>
)}
<Button
size="sm"
variant="secondary"
className="ml-auto"
onClick={() => setConfirmOpen(true)}
>
<LogOut size={14} /> Feierabend / Check-Out
</Button>
</>
)}
</div>
<FeierabendDialog