diff --git a/backend/src/routes/timer.routes.ts b/backend/src/routes/timer.routes.ts index 8b7a8a4..474b0c6 100644 --- a/backend/src/routes/timer.routes.ts +++ b/backend/src/routes/timer.routes.ts @@ -48,6 +48,16 @@ router.put( } ); +// DELETE /api/timer - Cancel (discard) the ongoing timer without creating a time entry +router.delete('/', requireAuth, async (req: AuthenticatedRequest, res, next) => { + try { + await timerService.cancel(req.user!.id); + res.status(204).send(); + } catch (error) { + next(error); + } +}); + // POST /api/timer/stop - Stop timer router.post( '/stop', diff --git a/backend/src/services/timer.service.ts b/backend/src/services/timer.service.ts index 4ad475a..792eed9 100644 --- a/backend/src/services/timer.service.ts +++ b/backend/src/services/timer.service.ts @@ -138,6 +138,15 @@ export class TimerService { }); } + async cancel(userId: string) { + const timer = await this.getOngoingTimer(userId); + if (!timer) { + throw new NotFoundError("No timer is running"); + } + + await prisma.ongoingTimer.delete({ where: { userId } }); + } + async stop(userId: string, data?: StopTimerInput) { const timer = await this.getOngoingTimer(userId); if (!timer) { diff --git a/frontend/src/api/timer.ts b/frontend/src/api/timer.ts index c0c06ae..7580651 100644 --- a/frontend/src/api/timer.ts +++ b/frontend/src/api/timer.ts @@ -30,4 +30,8 @@ export const timerApi = { }); return data; }, + + cancel: async (): Promise => { + await apiClient.delete('/timer'); + }, }; \ No newline at end of file diff --git a/frontend/src/components/TimerWidget.tsx b/frontend/src/components/TimerWidget.tsx index 3b07cb1..f8f400e 100644 --- a/frontend/src/components/TimerWidget.tsx +++ b/frontend/src/components/TimerWidget.tsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Play, Square, ChevronDown, Pencil, Check, X } from "lucide-react"; +import { Play, Square, ChevronDown, Pencil, Check, X, Trash2 } from "lucide-react"; import { useTimer } from "@/contexts/TimerContext"; import { useProjects } from "@/hooks/useProjects"; import { ProjectColorDot } from "@/components/ProjectColorDot"; @@ -49,12 +49,14 @@ export function TimerWidget() { elapsedSeconds, startTimer, stopTimer, + cancelTimer, updateTimerProject, updateTimerStartTime, } = useTimer(); const { projects } = useProjects(); const [showProjectSelect, setShowProjectSelect] = useState(false); const [error, setError] = useState(null); + const [confirmCancel, setConfirmCancel] = useState(false); // Start time editing state const [editingStartTime, setEditingStartTime] = useState(false); @@ -72,6 +74,7 @@ export function TimerWidget() { const handleStop = async () => { setError(null); + setConfirmCancel(false); try { await stopTimer(); } catch (err) { @@ -79,6 +82,16 @@ export function TimerWidget() { } }; + const handleCancelTimer = async () => { + setError(null); + try { + await cancelTimer(); + setConfirmCancel(false); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to cancel timer"); + } + }; + const handleProjectChange = async (projectId: string) => { setError(null); try { @@ -195,14 +208,44 @@ export function TimerWidget() { )} - {/* Stop Button */} - + {/* Stop + Cancel Buttons */} +
+ {confirmCancel ? ( +
+ Discard timer? + + +
+ ) : ( + <> + + + + )} +
{/* Project Selector — full width on mobile, auto on desktop */} diff --git a/frontend/src/contexts/TimerContext.tsx b/frontend/src/contexts/TimerContext.tsx index 2a72604..361b0ab 100644 --- a/frontend/src/contexts/TimerContext.tsx +++ b/frontend/src/contexts/TimerContext.tsx @@ -18,6 +18,7 @@ interface TimerContextType { startTimer: (projectId?: string) => Promise; updateTimerProject: (projectId?: string | null) => Promise; updateTimerStartTime: (startTime: string) => Promise; + cancelTimer: () => Promise; stopTimer: (projectId?: string) => Promise; } @@ -85,6 +86,14 @@ export function TimerProvider({ children }: { children: ReactNode }) { }, }); + // Cancel timer mutation + const cancelMutation = useMutation({ + mutationFn: timerApi.cancel, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] }); + }, + }); + // Stop timer mutation const stopMutation = useMutation({ mutationFn: timerApi.stop, @@ -115,6 +124,10 @@ export function TimerProvider({ children }: { children: ReactNode }) { [updateMutation], ); + const cancelTimer = useCallback(async () => { + await cancelMutation.mutateAsync(); + }, [cancelMutation]); + const stopTimer = useCallback( async (projectId?: string): Promise => { try { @@ -136,6 +149,7 @@ export function TimerProvider({ children }: { children: ReactNode }) { startTimer, updateTimerProject, updateTimerStartTime, + cancelTimer, stopTimer, }} >