Skip to content

Commit acdbcef

Browse files
committed
feat: added reset timer button
1 parent b52dd5e commit acdbcef

7 files changed

Lines changed: 92 additions & 9 deletions

File tree

taskin_api/api/todos.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,31 @@ def update_todo_status(todo_id: int, status: TaskStatus, db: Session = Depends(g
7979
return db_todo
8080

8181

82+
@router.post("/todos/{todo_id}/reset-timer", response_model=TodoResponse)
83+
def reset_todo_timer(todo_id: int, db: Session = Depends(get_db)):
84+
"""Reset the in-progress timer for a todo.
85+
86+
This clears the cumulative in-progress seconds and restarts the current
87+
in-progress session (if the task is currently in-progress).
88+
"""
89+
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
90+
if not db_todo:
91+
raise HTTPException(status_code=404, detail="Todo not found")
92+
93+
# Reset accumulated time
94+
db_todo.cumulative_in_progress_seconds = 0.0
95+
96+
# If currently in-progress, restart from now; otherwise clear start marker
97+
if db_todo.status == TaskStatus.in_progress:
98+
db_todo.in_progress_start = datetime.now()
99+
else:
100+
db_todo.in_progress_start = None
101+
102+
db.commit()
103+
db.refresh(db_todo)
104+
return db_todo
105+
106+
82107
def in_timeslot(time_dependency: TimeDependency, current_seconds: float) -> bool:
83108
"""Check if the current time in seconds is within the time dependency slot."""
84109
if time_dependency.start is not None:

ui/src/App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,26 @@ function App() {
609609
}
610610
};
611611

612+
const handleResetTimer = async (id: number) => {
613+
try {
614+
await api.resetTodoTimer(id);
615+
// Optimistically clear local timer data for this todo
616+
setCategories(prev => prev.map(cat => ({
617+
...cat,
618+
todos: cat.todos.map(t => t.id === id
619+
? { ...t, cumulative_in_progress_seconds: 0, in_progress_start: null }
620+
: t,
621+
),
622+
})));
623+
setRecommendedTodos(prev => prev.map(t => t.id === id
624+
? { ...t, cumulative_in_progress_seconds: 0, in_progress_start: null }
625+
: t,
626+
));
627+
} catch (e) {
628+
console.error('Failed to reset timer', e);
629+
}
630+
};
631+
612632
if (isLoading && !bootstrapped) {
613633
return (
614634
<div className="min-h-screen flex items-center justify-center">
@@ -805,8 +825,8 @@ function App() {
805825
style={currentTab === 'graph' ? {} : { WebkitOverflowScrolling: 'touch', overscrollBehavior: 'contain', paddingBottom: `${footerHeight}px` }}
806826
>
807827
<Routes>
808-
<Route path="/all" element={<AllTasksPage categories={categories} summary={summary as any} onStatusChange={handleStatusChange} timeslots={timeslots} />} />
809-
<Route path="/recommended" element={<RecommendedPage todos={recommendedTodos} oneOffs={recommendedOneOffs} onStatusChange={handleStatusChange} onOneOffStatusChange={handleOneOffStatusChange} timeslots={timeslots} />} />
828+
<Route path="/all" element={<AllTasksPage categories={categories} summary={summary as any} onStatusChange={handleStatusChange} timeslots={timeslots} onResetTimer={handleResetTimer} />} />
829+
<Route path="/recommended" element={<RecommendedPage todos={recommendedTodos} oneOffs={recommendedOneOffs} onStatusChange={handleStatusChange} onOneOffStatusChange={handleOneOffStatusChange} timeslots={timeslots} onResetTimer={handleResetTimer} />} />
810830
<Route path="/graph" element={<GraphPage />} />
811831
<Route path="/reports" element={<ReportsPage />} />
812832
<Route path="/settings" element={<SettingsPage />} />

ui/src/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export const api = {
3636
if (!response.ok) throw new Error('Failed to update todo status');
3737
},
3838

39+
async resetTodoTimer(id: number): Promise<void> {
40+
const response = await fetch(`${API_BASE}/todos/${id}/reset-timer`, {
41+
method: 'POST',
42+
});
43+
if (!response.ok) throw new Error('Failed to reset todo timer');
44+
},
45+
3946
// One-off todos
4047
async getOneOffTodos(): Promise<OneOffTodo[]> {
4148
const response = await fetch(`${API_BASE}/oneoff-todos`);

ui/src/components/CategoryCard.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ interface CategoryCardProps {
1212
onStatusChange: (id: number, status: TaskStatus) => void;
1313
timeslots?: Record<number, Timeslot>;
1414
}
15-
16-
export function CategoryCard({ category, onStatusChange, timeslots }: CategoryCardProps) {
15+
export function CategoryCard({ category, onStatusChange, timeslots, onResetTimer }: CategoryCardProps & { onResetTimer?: (id: number) => void }) {
1716
const [open, setOpen] = useState(true);
1817
const [tab, setTab] = useState<'open' | 'completed' | 'skipped' | 'notdue'>('open');
1918

@@ -142,6 +141,7 @@ export function CategoryCard({ category, onStatusChange, timeslots }: CategoryCa
142141
todo={todo}
143142
onStatusChange={onStatusChange}
144143
timeslot={timeslots ? timeslots[todo.id] : undefined}
144+
onResetTimer={onResetTimer}
145145
/>
146146
))}
147147
</div>
@@ -157,6 +157,7 @@ export function CategoryCard({ category, onStatusChange, timeslots }: CategoryCa
157157
todo={todo}
158158
onStatusChange={onStatusChange}
159159
timeslot={timeslots ? timeslots[todo.id] : undefined}
160+
onResetTimer={onResetTimer}
160161
/>
161162
))}
162163
</div>
@@ -172,6 +173,7 @@ export function CategoryCard({ category, onStatusChange, timeslots }: CategoryCa
172173
todo={todo}
173174
onStatusChange={onStatusChange}
174175
timeslot={timeslots ? timeslots[todo.id] : undefined}
176+
onResetTimer={onResetTimer}
175177
/>
176178
))}
177179
</div>
@@ -186,6 +188,7 @@ export function CategoryCard({ category, onStatusChange, timeslots }: CategoryCa
186188
todo={todo}
187189
onStatusChange={onStatusChange}
188190
timeslot={timeslots ? timeslots[todo.id] : undefined}
191+
onResetTimer={onResetTimer}
189192
/>
190193
))}
191194
</div>

