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

- 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`
— `src/utils/timeUtil.ts`, `src/components/ArchivedTaskRow.tsx`, `src/components/ArchiveEditDialog.tsx`, `src/components/ui/scroll-time-picker.tsx`, `src/components/ArchivedTaskRow.test.tsx`, `src/components/ArchiveEditDialog.test.tsx`, `src/utils/timeUtil.test.ts`
- Task durations and a day's end time can no longer go negative. `endDay` rounds the end time to 15 minutes while a running task's start is unrounded, so ending a day shortly after starting a task (e.g. task started 9:20, day ended 9:21 → rounded to 9:15) produced a negative task duration and an end time before the task began. `endDay` now never ends earlier than the day start, the running task's start, or the latest recorded task end. Every duration subtraction (task end, `beginTask`, `adjustTaskTime`, planned-task completion, archive task edit/add) goes through a new zero-floored `elapsedMs` helper, and `adjustTaskTime` collapses an end-before-start edit to a zero-length task. Stored negative values from earlier versions are ignored by the day/period/hours/revenue calculations and display as zero in `formatDuration`/`formatDurationLong`
Expand Down
42 changes: 42 additions & 0 deletions src/contexts/TimeTracking.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,48 @@ describe("TimeTrackingContext", () => {
});
});

describe("adjustTaskTime and a linked planned task", () => {
it("resyncs timeSpent after editing the ended task's time", async () => {
const { result } = renderHook(() => useTimeTracking(), { wrapper });
await waitFor(() => expect(result.current.loading).toBe(false));

await act(async () => {
result.current.startDay(new Date("2024-12-03T09:00:00Z"));
});
await act(async () => {
result.current.addPlannedTask({ title: "Board task" });
});
let plannedId: string;
await waitFor(() => { plannedId = result.current.plannedTasks[0].id; });

await act(async () => {
result.current.pullPlannedTaskToDay(plannedId);
});
let taskId: string;
await waitFor(() => { taskId = result.current.currentTask!.id; });

await act(async () => {
result.current.endDay(new Date("2024-12-03T11:30:00Z"));
});
await waitFor(() => {
expect(result.current.plannedTasks[0].timeSpent).toBe(9_000_000); // 2h30m
});

await act(async () => {
result.current.adjustTaskTime(
taskId,
new Date("2024-12-03T09:00:00Z"),
new Date("2024-12-03T11:45:00Z")
);
});

await waitFor(() => {
expect(result.current.plannedTasks[0].timeSpent).toBe(9_900_000); // 2h45m
expect(result.current.plannedTasks[0].timeEntries[0].duration).toBe(9_900_000);
});
});
});

describe("addBackdatedDay", () => {
it("adds a backdated day to the archive", async () => {
const { result } = renderHook(() => useTimeTracking(), { wrapper });
Expand Down
6 changes: 5 additions & 1 deletion src/contexts/TimeTrackingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,11 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
.then(() => setLastSyncTime(new Date()))
.catch(error => console.error("❌ Error saving after adjusting task time:", error));
}
}, [tasks, currentTask, dayStartTime, isDayStarted, dataService]);
if (roundedEndTime) {
const updatedTask = updatedTasks.find(t => t.id === taskId);
if (updatedTask) accumulatePlannedTaskTime(taskId, updatedTask.duration);
}
}, [tasks, currentTask, dayStartTime, isDayStarted, dataService, accumulatePlannedTaskTime]);

const getTotalHoursForPeriod = useCallback((startDate: Date, endDate: Date): number =>
calcTotalHoursForPeriod(archivedDays, startDate, endDate), [archivedDays]);
Expand Down
Loading