diff --git a/frontend/src/components/gantt/GanttChart.vue b/frontend/src/components/gantt/GanttChart.vue index 23fab0cc3..f60c255ac 100644 --- a/frontend/src/components/gantt/GanttChart.vue +++ b/frontend/src/components/gantt/GanttChart.vue @@ -85,6 +85,7 @@ import GanttTimelineHeader from '@/components/gantt/GanttTimelineHeader.vue' import Loading from '@/components/misc/Loading.vue' import {MILLISECONDS_A_DAY} from '@/constants/date' +import {roundToNaturalDayBoundary} from '@/helpers/time/roundToNaturalDayBoundary' const props = defineProps<{ isLoading: boolean, @@ -149,16 +150,16 @@ const ganttBars = ref([]) const ganttRows = ref([]) const cellsByRow = ref>({}) +function getRoundedDate(value: string | Date | undefined, fallback: Date, isStart: boolean) { + return roundToNaturalDayBoundary(value ? new Date(value) : new Date(fallback), isStart) +} + function transformTaskToGanttBar(t: ITask): GanttBarModel { - const startDate = t.startDate - ? new Date(t.startDate) - : new Date(props.defaultTaskStartDate) - const endDate = t.endDate - ? new Date(t.endDate) - : new Date(props.defaultTaskEndDate) - + const startDate = getRoundedDate(t.startDate, props.defaultTaskStartDate, true) + const endDate = getRoundedDate(t.endDate, props.defaultTaskEndDate, false) + const taskColor = getHexColor(t.hexColor) - + const bar = { id: String(t.id), start: startDate, @@ -171,7 +172,7 @@ function transformTaskToGanttBar(t: ITask): GanttBarModel { isDone: t.done, }, } - + return bar } @@ -181,22 +182,18 @@ watch( const bars: GanttBarModel[] = [] const rows: string[] = [] const cells: Record = {} - + const filteredTasks = Array.from(tasks.value.values()).filter(task => { if (!filters.value.showTasksWithoutDates && (!task.startDate || !task.endDate)) { return false } - - const taskStart = task.startDate - ? new Date(task.startDate) - : new Date(props.defaultTaskStartDate) - const taskEnd = task.endDate - ? new Date(task.endDate) - : new Date(props.defaultTaskEndDate) - + + const taskStart = getRoundedDate(task.startDate, props.defaultTaskStartDate, true) + const taskEnd = getRoundedDate(task.endDate, props.defaultTaskEndDate, false) + // Task is visible if it overlaps with the current date range - return taskStart <= dateToDate.value - && taskEnd >= dateFromDate.value + return taskStart <= dateToDate.value +&& taskEnd >= dateFromDate.value }) filteredTasks.forEach((t, index) => { @@ -225,8 +222,8 @@ watch( function updateGanttTask(id: string, newStart: Date, newEnd: Date) { emit('update:task', { id: Number(id), - startDate: dayjs(newStart).startOf('day').toDate(), - endDate: dayjs(newEnd).endOf('day').toDate(), + startDate: roundToNaturalDayBoundary(newStart, true), + endDate: roundToNaturalDayBoundary(newEnd), }) } diff --git a/frontend/src/components/gantt/GanttRowBars.vue b/frontend/src/components/gantt/GanttRowBars.vue index 7ae7f429e..9bc7e9111 100644 --- a/frontend/src/components/gantt/GanttRowBars.vue +++ b/frontend/src/components/gantt/GanttRowBars.vue @@ -104,6 +104,7 @@ import dayjs from 'dayjs' import type {GanttBarModel} from '@/composables/useGanttBar' import {getTextColor, LIGHT} from '@/helpers/color/getTextColor' import {MILLISECONDS_A_DAY} from '@/constants/date' +import {roundToNaturalDayBoundary} from '@/helpers/time/roundToNaturalDayBoundary' import GanttBarPrimitive from './primitives/GanttBarPrimitive.vue' @@ -152,7 +153,10 @@ function computeBarX(startDate: Date) { } function getDaysDifference(startDate: Date, endDate: Date): number { - return Math.floor((endDate.getTime() - startDate.getTime()) / MILLISECONDS_A_DAY) + return Math.ceil( + (roundToNaturalDayBoundary(endDate).getTime() - roundToNaturalDayBoundary(startDate, true).getTime()) / +MILLISECONDS_A_DAY, + ) } function computeBarWidth(bar: GanttBarModel) { diff --git a/frontend/src/helpers/time/roundToNaturalDayBoundary.test.ts b/frontend/src/helpers/time/roundToNaturalDayBoundary.test.ts new file mode 100644 index 000000000..b29850ada --- /dev/null +++ b/frontend/src/helpers/time/roundToNaturalDayBoundary.test.ts @@ -0,0 +1,30 @@ +import {describe, it, expect} from 'vitest' + +import {roundToNaturalDayBoundary} from './roundToNaturalDayBoundary' +import {MILLISECONDS_A_DAY} from '@/constants/date' + +describe('roundToNaturalDayBoundary', () => { + it('rounds start dates to start of day regardless of time', () => { + const morning = new Date('2024-01-01T08:00:00') + const afternoon = new Date('2024-01-01T13:00:00') + expect(roundToNaturalDayBoundary(morning, true)).toEqual(new Date('2024-01-01T00:00:00.000')) + expect(roundToNaturalDayBoundary(afternoon, true)).toEqual(new Date('2024-01-01T00:00:00.000')) + }) + + it('rounds end dates to natural boundaries', () => { + const morning = new Date('2024-01-01T08:00:00') + const afternoon = new Date('2024-01-01T13:00:00') + expect(roundToNaturalDayBoundary(morning)).toEqual(new Date('2024-01-01T00:00:00.000')) + expect(roundToNaturalDayBoundary(afternoon)).toEqual(new Date('2024-01-01T23:59:59.999')) + }) + + it('counts inclusive days for same-day evening end', () => { + const start = new Date('2024-01-01T15:00:00') + const end = new Date('2024-01-01T18:00:00') + const diff = Math.ceil( + (roundToNaturalDayBoundary(end).getTime() - roundToNaturalDayBoundary(start, true).getTime()) / + MILLISECONDS_A_DAY, + ) + expect(diff).toBe(1) + }) +}) diff --git a/frontend/src/helpers/time/roundToNaturalDayBoundary.ts b/frontend/src/helpers/time/roundToNaturalDayBoundary.ts new file mode 100644 index 000000000..8586914cb --- /dev/null +++ b/frontend/src/helpers/time/roundToNaturalDayBoundary.ts @@ -0,0 +1,9 @@ +export function roundToNaturalDayBoundary(date: Date, isStart = false): Date { + const d = new Date(date) + if (isStart || d.getHours() < 12) { + d.setHours(0, 0, 0, 0) + } else { + d.setHours(23, 59, 59, 999) + } + return d +}