ui/src/components/TodoItem.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ import { Todo, TaskStatus, Timeslot } from '../types';
22
import { useEffect, useState } from 'react';
33
import { Button } from './ui/button';
44
import { cn, statusButtonClasses, statusBadgeClasses } from '../lib/utils';
5-
import { Check, Circle, CircleDashed, SkipForward, Clock } from 'lucide-react';
5+
import { Check, Circle, CircleDashed, SkipForward, Clock, RotateCcw } from 'lucide-react';
66
import { Badge } from './ui/badge';
77
import { TimeslotChip } from './TimeslotChip';
88

99
interface TodoItemProps {
1010
todo: Todo;
1111
onStatusChange: (id: number, status: TaskStatus) => void;
1212
timeslot?: Timeslot;
13+
onResetTimer?: (id: number) => void;
1314
}
1415

15-
export function TodoItem({ todo, onStatusChange, timeslot }: TodoItemProps) {
16+
export function TodoItem({ todo, onStatusChange, timeslot, onResetTimer }: TodoItemProps) {
1617
const handleStatusChange = (newStatus: TaskStatus) => {
1718
// Optimistic update delegated to parent
1819
onStatusChange(todo.id, newStatus);
@@ -165,6 +166,19 @@ export function TodoItem({ todo, onStatusChange, timeslot }: TodoItemProps) {
165166
</Button>
166167
);
167168
})}
169+
{onResetTimer && (displaySeconds > 0 || todo.status === 'in-progress') && (
170+
<Button
171+
type="button"
172+
variant="outline"
173+
size="sm"
174+
className="flex items-center gap-1.5 border"
175+
onClick={() => onResetTimer(todo.id)}
176+
aria-label="Reset in-progress timer"
177+
>
178+
<RotateCcw className="w-4 h-4 sm:mr-2 shrink-0" />
179+
<span className="hidden sm:inline">Reset timer</span>
180+
</Button>
181+
)}
168182
{/* Realtime timer moved next to title to match Recommended page styling */}
169183
</div>
170184

ui/src/pages/AllTasksPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ interface Props {
2020
onStatusChange: (id: number, status: TaskStatus) => void;
2121
timeslots?: Record<number, Timeslot>;
2222
}
23-
24-
export default function AllTasksPage({ categories, summary, onStatusChange, timeslots }: Props) {
23+
export default function AllTasksPage({ categories, summary, onStatusChange, timeslots, onResetTimer }: Props & { onResetTimer: (id: number) => void }) {
2524
return (
2625
<div className="space-y-4">
2726
{/* Summary bar */}
@@ -94,6 +93,7 @@ export default function AllTasksPage({ categories, summary, onStatusChange, time
9493
category={category}
9594
onStatusChange={onStatusChange}
9695
timeslots={timeslots}
96+
onResetTimer={onResetTimer}
9797
/>
9898
))
9999
)}

ui/src/pages/RecommendedPage.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ interface Props {
1212
onStatusChange: (id: number, status: TaskStatus) => void;
1313
onOneOffStatusChange?: (id: number, status: TaskStatus) => void;
1414
timeslots?: Record<number, Timeslot>;
15+
onResetTimer: (id: number) => void;
1516
}
1617

17-
export default function RecommendedPage({ todos, oneOffs = [], onStatusChange, onOneOffStatusChange, timeslots }: Props) {
18+
export default function RecommendedPage({ todos, oneOffs = [], onStatusChange, onOneOffStatusChange, timeslots, onResetTimer }: Props) {
1819
if (todos.length === 0 && oneOffs.length === 0) {
1920
return (
2021
<div className="bg-background rounded-lg border shadow-sm p-6">
@@ -105,6 +106,19 @@ export default function RecommendedPage({ todos, oneOffs = [], onStatusChange, o
105106
<Button key={s} size="sm" variant="outline" className={classes} onClick={() => onStatusChange(todo.id, 'skipped')} aria-label="Mark as skipped"><SkipForward className="w-4 h-4 sm:mr-2 shrink-0" /><span className="hidden sm:inline">Skip</span></Button>
106107
);
107108
})}
109+
{(Number((todo as any).cumulative_in_progress_seconds || 0) > 0 || todo.status === 'in-progress') && (
110+
<Button
111+
type="button"
112+
size="sm"
113+
variant="outline"
114+
className="flex items-center gap-1.5 border"
115+
onClick={() => onResetTimer(todo.id)}
116+
aria-label="Reset in-progress timer"
117+
>
118+
<Clock className="w-4 h-4 sm:mr-2 shrink-0" />
119+
<span className="hidden sm:inline">Reset timer</span>
120+
</Button>
121+
)}
108122
</div>
109123
</div>
110124
);

0 commit comments

Comments
 (0)