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:
@@ -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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
<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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user