Anwesenheitszeit (Lock-in bis Feierabend) als reine Anzeige ergänzen
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:
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)}>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user