Add ability to manually adjust the running timer's start time #4

Merged
simonfranken merged 6 commits from feature/adjust-timer-start-time into main 2026-02-23 09:57:24 +00:00
5 changed files with 89 additions and 9 deletions
Showing only changes of commit 06596dcee9 - Show all commits

View File

@@ -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 // POST /api/timer/stop - Stop timer
router.post( router.post(
'/stop', '/stop',

View File

@@ -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) { async stop(userId: string, data?: StopTimerInput) {
const timer = await this.getOngoingTimer(userId); const timer = await this.getOngoingTimer(userId);
if (!timer) { if (!timer) {

View File

@@ -30,4 +30,8 @@ export const timerApi = {
}); });
return data; return data;
}, },
cancel: async (): Promise<void> => {
await apiClient.delete('/timer');
},
}; };

View File

@@ -1,5 +1,5 @@
import { useState, useRef } from "react"; 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 { useTimer } from "@/contexts/TimerContext";
import { useProjects } from "@/hooks/useProjects"; import { useProjects } from "@/hooks/useProjects";
import { ProjectColorDot } from "@/components/ProjectColorDot"; import { ProjectColorDot } from "@/components/ProjectColorDot";
@@ -49,12 +49,14 @@ export function TimerWidget() {
elapsedSeconds, elapsedSeconds,
startTimer, startTimer,
stopTimer, stopTimer,
cancelTimer,
updateTimerProject, updateTimerProject,
updateTimerStartTime, updateTimerStartTime,
} = useTimer(); } = useTimer();
const { projects } = useProjects(); const { projects } = useProjects();
const [showProjectSelect, setShowProjectSelect] = useState(false); const [showProjectSelect, setShowProjectSelect] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [confirmCancel, setConfirmCancel] = useState(false);
// Start time editing state // Start time editing state
const [editingStartTime, setEditingStartTime] = useState(false); const [editingStartTime, setEditingStartTime] = useState(false);
@@ -72,6 +74,7 @@ export function TimerWidget() {
const handleStop = async () => { const handleStop = async () => {
setError(null); setError(null);
setConfirmCancel(false);
try { try {
await stopTimer(); await stopTimer();
} catch (err) { } 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) => { const handleProjectChange = async (projectId: string) => {
setError(null); setError(null);
try { try {
@@ -195,14 +208,44 @@ export function TimerWidget() {
)} )}
</div> </div>
{/* Stop Button */} {/* Stop + Cancel Buttons */}
<div className="flex items-center space-x-2 shrink-0 sm:order-last">
{confirmCancel ? (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">Discard timer?</span>
<button
onClick={() => void handleCancelTimer()}
className="flex items-center space-x-1 px-3 py-2 bg-gray-700 text-white rounded-lg font-medium hover:bg-gray-800 transition-colors text-sm"
>
<Trash2 className="h-4 w-4" />
<span>Discard</span>
</button>
<button
onClick={() => setConfirmCancel(false)}
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
>
Keep
</button>
</div>
) : (
<>
<button
onClick={() => setConfirmCancel(true)}
title="Discard timer"
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<Trash2 className="h-5 w-5" />
</button>
<button <button
onClick={handleStop} onClick={handleStop}
className="flex items-center space-x-2 px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors shrink-0 sm:order-last" className="flex items-center space-x-2 px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
> >
<Square className="h-5 w-5 fill-current" /> <Square className="h-5 w-5 fill-current" />
<span>Stop</span> <span>Stop</span>
</button> </button>
</>
)}
</div>
</div> </div>
{/* Project Selector — full width on mobile, auto on desktop */} {/* Project Selector — full width on mobile, auto on desktop */}

View File

@@ -18,6 +18,7 @@ interface TimerContextType {
startTimer: (projectId?: string) => Promise<void>; startTimer: (projectId?: string) => Promise<void>;
updateTimerProject: (projectId?: string | null) => Promise<void>; updateTimerProject: (projectId?: string | null) => Promise<void>;
updateTimerStartTime: (startTime: string) => Promise<void>; updateTimerStartTime: (startTime: string) => Promise<void>;
cancelTimer: () => Promise<void>;
stopTimer: (projectId?: string) => Promise<TimeEntry | null>; stopTimer: (projectId?: string) => Promise<TimeEntry | null>;
} }
@@ -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 // Stop timer mutation
const stopMutation = useMutation({ const stopMutation = useMutation({
mutationFn: timerApi.stop, mutationFn: timerApi.stop,
@@ -115,6 +124,10 @@ export function TimerProvider({ children }: { children: ReactNode }) {
[updateMutation], [updateMutation],
); );
const cancelTimer = useCallback(async () => {
await cancelMutation.mutateAsync();
}, [cancelMutation]);
const stopTimer = useCallback( const stopTimer = useCallback(
async (projectId?: string): Promise<TimeEntry | null> => { async (projectId?: string): Promise<TimeEntry | null> => {
try { try {
@@ -136,6 +149,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
startTimer, startTimer,
updateTimerProject, updateTimerProject,
updateTimerStartTime, updateTimerStartTime,
cancelTimer,
stopTimer, stopTimer,
}} }}
> >