feat: include ongoing timer in balance calculation
The balance now accounts for any active timer whose project belongs to the tracked client. computeBalance() fetches the user's OngoingTimer, computes its elapsed seconds, and adds them to the matching period's tracked seconds before running the balance formula — so both currentPeriodTrackedSeconds and totalBalanceSeconds reflect the live timer without requiring a schema change. On the frontend, useClientTargets polls every 30 s while a timer is running, and a pulsing green dot is shown next to the balance figure on the Dashboard and Clients pages to signal the live contribution.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { clientTargetsApi } from '@/api/clientTargets';
|
||||
import { useTimer } from '@/contexts/TimerContext';
|
||||
import type {
|
||||
CreateClientTargetInput,
|
||||
UpdateClientTargetInput,
|
||||
@@ -8,10 +9,13 @@ import type {
|
||||
|
||||
export function useClientTargets() {
|
||||
const queryClient = useQueryClient();
|
||||
const { ongoingTimer } = useTimer();
|
||||
|
||||
const { data: targets, isLoading, error } = useQuery({
|
||||
queryKey: ['clientTargets'],
|
||||
queryFn: clientTargetsApi.getAll,
|
||||
// Poll every 30 s while a timer is running so the balance stays current
|
||||
refetchInterval: ongoingTimer ? 30_000 : false,
|
||||
});
|
||||
|
||||
const createTarget = useMutation({
|
||||
|
||||
@@ -316,6 +316,12 @@ function ClientTargetPanel({
|
||||
<span className="font-medium">{target!.targetHours}h</span>/{periodLabel}
|
||||
</span>
|
||||
<span className={`text-xs font-semibold ${balance.color}`}>{balance.text}</span>
|
||||
{target!.hasOngoingTimer && (
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse"
|
||||
title="Timer running — balance updates every 30 s"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
|
||||
@@ -132,19 +132,27 @@ export function DashboardPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p
|
||||
className={`text-sm font-bold ${
|
||||
isEven
|
||||
? 'text-gray-500'
|
||||
: isOver
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{isEven
|
||||
? '±0'
|
||||
: (isOver ? '+' : '−') + formatDurationHoursMinutes(absBalance)}
|
||||
</p>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
{target.hasOngoingTimer && (
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse"
|
||||
title="Timer running — balance updates every 30 s"
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
className={`text-sm font-bold ${
|
||||
isEven
|
||||
? 'text-gray-500'
|
||||
: isOver
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{isEven
|
||||
? '±0'
|
||||
: (isOver ? '+' : '−') + formatDurationHoursMinutes(absBalance)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">running balance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,6 +186,8 @@ export interface ClientTargetWithBalance {
|
||||
currentPeriodTrackedSeconds: number;
|
||||
currentPeriodTargetSeconds: number;
|
||||
periods: PeriodBalance[];
|
||||
/** True when an active timer for a project belonging to this client is running. */
|
||||
hasOngoingTimer: boolean;
|
||||
}
|
||||
|
||||
export interface CreateClientTargetInput {
|
||||
|
||||
Reference in New Issue
Block a user