From ba31037d0f510ca0a4110dc37ba8db79865d412a Mon Sep 17 00:00:00 2001 From: felix bear Date: Tue, 9 Jun 2026 03:44:47 +0900 Subject: [PATCH] fix(worklog): avoid misleading export start/end times (#8090) * fix(worklog): avoid misleading export times #5654 * chore: rerun worklog export checks * fix(worklog): show session times once per day #5654 --------- Co-authored-by: cocojojo5213 --- docs/wiki/4.21-Worklog.md | 4 +- .../worklog-export.util.spec.ts | 39 ++++++++++++++++++- .../worklog-export/worklog-export.util.ts | 18 ++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/docs/wiki/4.21-Worklog.md b/docs/wiki/4.21-Worklog.md index dc931ee0a6..c19a79d479 100644 --- a/docs/wiki/4.21-Worklog.md +++ b/docs/wiki/4.21-Worklog.md @@ -20,7 +20,7 @@ The transformation does the following: - **Iterates over every task** and every day in that task’s “time spent per day” map. - **Builds a hierarchy** — Year → month → day (or, in week view, year → week → day). Each day has a list of entries (task + time for that day) and a **total time** for the day. - **Places subtasks** — When a task has a parent, its entry is placed immediately after the parent (or after a sibling) so the list reflects hierarchy. -- **Uses locale and work times** — Day labels use your locale; if you have work start/end times configured, they can be shown per day where available. +- **Uses locale and work times** — Day labels use your locale; if you have work start/end times configured, they can be shown for day-level rows where available. - **Aggregates without double-counting** — Only **leaf tasks** (tasks with no subtasks) contribute their time to the day/month/year totals. Parent tasks’ own “time spent per day” is not added again on top of subtask time, so the totals are not inflated. So the worklog is a **reorganized view** of the same “time spent per day” data that lives on tasks (and in the archive). When you open History (including its legacy `worklog`/`quick-history` routes) or the daily summary, the app loads the relevant tasks (active + archive for the context), runs this transformation, and shows the result. @@ -37,7 +37,7 @@ So the worklog is a **reorganized view** of the same “time spent per day” da - **Inline time correction** — Leaf-task rows can be corrected inline. The app updates the task's “time spent per day” value for that date, so the corrected time is reflected in History, reports, and metrics. - **Parent rows** — Parent tasks show the sum of their subtasks and are not edited directly, which avoids double-counting. -- **Export** — You can export the worklog (e.g. to CSV or for external time sheets). Export uses the same task–date–duration data and can be **filtered by project and date range**. An option lets you **exclude time that falls outside the chosen range**, so only time within the range is included in the export. That way you get a clean slice of work for the period you care about. +- **Export** — You can export the worklog (e.g. to CSV or for external time sheets). Export uses the same task–date–duration data and can be **filtered by project and date range**. An option lets you **exclude time that falls outside the chosen range**, so only time within the range is included in the export. When export is grouped by individual worklog rows, each row represents a task, date, and duration; start and end times are day-level values rather than per-task session times. That way you get a clean slice of work for the period you care about. ## Summary diff --git a/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts b/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts index 38397b83ff..f3fd62c496 100644 --- a/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts +++ b/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts @@ -1,6 +1,6 @@ import { WorkStartEnd } from 'src/app/features/work-context/work-context.model'; import { WorklogGrouping } from '../worklog.model'; -import { createRows } from './worklog-export.util'; +import { createRows, formatRows } from './worklog-export.util'; import { DEFAULT_TASK, WorklogTask } from '../../tasks/task.model'; import { DEFAULT_PROJECT } from '../../project/project.const'; import { DEFAULT_TAG } from '../../tag/tag.const'; @@ -372,6 +372,43 @@ describe('createRows', () => { expect(rows[2].titlesWithSub).toEqual([taskId2]); expect(rows[2].dates).toEqual([dateKey2]); }); + + it('should only show day-level start and end times on the first task row for a day', () => { + const rows = createRows( + createWorklogData({ + tasks: [task1, task2], + workTimes, + }), + WorklogGrouping.WORKLOG, + ); + + expect(rows.length).toBe(3); + expect(rows.map((row) => row.workStart)).toEqual([ + workTimes.start[dateKey1], + 0, + workTimes.start[dateKey2], + ]); + expect(rows.map((row) => row.workEnd)).toEqual([ + workTimes.end[dateKey1], + 0, + workTimes.end[dateKey2], + ]); + + const formattedRows = formatRows(rows, { + roundWorkTimeTo: null, + roundStartTimeTo: null, + roundEndTimeTo: null, + separateTasksBy: ' | ', + cols: ['DATE', 'START', 'END', 'TIME_CLOCK', 'TITLES_INCLUDING_SUB'], + groupBy: WorklogGrouping.WORKLOG, + }); + + expect(formattedRows.map((row) => row.slice(0, 3))).toEqual([ + [dateKey1, '10:00', '12:00'], + [dateKey1, ' - ', ' - '], + [dateKey2, '14:00', '16:00'], + ]); + }); }); }); diff --git a/src/app/features/worklog/worklog-export/worklog-export.util.ts b/src/app/features/worklog/worklog-export/worklog-export.util.ts index 1435dbc00e..af8190c80f 100644 --- a/src/app/features/worklog/worklog-export/worklog-export.util.ts +++ b/src/app/features/worklog/worklog-export/worklog-export.util.ts @@ -49,7 +49,7 @@ export const createRows = ( rows.push(groups[key]); }); - return rows; + return groupBy === WorklogGrouping.WORKLOG ? clearRepeatedWorklogDayTimes(rows) : rows; }; /** @@ -185,6 +185,22 @@ const handleWorklogGroup = (data: WorklogExportData): ItemsByKey => { return taskGroups; }; +const clearRepeatedWorklogDayTimes = (rows: RowItem[]): RowItem[] => { + const seenDays = new Set(); + return rows.map((row) => { + const day = row.dates[0]; + if (seenDays.has(day)) { + return { + ...row, + workStart: 0, + workEnd: 0, + }; + } + seenDays.add(day); + return row; + }); +}; + /** * Unfolds task into taskFields while mapping id's to titles, and minor formatting */