Add timer functionality to track time spent on tasks
Introduces a `useTimer` hook to manage starting and stopping timers for tasks, updating their `timeTracked` property, and displaying tracking status. This involves changes in `App.tsx`, `KanbanBoard.tsx`, `TaskCard.tsx`, and `TasksWithCalendar.tsx` to integrate the timer start/stop actions and display. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/P2PZNJ9
This commit is contained in:
@@ -18,10 +18,6 @@ externalPort = 80
|
|||||||
localPort = 35345
|
localPort = 35345
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 40265
|
|
||||||
externalPort = 3003
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41353
|
localPort = 41353
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import ProjectTemplate from './components/ProjectTemplate';
|
|||||||
import ThemeToggle from './components/ThemeToggle';
|
import ThemeToggle from './components/ThemeToggle';
|
||||||
import { Task } from '@shared/schema';
|
import { Task } from '@shared/schema';
|
||||||
import { addDays, subDays } from 'date-fns';
|
import { addDays, subDays } from 'date-fns';
|
||||||
|
import { useTimer } from './hooks/useTimer';
|
||||||
|
|
||||||
// Mock data for prototype
|
// Mock data for prototype
|
||||||
const initialTasks: Task[] = [
|
const initialTasks: Task[] = [
|
||||||
@@ -119,6 +120,12 @@ function App() {
|
|||||||
console.log('Task updated:', taskId, updates);
|
console.log('Task updated:', taskId, updates);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize the timer hook after handleTaskUpdate is defined
|
||||||
|
const { startTimer, stopTimer } = useTimer({
|
||||||
|
tasks,
|
||||||
|
onTaskUpdate: handleTaskUpdate
|
||||||
|
});
|
||||||
|
|
||||||
const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => {
|
const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => {
|
||||||
handleTaskUpdate(taskId, { status: newStatus });
|
handleTaskUpdate(taskId, { status: newStatus });
|
||||||
};
|
};
|
||||||
@@ -156,6 +163,8 @@ function App() {
|
|||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
onTaskUpdate={handleTaskUpdate}
|
onTaskUpdate={handleTaskUpdate}
|
||||||
onTaskEdit={(task) => console.log('Edit task:', task.title)}
|
onTaskEdit={(task) => console.log('Edit task:', task.title)}
|
||||||
|
onStartTimer={startTimer}
|
||||||
|
onStopTimer={stopTimer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,6 +185,8 @@ function App() {
|
|||||||
onTaskStatusChange={handleTaskStatusChange}
|
onTaskStatusChange={handleTaskStatusChange}
|
||||||
onTaskUpdate={handleTaskUpdate}
|
onTaskUpdate={handleTaskUpdate}
|
||||||
onTaskClick={handleTaskClick}
|
onTaskClick={handleTaskClick}
|
||||||
|
onStartTimer={startTimer}
|
||||||
|
onStopTimer={stopTimer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ interface KanbanBoardProps {
|
|||||||
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
||||||
onTaskClick?: (task: Task) => void;
|
onTaskClick?: (task: Task) => void;
|
||||||
onTaskDelete?: (taskId: string) => void;
|
onTaskDelete?: (taskId: string) => void;
|
||||||
|
onStartTimer?: (taskId: string) => void;
|
||||||
|
onStopTimer?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewMode = 'traditional' | 'weekly' | 'monthly';
|
type ViewMode = 'traditional' | 'weekly' | 'monthly';
|
||||||
|
|
||||||
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick, onTaskDelete }: KanbanBoardProps) {
|
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick, onTaskDelete, onStartTimer, onStopTimer }: KanbanBoardProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('traditional');
|
const [viewMode, setViewMode] = useState<ViewMode>('traditional');
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [draggedTask, setDraggedTask] = useState<string | null>(null);
|
const [draggedTask, setDraggedTask] = useState<string | null>(null);
|
||||||
@@ -188,14 +190,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
|||||||
>
|
>
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={task}
|
task={task}
|
||||||
onPlay={() => {
|
onStartTimer={() => onStartTimer?.(task.id)}
|
||||||
onTaskUpdate?.(task.id, { isTracking: true });
|
onStopTimer={() => onStopTimer?.(task.id)}
|
||||||
console.log(`Timer started for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onPause={() => {
|
|
||||||
onTaskUpdate?.(task.id, { isTracking: false });
|
|
||||||
console.log(`Timer paused for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onTaskClick?.(task);
|
onTaskClick?.(task);
|
||||||
console.log(`Task clicked from board: ${task.title}`);
|
console.log(`Task clicked from board: ${task.title}`);
|
||||||
@@ -258,14 +254,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
|||||||
>
|
>
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={task}
|
task={task}
|
||||||
onPlay={() => {
|
onStartTimer={() => onStartTimer?.(task.id)}
|
||||||
onTaskUpdate?.(task.id, { isTracking: true });
|
onStopTimer={() => onStopTimer?.(task.id)}
|
||||||
console.log(`Timer started for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onPause={() => {
|
|
||||||
onTaskUpdate?.(task.id, { isTracking: false });
|
|
||||||
console.log(`Timer paused for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onTaskClick?.(task);
|
onTaskClick?.(task);
|
||||||
console.log(`Task clicked from board: ${task.title}`);
|
console.log(`Task clicked from board: ${task.title}`);
|
||||||
@@ -328,14 +318,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
|||||||
>
|
>
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={task}
|
task={task}
|
||||||
onPlay={() => {
|
onStartTimer={() => onStartTimer?.(task.id)}
|
||||||
onTaskUpdate?.(task.id, { isTracking: true });
|
onStopTimer={() => onStopTimer?.(task.id)}
|
||||||
console.log(`Timer started for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onPause={() => {
|
|
||||||
onTaskUpdate?.(task.id, { isTracking: false });
|
|
||||||
console.log(`Timer paused for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onTaskClick?.(task);
|
onTaskClick?.(task);
|
||||||
console.log(`Task clicked from board: ${task.title}`);
|
console.log(`Task clicked from board: ${task.title}`);
|
||||||
|
|||||||
@@ -9,22 +9,20 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle } from "lucide-react";
|
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
|
||||||
import { Task, Label } from '@shared/schema';
|
import { Task, Label } from '@shared/schema';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
onPlay?: () => void;
|
onStartTimer?: () => void;
|
||||||
onPause?: () => void;
|
onStopTimer?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onStatusChange?: (status: Task['status']) => void;
|
onStatusChange?: (status: Task['status']) => void;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
|
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
|
||||||
const [isTimerRunning, setIsTimerRunning] = useState(task.isTracking);
|
|
||||||
|
|
||||||
// Fetch labels to get the label color
|
// Fetch labels to get the label color
|
||||||
const { data: labels = [] } = useQuery<Label[]>({
|
const { data: labels = [] } = useQuery<Label[]>({
|
||||||
@@ -37,14 +35,13 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
|
|||||||
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
|
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
|
||||||
|
|
||||||
const handleToggleTimer = () => {
|
const handleToggleTimer = () => {
|
||||||
if (isTimerRunning) {
|
if (task.isTracking) {
|
||||||
onPause?.();
|
onStopTimer?.();
|
||||||
console.log(`Timer paused for task: ${task.title}`);
|
console.log(`Timer stopped for task: ${task.title}`);
|
||||||
} else {
|
} else {
|
||||||
onPlay?.();
|
onStartTimer?.();
|
||||||
console.log(`Timer started for task: ${task.title}`);
|
console.log(`Timer started for task: ${task.title}`);
|
||||||
}
|
}
|
||||||
setIsTimerRunning(!isTimerRunning);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
const getPriorityColor = (priority: string) => {
|
||||||
@@ -64,8 +61,14 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (minutes: number) => {
|
const formatTime = (minutes: number) => {
|
||||||
|
if (minutes < 60) {
|
||||||
|
return `${minutes}m`;
|
||||||
|
}
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const mins = minutes % 60;
|
const mins = minutes % 60;
|
||||||
|
if (mins === 0) {
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
return `${hours}h ${mins}m`;
|
return `${hours}h ${mins}m`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,11 +122,15 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{task.timeTracked > 0 && (
|
{(task.timeTracked > 0 || task.isTracking) && (
|
||||||
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-1 mt-2 text-xs">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className={`w-3 h-3 ${task.isTracking ? 'text-primary animate-pulse' : 'text-muted-foreground'}`} />
|
||||||
<span data-testid={`text-time-tracked-${task.id}`}>
|
<span
|
||||||
{formatTime(task.timeTracked)}
|
data-testid={`text-time-tracked-${task.id}`}
|
||||||
|
className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'}
|
||||||
|
>
|
||||||
|
{task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'}
|
||||||
|
{task.isTracking && ' (running)'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -163,7 +170,7 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
|
|||||||
data-testid={`menu-timer-${task.id}`}
|
data-testid={`menu-timer-${task.id}`}
|
||||||
>
|
>
|
||||||
<Timer className="w-4 h-4 mr-2" />
|
<Timer className="w-4 h-4 mr-2" />
|
||||||
{isTimerRunning ? 'Stop Timer' : 'Start Timer'}
|
{task.isTracking ? 'Stop Timer' : 'Start Timer'}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -245,12 +252,12 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant={isTimerRunning ? "default" : "outline"}
|
variant={task.isTracking ? "default" : "outline"}
|
||||||
onClick={handleToggleTimer}
|
onClick={handleToggleTimer}
|
||||||
data-testid={`button-timer-${task.id}`}
|
data-testid={`button-timer-${task.id}`}
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
>
|
>
|
||||||
{isTimerRunning ? <Pause className="w-3 h-3" /> : <Play className="w-3 h-3" />}
|
{task.isTracking ? <Pause className="w-3 h-3" /> : <Play className="w-3 h-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ interface TasksWithCalendarProps {
|
|||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
||||||
onTaskEdit?: (task: Task) => void;
|
onTaskEdit?: (task: Task) => void;
|
||||||
|
onStartTimer?: (taskId: string) => void;
|
||||||
|
onStopTimer?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
|
type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
|
||||||
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
|
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
|
||||||
|
|
||||||
export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: TasksWithCalendarProps) {
|
export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onStartTimer, onStopTimer }: TasksWithCalendarProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
|
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
|
||||||
const [filterBy, setFilterBy] = useState<FilterOption>('all');
|
const [filterBy, setFilterBy] = useState<FilterOption>('all');
|
||||||
@@ -75,14 +77,14 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
|
|||||||
|
|
||||||
case 'priority':
|
case 'priority':
|
||||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
return priorityOrder[b.priority as keyof typeof priorityOrder] - priorityOrder[a.priority as keyof typeof priorityOrder];
|
||||||
|
|
||||||
case 'title':
|
case 'title':
|
||||||
return a.title.localeCompare(b.title);
|
return a.title.localeCompare(b.title);
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
const statusOrder = { todo: 1, inProgress: 2, done: 3 };
|
const statusOrder = { todo: 1, inProgress: 2, done: 3 };
|
||||||
return statusOrder[a.status] - statusOrder[b.status];
|
return statusOrder[a.status as keyof typeof statusOrder] - statusOrder[b.status as keyof typeof statusOrder];
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -273,14 +275,8 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
|
|||||||
>
|
>
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={task}
|
task={task}
|
||||||
onPlay={() => {
|
onStartTimer={() => onStartTimer?.(task.id)}
|
||||||
onTaskUpdate?.(task.id, { isTracking: true, timeTracked: task.timeTracked });
|
onStopTimer={() => onStopTimer?.(task.id)}
|
||||||
console.log(`Timer started for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onPause={() => {
|
|
||||||
onTaskUpdate?.(task.id, { isTracking: false });
|
|
||||||
console.log(`Timer paused for ${task.title}`);
|
|
||||||
}}
|
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onTaskEdit?.(task);
|
onTaskEdit?.(task);
|
||||||
console.log(`Edit task ${task.title}`);
|
console.log(`Edit task ${task.title}`);
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Task } from '@shared/schema';
|
||||||
|
|
||||||
|
interface UseTimerProps {
|
||||||
|
tasks: Task[];
|
||||||
|
onTaskUpdate: (taskId: string, updates: Partial<Task>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimer({ tasks, onTaskUpdate }: UseTimerProps) {
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastUpdateRef = useRef<{ [taskId: string]: number }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Find tasks that are currently being tracked
|
||||||
|
const trackingTasks = tasks.filter(task => task.isTracking);
|
||||||
|
|
||||||
|
if (trackingTasks.length > 0) {
|
||||||
|
// Start the timer
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
trackingTasks.forEach(task => {
|
||||||
|
// Initialize last update time if not set
|
||||||
|
if (!lastUpdateRef.current[task.id]) {
|
||||||
|
lastUpdateRef.current[task.id] = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate elapsed time since last update (in minutes)
|
||||||
|
const elapsed = (now - lastUpdateRef.current[task.id]) / 1000 / 60;
|
||||||
|
|
||||||
|
// Only update if at least 1 minute has passed (to avoid too frequent updates)
|
||||||
|
if (elapsed >= 1) {
|
||||||
|
const currentTime = task.timeTracked || 0;
|
||||||
|
const newTime = currentTime + Math.floor(elapsed);
|
||||||
|
|
||||||
|
onTaskUpdate(task.id, { timeTracked: newTime });
|
||||||
|
lastUpdateRef.current[task.id] = now;
|
||||||
|
|
||||||
|
console.log(`Timer updated for "${task.title}": +${Math.floor(elapsed)}min (total: ${newTime}min)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 60000); // Update every minute
|
||||||
|
} else {
|
||||||
|
// No tasks are being tracked, clear the timer
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
// Clear last update times for stopped tasks
|
||||||
|
lastUpdateRef.current = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount or when tracking tasks change
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [tasks.map(t => `${t.id}:${t.isTracking}`).join(','), onTaskUpdate]);
|
||||||
|
|
||||||
|
// Initialize last update times for currently tracking tasks
|
||||||
|
useEffect(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
tasks.forEach(task => {
|
||||||
|
if (task.isTracking && !lastUpdateRef.current[task.id]) {
|
||||||
|
lastUpdateRef.current[task.id] = now;
|
||||||
|
} else if (!task.isTracking && lastUpdateRef.current[task.id]) {
|
||||||
|
delete lastUpdateRef.current[task.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [tasks]);
|
||||||
|
|
||||||
|
const startTimer = (taskId: string) => {
|
||||||
|
const now = Date.now();
|
||||||
|
lastUpdateRef.current[taskId] = now;
|
||||||
|
onTaskUpdate(taskId, { isTracking: true });
|
||||||
|
console.log(`Timer started for task: ${taskId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTimer = (taskId: string) => {
|
||||||
|
const task = tasks.find(t => t.id === taskId);
|
||||||
|
if (task && lastUpdateRef.current[taskId]) {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = (now - lastUpdateRef.current[taskId]) / 1000 / 60;
|
||||||
|
|
||||||
|
if (elapsed >= 0.1) { // Add time if at least 6 seconds have passed
|
||||||
|
const currentTime = task.timeTracked || 0;
|
||||||
|
const additionalTime = Math.max(1, Math.round(elapsed)); // At least 1 minute
|
||||||
|
const newTime = currentTime + additionalTime;
|
||||||
|
|
||||||
|
onTaskUpdate(taskId, {
|
||||||
|
isTracking: false,
|
||||||
|
timeTracked: newTime
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Timer stopped for "${task.title}": +${additionalTime}min (total: ${newTime}min)`);
|
||||||
|
} else {
|
||||||
|
onTaskUpdate(taskId, { isTracking: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
delete lastUpdateRef.current[taskId];
|
||||||
|
} else {
|
||||||
|
onTaskUpdate(taskId, { isTracking: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTimer,
|
||||||
|
stopTimer
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user