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 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
felix bear 2026-06-09 03:44:47 +09:00 committed by GitHub
parent fe65d4b9a6
commit ba31037d0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 4 deletions

View file

@ -20,7 +20,7 @@ The transformation does the following:
- **Iterates over every task** and every day in that tasks “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 taskdateduration 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 taskdateduration 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

View file

@ -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'],
]);
});
});
});

View file

@ -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<RowItem> => {
return taskGroups;
};
const clearRepeatedWorklogDayTimes = (rows: RowItem[]): RowItem[] => {
const seenDays = new Set<string>();
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
*/