diff --git a/frontend/src/components/gantt/GanttChart.vue b/frontend/src/components/gantt/GanttChart.vue
index 642fe7792..99a39fa16 100644
--- a/frontend/src/components/gantt/GanttChart.vue
+++ b/frontend/src/components/gantt/GanttChart.vue
@@ -62,6 +62,13 @@
+
@@ -76,6 +83,7 @@ import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
import {getHexColor} from '@/models/task'
import {buildGanttTaskTree, type GanttTaskTreeNode} from '@/helpers/ganttTaskTree'
+import {buildRelationArrows, type GanttBarPosition, type GanttArrow} from '@/helpers/ganttRelationArrows'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO'
@@ -87,18 +95,22 @@ import GanttRow from '@/components/gantt/GanttRow.vue'
import GanttRowBars from '@/components/gantt/GanttRowBars.vue'
import GanttVerticalGridLines from '@/components/gantt/GanttVerticalGridLines.vue'
import GanttTimelineHeader from '@/components/gantt/GanttTimelineHeader.vue'
+import GanttRelationArrows from '@/components/gantt/GanttRelationArrows.vue'
import Loading from '@/components/misc/Loading.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import {roundToNaturalDayBoundary} from '@/helpers/time/roundToNaturalDayBoundary'
-const props = defineProps<{
+const props = withDefaults(defineProps<{
isLoading: boolean,
filters: GanttFilters,
tasks: Map,
defaultTaskStartDate: DateISO
defaultTaskEndDate: DateISO
-}>()
+ showRelationArrows?: boolean
+}>(), {
+ showRelationArrows: false,
+})
const emit = defineEmits<{
(e: 'update:task', task: ITaskPartialWithId): void
@@ -180,8 +192,8 @@ const visibleNodes = computed(() => {
return result
})
-// Used in Task 8 for arrow re-routing when children are collapsed
-const _hiddenToAncestor = computed(() => {
+// Map hidden tasks to their visible ancestor for arrow re-routing
+const hiddenToAncestor = computed(() => {
const map = new Map()
const hiddenParents = new Set()
@@ -323,6 +335,50 @@ watch(
{deep: true, immediate: true},
)
+// Compute bar positions for arrow rendering
+const ROW_HEIGHT = 40
+
+const barPositions = computed(() => {
+ const positions = new Map()
+
+ ganttBars.value.forEach((rowBars, rowIndex) => {
+ for (const bar of rowBars) {
+ const taskId = Number(bar.id)
+ const x = computeBarX(bar.start)
+ const width = computeBarWidth(bar)
+ const y = rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2
+
+ positions.set(taskId, {x, y, width, rowIndex})
+ }
+ })
+
+ return positions
+})
+
+function computeBarX(date: Date): number {
+ const diff = Math.ceil(
+ (roundToNaturalDayBoundary(date, true).getTime() - dateFromDate.value.getTime()) /
+ MILLISECONDS_A_DAY,
+ )
+ return diff * DAY_WIDTH_PIXELS
+}
+
+function computeBarWidth(bar: GanttBarModel): number {
+ const diff = Math.ceil(
+ (roundToNaturalDayBoundary(bar.end).getTime() - roundToNaturalDayBoundary(bar.start, true).getTime()) /
+ MILLISECONDS_A_DAY,
+ )
+ return diff * DAY_WIDTH_PIXELS
+}
+
+// Compute relation arrows
+const relationArrows = computed(() => {
+ if (!props.showRelationArrows) return []
+ return buildRelationArrows(tasks.value, barPositions.value, hiddenToAncestor.value)
+})
+
+const totalHeight = computed(() => ganttRows.value.length * ROW_HEIGHT)
+
function updateGanttTask(id: string, newStart: Date, newEnd: Date) {
const task = tasks.value.get(Number(id))
if (!task) return
diff --git a/frontend/src/components/project/views/ProjectGantt.vue b/frontend/src/components/project/views/ProjectGantt.vue
index dc613208a..5c37f6f12 100644
--- a/frontend/src/components/project/views/ProjectGantt.vue
+++ b/frontend/src/components/project/views/ProjectGantt.vue
@@ -38,6 +38,12 @@
>
{{ $t('task.show.noDates') }}
+
+ {{ $t('project.gantt.toggleRelationArrows') }}
+
@@ -53,6 +59,7 @@
:is-loading="isLoading"
:default-task-start-date="defaultTaskStartDate"
:default-task-end-date="defaultTaskEndDate"
+ :show-relation-arrows="showRelationArrows"
@update:task="updateTask"
/>