feat(gantt): natural day-boundary rounding in Gantt chart (#1476)
This commit is contained in:
parent
0506b9215a
commit
d14443d2f2
|
|
@ -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<GanttBarModel[][]>([])
|
|||
const ganttRows = ref<string[]>([])
|
||||
const cellsByRow = ref<Record<string, string[]>>({})
|
||||
|
||||
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<string, string[]> = {}
|
||||
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue