Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Deleting the active task used to leave the day with no running task and no way to resume the one before it. `deleteTask` now surfaces an "Resume previous task?" prompt on the Dashboard when the deleted task was active and an earlier task exists, letting the user resume it (via new `reopenTask`, which clears its `endTime`/`duration` and makes it current again) or start a new task instead
— `src/contexts/TimeTrackingContext.tsx`, `src/pages/Index.tsx`
- Manually editing a current-day task's end time (via `TaskEditDialog`) no longer leaves a linked planned task's "Time Spent" stale. `adjustTaskTime` previously updated only the day task's own `duration`; it now also calls `accumulatePlannedTaskTime` so the planned task's `timeSpent` and matching `timeEntries` duration resync to the edited time
— `src/contexts/TimeTrackingContext.tsx`, `src/contexts/TimeTracking.test.tsx`
- Archive editors now reject an end time before the start time instead of silently saving a zero-length task. `ArchivedTaskRow` and the Add Task form in `ArchiveEditDialog` show an inline `role="alert"` message, set `aria-invalid`/`aria-describedby` on the end-time input, and disable Save/Add Task until corrected; the day summary form rejects it through its zod schema, and Save Changes reopens a collapsed summary editor so the error is visible. Equal start and end times remain allowed because 15-minute rounding can collapse a short task to zero length. New shared `isEndBeforeStart` and `END_BEFORE_START_MESSAGE` in `timeUtil`; `TimePicker` accepts `aria-invalid`
Expand Down
20 changes: 20 additions & 0 deletions src/contexts/TimeTrackingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ interface TimeTrackingContextType {
) => string;
updateTask: (taskId: string, updates: Partial<Task>) => void;
deleteTask: (taskId: string) => void;
reopenTask: (taskId: string) => void;
postDay: (notes?: string) => void;

// Project management
Expand Down Expand Up @@ -909,6 +910,23 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
}
}, [tasks, currentTask, dayStartTime, isDayStarted, dataService]);

// Reopens a previously completed task as the current task (clears endTime/duration).
// Used when the active task is deleted and the user chooses to resume the one before it.
const reopenTask = useCallback((taskId: string) => {
const target = tasks.find(t => t.id === taskId);
if (!target) return;
const { endTime, duration, ...reopened } = target;
const updatedTasks = tasks.map(t => (t.id === taskId ? reopened : t));
setTasks(updatedTasks);
setCurrentTask(reopened);
setHasUnsavedChanges(true);
if (dataService) {
dataService.saveCurrentDay({ isDayStarted, dayStartTime, currentTask: reopened, tasks: updatedTasks })
.then(() => setLastSyncTime(new Date()))
.catch(error => console.error("❌ Error saving after reopening task:", error));
}
}, [tasks, dayStartTime, isDayStarted, dataService]);

const postDay = useCallback(async (notes?: string) => {
if (!dayStartTime) return;

Expand Down Expand Up @@ -1761,6 +1779,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
startNewTask,
updateTask,
deleteTask,
reopenTask,
postDay,
plannedTasks,
addPlannedTask,
Expand Down Expand Up @@ -1832,6 +1851,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
startNewTask,
updateTask,
deleteTask,
reopenTask,
postDay,
plannedTasks,
addPlannedTask,
Expand Down
48 changes: 47 additions & 1 deletion src/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import { Popover, PopoverContent, PopoverHeader, PopoverTrigger, PopoverTitle } from "@/components/ui/popover";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import type { Task } from "@/contexts/TimeTrackingContext";

// Stable epoch constant — avoids creating new Date(0) on every render
const EPOCH = new Date(0);
Expand All @@ -47,13 +58,15 @@ const TimeTrackerContent = () => {
discardDay,
postDay,
deleteTask,
reopenTask,
startNewTask,
getTotalHoursForPeriod,
} = useTimeTracking();
const now = useCurrentTime();

const [showStartDayDialog, setShowStartDayDialog] = useState(false);
const [showAddTaskForm, setShowAddTaskForm] = useState(false);
const [resumeCandidate, setResumeCandidate] = useState<Task | null>(null);

useEffect(() => {
if (consumePendingMenuAction("new-task")) {
Expand Down Expand Up @@ -82,6 +95,15 @@ const TimeTrackerContent = () => {
discardDay();
};

const handleDeleteTask = (taskId: string) => {
const wasActive = currentTask?.id === taskId;
const remaining = tasks.filter(t => t.id !== taskId);
deleteTask(taskId);
if (wasActive && remaining.length > 0) {
setResumeCandidate(remaining[remaining.length - 1]);
}
};

const handleNewTask = (
title: string,
description?: string,
Expand Down Expand Up @@ -385,7 +407,7 @@ const TimeTrackerContent = () => {
? getCurrentTaskDuration(currentTask, now)
: 0
}
onDelete={deleteTask}
onDelete={handleDeleteTask}
/>
</motion.div>
))}
Expand All @@ -406,6 +428,30 @@ const TimeTrackerContent = () => {
</div>
</div>
</div>

<AlertDialog open={resumeCandidate !== null} onOpenChange={(open) => !open && setResumeCandidate(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Resume previous task?</AlertDialogTitle>
<AlertDialogDescription>
The active task was deleted. Resume "{resumeCandidate?.title}" or start a new task?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setResumeCandidate(null)}>
Start New Task
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (resumeCandidate) reopenTask(resumeCandidate.id);
setResumeCandidate(null);
}}
>
Resume "{resumeCandidate?.title}"
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</PageLayout>
);
};
Expand Down
Loading