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:
paul-nothaft
2025-09-11 22:27:05 +00:00
parent 4b96a8c939
commit 95f2b1d4a1
6 changed files with 165 additions and 58 deletions
+9 -25
View File
@@ -15,11 +15,13 @@ interface KanbanBoardProps {
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskClick?: (task: Task) => void;
onTaskDelete?: (taskId: string) => void;
onStartTimer?: (taskId: string) => void;
onStopTimer?: (taskId: string) => void;
}
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 [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null);
@@ -188,14 +190,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
>
<TaskCard
task={task}
onPlay={() => {
onTaskUpdate?.(task.id, { isTracking: true });
console.log(`Timer started for ${task.title}`);
}}
onPause={() => {
onTaskUpdate?.(task.id, { isTracking: false });
console.log(`Timer paused for ${task.title}`);
}}
onStartTimer={() => onStartTimer?.(task.id)}
onStopTimer={() => onStopTimer?.(task.id)}
onEdit={() => {
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
@@ -258,14 +254,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
>
<TaskCard
task={task}
onPlay={() => {
onTaskUpdate?.(task.id, { isTracking: true });
console.log(`Timer started for ${task.title}`);
}}
onPause={() => {
onTaskUpdate?.(task.id, { isTracking: false });
console.log(`Timer paused for ${task.title}`);
}}
onStartTimer={() => onStartTimer?.(task.id)}
onStopTimer={() => onStopTimer?.(task.id)}
onEdit={() => {
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
@@ -328,14 +318,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
>
<TaskCard
task={task}
onPlay={() => {
onTaskUpdate?.(task.id, { isTracking: true });
console.log(`Timer started for ${task.title}`);
}}
onPause={() => {
onTaskUpdate?.(task.id, { isTracking: false });
console.log(`Timer paused for ${task.title}`);
}}
onStartTimer={() => onStartTimer?.(task.id)}
onStopTimer={() => onStopTimer?.(task.id)}
onEdit={() => {
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
+25 -18
View File
@@ -9,22 +9,20 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle } from "lucide-react";
import { useState } from "react";
import { Task, Label } from '@shared/schema';
import { useQuery } from '@tanstack/react-query';
interface TaskCardProps {
task: Task;
onPlay?: () => void;
onPause?: () => void;
onStartTimer?: () => void;
onStopTimer?: () => void;
onEdit?: () => void;
onDelete?: () => void;
onStatusChange?: (status: Task['status']) => void;
isDragging?: boolean;
}
export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
const [isTimerRunning, setIsTimerRunning] = useState(task.isTracking);
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
// Fetch labels to get the label color
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 handleToggleTimer = () => {
if (isTimerRunning) {
onPause?.();
console.log(`Timer paused for task: ${task.title}`);
if (task.isTracking) {
onStopTimer?.();
console.log(`Timer stopped for task: ${task.title}`);
} else {
onPlay?.();
onStartTimer?.();
console.log(`Timer started for task: ${task.title}`);
}
setIsTimerRunning(!isTimerRunning);
};
const getPriorityColor = (priority: string) => {
@@ -64,8 +61,14 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
};
const formatTime = (minutes: number) => {
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (mins === 0) {
return `${hours}h`;
}
return `${hours}h ${mins}m`;
};
@@ -119,11 +122,15 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
)}
</div>
{task.timeTracked > 0 && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<Clock className="w-3 h-3" />
<span data-testid={`text-time-tracked-${task.id}`}>
{formatTime(task.timeTracked)}
{(task.timeTracked > 0 || task.isTracking) && (
<div className="flex items-center gap-1 mt-2 text-xs">
<Clock className={`w-3 h-3 ${task.isTracking ? 'text-primary animate-pulse' : 'text-muted-foreground'}`} />
<span
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>
</div>
)}
@@ -163,7 +170,7 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
data-testid={`menu-timer-${task.id}`}
>
<Timer className="w-4 h-4 mr-2" />
{isTimerRunning ? 'Stop Timer' : 'Start Timer'}
{task.isTracking ? 'Stop Timer' : 'Start Timer'}
</DropdownMenuItem>
<DropdownMenuSeparator />
@@ -245,12 +252,12 @@ export default function TaskCard({ task, onPlay, onPause, onEdit, onDelete, onSt
<Button
size="icon"
variant={isTimerRunning ? "default" : "outline"}
variant={task.isTracking ? "default" : "outline"}
onClick={handleToggleTimer}
data-testid={`button-timer-${task.id}`}
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>
</div>
</div>
+7 -11
View File
@@ -15,12 +15,14 @@ interface TasksWithCalendarProps {
tasks: Task[];
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskEdit?: (task: Task) => void;
onStartTimer?: (taskId: string) => void;
onStopTimer?: (taskId: string) => void;
}
type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
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 [sortBy, setSortBy] = useState<SortOption>('dueDate');
const [filterBy, setFilterBy] = useState<FilterOption>('all');
@@ -75,14 +77,14 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
case 'priority':
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':
return a.title.localeCompare(b.title);
case 'status':
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:
return 0;
@@ -273,14 +275,8 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
>
<TaskCard
task={task}
onPlay={() => {
onTaskUpdate?.(task.id, { isTracking: true, timeTracked: task.timeTracked });
console.log(`Timer started for ${task.title}`);
}}
onPause={() => {
onTaskUpdate?.(task.id, { isTracking: false });
console.log(`Timer paused for ${task.title}`);
}}
onStartTimer={() => onStartTimer?.(task.id)}
onStopTimer={() => onStopTimer?.(task.id)}
onEdit={() => {
onTaskEdit?.(task);
console.log(`Edit task ${task.title}`);