wip(task-timeline): stage changes

This commit is contained in:
Matej Glasnák committed 2025-12-07 22:36:57 +01:00
1 parent 4768986a25
commit b1771ead05
11 files changed
+2220 -682

No files matched your search

@@ -0,0 +1,237 @@
import { useQuery } from '@tanstack/vue-query'
import { computed, Ref, unref } from 'vue'
import axios from '@/plugins/w@custom/axios/models/axios'
import moment from 'moment'
import timelineUtils from '@/plugins/tm@dashboard/tasks/_utils/timeline.utils'
import {
detectCollision,
calculateTimeblockDuration,
getCollisionSegments,
type Timeblock,
type CollisionInfo,
} from './timeblock.utils'
interface Employee {
id: number
name: string
surname: string
work_start: number
work_end: number
}
export function useEmployeeTimeline(
employee: Ref<Employee | null>,
date: Ref<Date | string | null>,
currentTimeblock: Ref<Timeblock | null>
) {
// Fetch employee's tasks for the selected date
const { data: tasksData, isLoading, error, refetch } = useQuery({
queryKey: computed(() => [
'employee-timeline',
unref(employee)?.id,
unref(date) ? moment(unref(date)).format('YYYY-MM-DD') : null,
]),
queryFn: async () => {
const emp = unref(employee)
const d = unref(date)
if (!emp?.id || !d) {
return null
}
const dateStr = moment(d).format('YYYY-MM-DD')
const queryParams = new URLSearchParams({
start: dateStr,
end: dateStr,
employee_id: emp.id.toString(),
}).toString()
const response = await axios.get_auth(`v1/subtasks?${queryParams}`)
return response
},
enabled: computed(() => {
const emp = unref(employee)
const d = unref(date)
return !!(emp?.id && d)
}),
})
// Generate timeline tasks from fetched data
const timelineTasks = computed(() => {
const data = unref(tasksData)
const emp = unref(employee)
const d = unref(date)
if (!data || !emp || !d) {
return []
}
const subtasks = [...(data.subtasks || []), ...(data.blockers || [])]
const dateObj = new Date(d)
const selectedDateStr = moment(d).format('YYYY-MM-DD')
try {
// Process timeblocks directly instead of using timeline utils
// which is designed for multi-day views
const tasks: any[] = []
subtasks.forEach((subtask) => {
if (!['blocker', 'adhoc', 'home_office', 'vacation', 'free_day', 'office365_blocker'].includes(subtask.type)) {
subtask.timeblocks?.forEach((timeblock: any) => {
const timeblockDateStr = moment(timeblock.start).format('YYYY-MM-DD')
// Only include timeblocks for the selected date
if (timeblockDateStr === selectedDateStr) {
let type = 'subtask'
if (subtask.type === 'office365') type = 'office365'
tasks.push({
id: subtask.id,
start: new Date(timeblock.start),
end: new Date(timeblock.end),
master_task: subtask.master_task,
date: new Date(timeblock.start),
project_code: subtask.project_code,
subtask: subtask,
type,
_timeblock_id: timeblock.id,
_timeblock_is_finished: timeblock.is_finished,
_real_start: new Date(timeblock.start),
_real_end: new Date(timeblock.end),
})
}
})
}
})
return tasks
} catch (error) {
console.error('Error generating timeline tasks:', error)
return []
}
})
// Calculate scheduled time for the day (in hours)
const scheduledTime = computed(() => {
const tasks = unref(timelineTasks)
const d = unref(date)
const now = moment()
if (!tasks || !d) return 0
let totalMinutes = 0
tasks.forEach((task) => {
if (
task.type === 'non-working-hours' ||
task.subtask?.is_finished ||
task.blocker?.is_finished
) {
return
}
if (now.isAfter(moment(task._real_end))) {
// Ignore elapsed tasks
return
} else if (
now.isAfter(moment(task._real_start)) &&
now.isBefore(moment(task._real_end))
) {
// Task in progress
totalMinutes += timelineUtils._getDifferenceInMinutes(task._real_end, new Date())
} else {
// Future task
totalMinutes += timelineUtils._getDifferenceInMinutes(
task._real_end,
task._real_start
)
}
})
return (totalMinutes / 60).toFixed(1)
})
// Calculate free/unscheduled time for the day (in hours)
const freeTime = computed(() => {
const emp = unref(employee)
const d = unref(date)
const scheduled = parseFloat(String(unref(scheduledTime)))
if (!emp || !d) return '0'
const now = moment()
const dateObj = moment(d)
const dailyCapacityHours = emp.work_end - emp.work_start
if (dateObj.isBefore(now, 'days')) {
// Past date
return '0'
} else if (dateObj.isSame(now, 'days')) {
// Today - calculate remaining capacity
const elapsedMinutes = now.diff(now.clone().startOf('day'), 'minutes')
const workStart =
emp.work_start * 60 > elapsedMinutes ? emp.work_start : elapsedMinutes / 60
const todayCapacityHours = emp.work_end - workStart
const free = todayCapacityHours - scheduled
return free < 0 ? '0' : free.toFixed(1)
} else {
// Future date
const free = dailyCapacityHours - scheduled
return free < 0 ? '0' : free.toFixed(1)
}
})
// Detect collisions with current timeblock
const collision = computed<CollisionInfo>(() => {
const timeblock = unref(currentTimeblock)
const tasks = unref(timelineTasks)
if (!timeblock || !tasks) {
return {
hasCollision: false,
startCollides: false,
endCollides: false,
collidingTasks: [],
}
}
return detectCollision(timeblock, tasks)
})
// Get collision segments for timeline visualization
const collisionSegments = computed(() => {
const timeblock = unref(currentTimeblock)
const tasks = unref(timelineTasks)
if (!timeblock || !tasks) {
return []
}
return getCollisionSegments(timeblock, tasks)
})
// Calculate current timeblock duration
const timeblockDuration = computed(() => {
const timeblock = unref(currentTimeblock)
if (!timeblock) return 0
return calculateTimeblockDuration(timeblock)
})
return {
// Query states
isLoading,
error,
refetch,
// Timeline data
timelineTasks,
scheduledTime,
freeTime,
// Collision detection
collision,
collisionSegments,
timeblockDuration,
}
}