feat: include ongoing timer in today's tracked time on Dashboard

The 'Today' stat card now adds the running timer's elapsed seconds to
the total, so the displayed duration ticks up live alongside the timer
widget. The timer is only counted when it started today (timers carried
over from the previous day are excluded).

A pulsing green indicator dot is shown on the stat card value while the
timer is active, consistent with the balance widget treatment. The dot
is implemented via a new optional 'indicator' prop on StatCard so it
can be reused elsewhere without changing existing call sites.
This commit is contained in:
2026-03-09 11:14:21 +01:00
parent 7ec76e3e8e
commit c9bd0abf18
2 changed files with 26 additions and 6 deletions

View File

@@ -3,25 +3,35 @@ interface StatCardProps {
label: string; label: string;
value: string; value: string;
color: 'blue' | 'green' | 'purple' | 'orange'; color: 'blue' | 'green' | 'purple' | 'orange';
/** When true, renders a pulsing green dot to signal a live/active state. */
indicator?: boolean;
} }
const colorClasses: Record<StatCardProps['color'], string> = { const colorClasses: Record<NonNullable<StatCardProps['color']>, string> = {
blue: 'bg-blue-50 text-blue-600', blue: 'bg-blue-50 text-blue-600',
green: 'bg-green-50 text-green-600', green: 'bg-green-50 text-green-600',
purple: 'bg-purple-50 text-purple-600', purple: 'bg-purple-50 text-purple-600',
orange: 'bg-orange-50 text-orange-600', orange: 'bg-orange-50 text-orange-600',
}; };
export function StatCard({ icon: Icon, label, value, color }: StatCardProps) { export function StatCard({ icon: Icon, label, value, color, indicator }: StatCardProps) {
return ( return (
<div className="card p-4"> <div className="card p-4">
<div className="flex items-center"> <div className="flex items-center">
<div className={`p-3 rounded-lg ${colorClasses[color]}`}> <div className={`p-3 rounded-lg ${colorClasses[color]}`}>
<Icon className="h-6 w-6" /> <Icon className="h-6 w-6" />
</div> </div>
<div className="ml-4"> <div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-600">{label}</p> <p className="text-sm font-medium text-gray-600">{label}</p>
<p className="text-2xl font-bold text-gray-900">{value}</p> <div className="flex items-center gap-2">
<p className="text-2xl font-bold text-gray-900">{value}</p>
{indicator && (
<span
className="inline-block h-2.5 w-2.5 rounded-full bg-green-500 animate-pulse"
title="Timer running"
/>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import { Clock, Calendar, Briefcase, TrendingUp, Target, Edit2, Trash2 } from "lucide-react"; import { Clock, Calendar, Briefcase, TrendingUp, Target, Edit2, Trash2 } from "lucide-react";
import { useTimeEntries } from "@/hooks/useTimeEntries"; import { useTimeEntries } from "@/hooks/useTimeEntries";
import { useClientTargets } from "@/hooks/useClientTargets"; import { useClientTargets } from "@/hooks/useClientTargets";
import { useTimer } from "@/contexts/TimerContext";
import { ProjectColorDot } from "@/components/ProjectColorDot"; import { ProjectColorDot } from "@/components/ProjectColorDot";
import { StatCard } from "@/components/StatCard"; import { StatCard } from "@/components/StatCard";
import { TimeEntryFormModal } from "@/components/TimeEntryFormModal"; import { TimeEntryFormModal } from "@/components/TimeEntryFormModal";
@@ -30,6 +31,7 @@ export function DashboardPage() {
}); });
const { targets } = useClientTargets(); const { targets } = useClientTargets();
const { ongoingTimer, elapsedSeconds } = useTimer();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEntry, setEditingEntry] = useState<TimeEntry | null>(null); const [editingEntry, setEditingEntry] = useState<TimeEntry | null>(null);
@@ -54,10 +56,17 @@ export function DashboardPage() {
} }
}; };
const totalTodaySeconds = const completedTodaySeconds =
todayEntries?.entries.reduce((total, entry) => { todayEntries?.entries.reduce((total, entry) => {
return total + calculateDuration(entry.startTime, entry.endTime, entry.breakMinutes); return total + calculateDuration(entry.startTime, entry.endTime, entry.breakMinutes);
}, 0) || 0; }, 0) ?? 0;
// Only add the running timer if it started today (not a timer left running from yesterday)
const timerStartedToday =
ongoingTimer !== null &&
new Date(ongoingTimer.startTime) >= startOfDay(today);
const totalTodaySeconds = completedTodaySeconds + (timerStartedToday ? elapsedSeconds : 0);
const targetsWithData = targets?.filter(t => t.periods.length > 0) ?? []; const targetsWithData = targets?.filter(t => t.periods.length > 0) ?? [];
@@ -78,6 +87,7 @@ export function DashboardPage() {
label="Today" label="Today"
value={formatDurationHoursMinutes(totalTodaySeconds)} value={formatDurationHoursMinutes(totalTodaySeconds)}
color="blue" color="blue"
indicator={timerStartedToday}
/> />
<StatCard <StatCard
icon={Calendar} icon={Calendar}