Files
panel/resources/js/pages/dashboards/gantt.vue

1261 lines
37 KiB
Vue
Raw Normal View History

2025-08-04 16:33:07 +03:30
<script setup>
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
// Reactive data
const ganttContainer = ref(null)
const isLoading = ref(true)
const selectedTask = ref(null)
const showTaskDetails = ref(false)
const showResourcePanel = ref(false)
const showFilterPanel = ref(false)
const currentView = ref('month')
const searchQuery = ref('')
const snackbar = ref(false)
const snackbarText = ref('')
// Sample data with proper date format
const projectData = ref({
tasks: {
data: [
{ id: 1, text: "Software Development Project", start_date: "01-01-2024", duration: 30, progress: 0.6, open: true, priority: "high", resource: "team1" },
{ id: 2, text: "Requirements Analysis", start_date: "01-01-2024", duration: 5, progress: 0.4, parent: 1, priority: "medium", resource: "analyst1" },
{ id: 3, text: "Architecture Design", start_date: "06-01-2024", duration: 7, progress: 0.6, parent: 1, priority: "high", resource: "architect1" },
{ id: 4, text: "Frontend Development", start_date: "13-01-2024", duration: 10, progress: 0.8, parent: 1, priority: "medium", resource: "developer1" },
{ id: 5, text: "Backend Development", start_date: "13-01-2024", duration: 12, progress: 0.7, parent: 1, priority: "medium", resource: "developer2" },
{ id: 6, text: "Testing & Debugging", start_date: "25-01-2024", duration: 5, progress: 0.3, parent: 1, priority: "low", resource: "tester1" },
{ id: 7, text: "Documentation", start_date: "30-01-2024", duration: 3, progress: 0.1, parent: 1, priority: "low", resource: "writer1" },
{ id: 8, text: "Marketing Campaign", start_date: "15-01-2024", duration: 20, progress: 0.5, open: true, priority: "medium", resource: "team2" },
{ id: 9, text: "Market Research", start_date: "15-01-2024", duration: 5, progress: 0.8, parent: 8, priority: "high", resource: "researcher1" },
{ id: 10, text: "Campaign Design", start_date: "20-01-2024", duration: 7, progress: 0.6, parent: 8, priority: "medium", resource: "designer1" },
{ id: 11, text: "Campaign Execution", start_date: "27-01-2024", duration: 8, progress: 0.2, parent: 8, priority: "medium", resource: "marketer1" }
],
links: [
{ id: 1, source: 2, target: 3, type: "0" },
{ id: 2, source: 3, target: 4, type: "0" },
{ id: 3, source: 3, target: 5, type: "0" },
{ id: 4, source: 4, target: 6, type: "0" },
{ id: 5, source: 5, target: 6, type: "0" },
{ id: 6, source: 6, target: 7, type: "0" },
{ id: 7, source: 9, target: 10, type: "0" },
{ id: 8, source: 10, target: 11, type: "0" }
]
},
resources: [
{ id: "team1", name: "Development Team 1", capacity: 100 },
{ id: "team2", name: "Marketing Team", capacity: 80 },
{ id: "analyst1", name: "Senior Analyst", capacity: 100 },
{ id: "architect1", name: "Software Architect", capacity: 100 },
{ id: "developer1", name: "Frontend Developer", capacity: 100 },
{ id: "developer2", name: "Backend Developer", capacity: 100 },
{ id: "tester1", name: "QA Tester", capacity: 100 },
{ id: "writer1", name: "Technical Writer", capacity: 100 },
{ id: "researcher1", name: "Market Researcher", capacity: 100 },
{ id: "designer1", name: "Graphic Designer", capacity: 100 },
{ id: "marketer1", name: "Marketing Specialist", capacity: 100 }
]
})
// Computed properties
const filteredTasks = computed(() => {
if (!searchQuery.value) return projectData.value.tasks.data
return projectData.value.tasks.data.filter(task =>
task.text.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const priorityOptions = [
{ value: 'high', label: 'High', color: 'error', icon: 'tabler-arrow-up' },
{ value: 'medium', label: 'Medium', color: 'warning', icon: 'tabler-arrow-right' },
{ value: 'low', label: 'Low', color: 'success', icon: 'tabler-arrow-down' }
]
const viewOptions = [
{ value: 'day', label: 'Daily', icon: 'tabler-calendar' },
{ value: 'week', label: 'Weekly', icon: 'tabler-calendar-week' },
{ value: 'month', label: 'Monthly', icon: 'tabler-calendar-month' },
{ value: 'quarter', label: 'Quarterly', icon: 'tabler-calendar-time' },
{ value: 'year', label: 'Yearly', icon: 'tabler-calendar-stats' }
]
// Watch for search changes
watch(searchQuery, (newValue) => {
performSearch()
})
// Gantt configuration
const initGantt = () => {
// Set date format
gantt.config.date_format = "%d-%m-%Y"
gantt.config.xml_date = "%d-%m-%Y"
// Configure scale
gantt.config.scale_unit = currentView.value
gantt.config.date_scale = "%F %Y"
// Configure subscales based on view
updateViewConfig(currentView.value)
// Enable features
gantt.config.drag_progress = true
gantt.config.drag_resize = true
gantt.config.drag_move = true
gantt.config.drag_links = true
gantt.config.show_links = true
gantt.config.show_progress = true
gantt.config.show_chart = true
gantt.config.show_grid = true
gantt.config.auto_scheduling = false
gantt.config.auto_scheduling_strict = false
// Prevent task overlapping issues
gantt.config.work_time = true
gantt.config.correct_work_time = true
// Custom columns
gantt.config.columns = [
{ name: "text", label: "Task Name", width: 200, tree: true },
{
name: "start_date",
label: "Start Date",
align: "center",
width: 100,
template: function(obj) {
if (!obj.start_date) return ""
try {
return gantt.date.date_to_str("%d/%m/%Y")(gantt.date.str_to_date("%d-%m-%Y")(obj.start_date))
} catch (e) {
return obj.start_date
}
}
},
{ name: "duration", label: "Duration (Days)", align: "center", width: 80 },
{
name: "progress",
label: "Progress",
align: "center",
width: 100,
template: function(obj) {
return Math.round((obj.progress || 0) * 100) + "%"
}
},
{
name: "priority",
label: "Priority",
align: "center",
width: 100,
template: function(obj) {
const priority = priorityOptions.find(p => p.value === obj.priority)
if (!priority) return obj.priority || ''
return `<span style="color: var(--v-theme-${priority.color}); font-weight: bold;">${priority.label}</span>`
}
},
{
name: "resource",
label: "Resource",
align: "center",
width: 120,
template: function(obj) {
const resource = projectData.value.resources.find(r => r.id === obj.resource)
return resource ? resource.name : (obj.resource || '')
}
}
]
// Templates
gantt.templates.task_text = function(start, end, task) {
return `<span title='${task.text || ""}'>${task.text || ""}</span>`
}
gantt.templates.progress_text = function(start, end, task) {
return Math.round((task.progress || 0) * 100) + "%"
}
gantt.templates.task_class = function(start, end, task) {
let classes = []
if (task.priority === 'high') classes.push('high-priority')
if (task.priority === 'medium') classes.push('medium-priority')
if (task.priority === 'low') classes.push('low-priority')
classes.push('gantt-task-rounded')
return classes.join(' ')
}
gantt.templates.tooltip_text = function(start, end, task) {
const resource = projectData.value.resources.find(r => r.id === task.resource)
const startDate = task.start_date || "Not set"
const duration = task.duration || 0
const progress = Math.round((task.progress || 0) * 100)
const priority = task.priority || "Not set"
const resourceName = resource ? resource.name : (task.resource || "Not assigned")
return `<b>${task.text || "Unnamed Task"}</b><br/>
Start: ${startDate}<br/>
Duration: ${duration} days<br/>
Progress: ${progress}%<br/>
Priority: ${priority}<br/>
Resource: ${resourceName}`
}
// Events
gantt.attachEvent("onTaskClick", function(id, e) {
try {
const task = gantt.getTask(id)
if (task) {
selectedTask.value = { ...task } // Create a copy to avoid direct mutation
showTaskDetails.value = true
}
} catch (error) {
console.error("Error selecting task:", error)
showSnackbar('Error selecting task', 'error')
}
return true
})
gantt.attachEvent("onAfterTaskUpdate", function(id, task) {
showSnackbar('Task updated successfully!')
})
gantt.attachEvent("onAfterLinkAdd", function(id, link) {
showSnackbar('Link added!')
})
gantt.attachEvent("onAfterTaskAdd", function(id, task) {
showSnackbar('Task added!')
})
gantt.attachEvent("onAfterTaskDelete", function(id) {
showSnackbar('Task deleted!')
if (selectedTask.value && selectedTask.value.id === id) {
selectedTask.value = null
showTaskDetails.value = false
}
})
// Error handling
gantt.attachEvent("onTaskDragEnd", function(id, mode, e) {
try {
gantt.updateTask(id)
return true
} catch (error) {
console.error("Error updating task:", error)
showSnackbar('Error updating task', 'error')
return false
}
})
// Initialize
try {
gantt.init(ganttContainer.value)
gantt.parse(projectData.value)
isLoading.value = false
} catch (error) {
console.error("Error initializing Gantt:", error)
showSnackbar('Error initializing Gantt chart', 'error')
isLoading.value = false
}
}
// Helper function for view configuration
const updateViewConfig = (view) => {
gantt.config.scale_unit = view
// Clear existing subscales
gantt.config.subscales = []
// Configure subscales based on view
switch (view) {
case 'day':
gantt.config.date_scale = "%d %F %Y"
gantt.config.subscales = [
{ unit: "hour", step: 6, date: "%H:00" }
]
break
case 'week':
gantt.config.date_scale = "Week %W, %F %Y"
gantt.config.subscales = [
{ unit: "day", step: 1, date: "%d %M" }
]
break
case 'month':
gantt.config.date_scale = "%F %Y"
gantt.config.subscales = [
{ unit: "week", step: 1, date: "Week %W" }
]
break
case 'quarter':
gantt.config.date_scale = "%Y"
gantt.config.subscales = [
{ unit: "month", step: 1, date: "%F" }
]
break
case 'year':
gantt.config.date_scale = "%Y"
gantt.config.subscales = [
{ unit: "quarter", step: 1, date: "Q%q" }
]
break
}
}
// Helper function for snackbar
const showSnackbar = (message, type = 'success') => {
snackbarText.value = message
snackbar.value = true
}
// Task management functions
const addTask = () => {
try {
const today = new Date()
const formattedDate = gantt.date.date_to_str("%d-%m-%Y")(today)
const newTask = {
id: gantt.uid(),
text: "New Task",
start_date: formattedDate,
duration: 1,
progress: 0,
priority: "medium",
resource: projectData.value.resources[0]?.id || ""
}
gantt.addTask(newTask)
} catch (error) {
console.error("Error adding task:", error)
showSnackbar('Error adding task', 'error')
}
}
const deleteTask = () => {
if (selectedTask.value) {
try {
gantt.deleteTask(selectedTask.value.id)
selectedTask.value = null
showTaskDetails.value = false
} catch (error) {
console.error("Error deleting task:", error)
showSnackbar('Error deleting task', 'error')
}
}
}
const updateTask = (task) => {
try {
if (!task || !task.id) {
throw new Error("Invalid task data")
}
// Validate required fields
if (!task.text || task.text.trim() === '') {
task.text = "Unnamed Task"
}
if (!task.start_date) {
const today = new Date()
task.start_date = gantt.date.date_to_str("%d-%m-%Y")(today)
}
if (!task.duration || task.duration < 1) {
task.duration = 1
}
if (task.progress < 0) task.progress = 0
if (task.progress > 1) task.progress = 1
gantt.updateTask(task.id, task)
// Update selectedTask to reflect changes
selectedTask.value = { ...task }
showTaskDetails.value = false
showSnackbar('Task updated successfully!')
} catch (error) {
console.error("Error updating task:", error)
showSnackbar('Error updating task', 'error')
}
}
// View management
const changeView = (view) => {
try {
currentView.value = view
updateViewConfig(view)
gantt.render()
showSnackbar(`View changed to ${view}`)
} catch (error) {
console.error("Error changing view:", error)
showSnackbar('Error changing view', 'error')
}
}
// Export functions with error handling
const exportToPDF = () => {
try {
if (gantt.exportToPDF) {
gantt.exportToPDF({
name: "project-gantt.pdf",
header: "<h1>Project Gantt Chart</h1>",
footer: "<div>Generated on " + new Date().toLocaleDateString('en-US') + "</div>"
})
showSnackbar('PDF export started')
} else {
showSnackbar('PDF export not available', 'error')
}
} catch (error) {
console.error("Error exporting to PDF:", error)
showSnackbar('Error exporting to PDF', 'error')
}
}
const exportToPNG = () => {
try {
if (gantt.exportToPNG) {
gantt.exportToPNG({
name: "project-gantt.png"
})
showSnackbar('PNG export started')
} else {
showSnackbar('PNG export not available', 'error')
}
} catch (error) {
console.error("Error exporting to PNG:", error)
showSnackbar('Error exporting to PNG', 'error')
}
}
const exportToExcel = () => {
try {
const data = gantt.getTaskByTime()
// Create a simple CSV export as fallback
const csvContent = "data:text/csv;charset=utf-8," +
"Task Name,Start Date,Duration,Progress,Priority,Resource\n" +
data.map(task => {
const resource = projectData.value.resources.find(r => r.id === task.resource)
return `"${task.text}","${task.start_date}","${task.duration}","${Math.round((task.progress || 0) * 100)}%","${task.priority}","${resource ? resource.name : task.resource || ''}"`
}).join("\n")
const encodedUri = encodeURI(csvContent)
const link = document.createElement("a")
link.setAttribute("href", encodedUri)
link.setAttribute("download", "project-gantt.csv")
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
showSnackbar('Excel/CSV export completed')
} catch (error) {
console.error("Error exporting to Excel:", error)
showSnackbar('Error exporting to Excel', 'error')
}
}
// Search functionality with error handling
const performSearch = () => {
try {
if (!gantt.getTaskByTime) {
console.warn("Gantt not fully initialized for search")
return
}
if (searchQuery.value && searchQuery.value.trim() !== '') {
const tasks = gantt.getTaskByTime()
const filtered = tasks.filter(task =>
task.text && task.text.toLowerCase().includes(searchQuery.value.toLowerCase())
)
// Clear previous highlights
gantt.eachTask(function(task) {
gantt.removeTaskClass(task.id, "search-highlight")
})
// Highlight filtered tasks
filtered.forEach(task => {
gantt.addTaskClass(task.id, "search-highlight")
})
if (filtered.length === 0) {
showSnackbar('No tasks found matching search criteria', 'warning')
}
} else {
// Clear all highlights when search is empty
gantt.eachTask(function(task) {
gantt.removeTaskClass(task.id, "search-highlight")
})
}
} catch (error) {
console.error("Error performing search:", error)
showSnackbar('Error performing search', 'error')
}
}
// Close task details when clicking outside
const closeTaskDetails = () => {
showTaskDetails.value = false
selectedTask.value = null
}
// Resource utilization calculation
const getResourceUtilization = (resourceId) => {
try {
const tasksForResource = projectData.value.tasks.data.filter(task => task.resource === resourceId)
if (tasksForResource.length === 0) return 0
const totalProgress = tasksForResource.reduce((sum, task) => sum + (task.progress || 0), 0)
const avgProgress = totalProgress / tasksForResource.length
return Math.round(avgProgress * 100)
} catch (error) {
console.error("Error calculating resource utilization:", error)
return 0
}
}
// Filter functionality
const filterOptions = ref({
priority: '',
resource: '',
progressRange: [0, 100]
})
const applyFilters = () => {
try {
const { priority, resource, progressRange } = filterOptions.value
gantt.eachTask(function(task) {
let shouldShow = true
// Filter by priority
if (priority && task.priority !== priority) {
shouldShow = false
}
// Filter by resource
if (resource && task.resource !== resource) {
shouldShow = false
}
// Filter by progress
const taskProgress = Math.round((task.progress || 0) * 100)
if (taskProgress < progressRange[0] || taskProgress > progressRange[1]) {
shouldShow = false
}
// Hide/show task
if (shouldShow) {
gantt.showTask(task.id)
} else {
gantt.hideTask(task.id)
}
})
gantt.render()
showFilterPanel.value = false
showSnackbar('Filters applied successfully')
} catch (error) {
console.error("Error applying filters:", error)
showSnackbar('Error applying filters', 'error')
}
}
// Clear all filters
const clearFilters = () => {
try {
filterOptions.value = {
priority: '',
resource: '',
progressRange: [0, 100]
}
gantt.eachTask(function(task) {
gantt.showTask(task.id)
})
gantt.render()
showSnackbar('Filters cleared')
} catch (error) {
console.error("Error clearing filters:", error)
showSnackbar('Error clearing filters', 'error')
}
}
// Lifecycle
onMounted(async () => {
try {
await nextTick()
if (ganttContainer.value) {
initGantt()
} else {
throw new Error("Gantt container not found")
}
} catch (error) {
console.error("Error mounting Gantt:", error)
isLoading.value = false
showSnackbar('Error initializing Gantt chart', 'error')
}
})
onUnmounted(() => {
try {
if (gantt && gantt.clearAll) {
gantt.clearAll()
}
} catch (error) {
console.error("Error cleaning up Gantt:", error)
}
})
</script>
<template>
<VCard class="gantt-card">
<VCardTitle class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<VIcon
icon="tabler-chart-timeline"
size="24"
class="me-3"
color="primary"
/>
<span class="text-h5 font-weight-medium">Project Gantt Chart</span>
</div>
<div class="d-flex gap-2">
<VBtn
color="primary"
prepend-icon="tabler-plus"
@click="addTask"
variant="flat"
rounded="lg"
size="small"
class="me-1"
title="Add New Task"
>
Add Task
</VBtn>
<VBtn
color="error"
variant="flat"
prepend-icon="tabler-trash"
:disabled="!selectedTask"
@click="deleteTask"
rounded="lg"
size="small"
class="me-1"
title="Delete Selected Task"
>
Delete Task
</VBtn>
<VMenu>
<template #activator="{ props }">
<VBtn
color="secondary"
variant="flat"
prepend-icon="tabler-download"
v-bind="props"
rounded="lg"
size="small"
class="me-1"
title="Export Gantt Chart"
>
Export
</VBtn>
</template>
<VList>
<VListItem @click="exportToPDF" :disabled="!gantt.exportToPDF">
<template #prepend>
<VIcon>tabler-file-type-pdf</VIcon>
</template>
<VListItemTitle>PDF</VListItemTitle>
</VListItem>
<VListItem @click="exportToPNG" :disabled="!gantt.exportToPNG">
<template #prepend>
<VIcon>tabler-file-type-png</VIcon>
</template>
<VListItemTitle>PNG</VListItemTitle>
</VListItem>
<VListItem @click="exportToExcel">
<template #prepend>
<VIcon>tabler-file-spreadsheet</VIcon>
</template>
<VListItemTitle>Excel/CSV</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</div>
</VCardTitle>
<VDivider />
<!-- Toolbar -->
<VCardText class="py-4">
<VRow>
<VCol cols="12" md="3">
<VTextField
v-model="searchQuery"
label="Search Tasks"
prepend-inner-icon="tabler-search"
variant="outlined"
density="compact"
clearable
class="me-1"
title="Search for tasks by name"
/>
</VCol>
<VCol cols="12" md="3">
<VSelect
v-model="currentView"
:items="viewOptions"
item-title="label"
item-value="value"
label="View"
variant="outlined"
density="compact"
@update:model-value="changeView"
class="me-1"
title="Change Gantt chart view"
>
<template #item="{ props, item }">
<VListItem v-bind="props">
<template #prepend>
<VIcon :icon="item.raw.icon" />
</template>
</VListItem>
</template>
</VSelect>
</VCol>
<VCol cols="12" md="3">
<VBtn
color="info"
variant="flat"
prepend-icon="tabler-filter"
@click="showFilterPanel = !showFilterPanel"
rounded="lg"
size="small"
class="me-1"
title="Filter tasks"
>
Filter
</VBtn>
</VCol>
<VCol cols="12" md="3">
<VBtn
color="success"
variant="flat"
prepend-icon="tabler-users"
@click="showResourcePanel = !showResourcePanel"
rounded="lg"
size="small"
class="me-1"
title="Manage resources"
>
Resources
</VBtn>
</VCol>
</VRow>
</VCardText>
<VDivider />
<VCardText class="pa-0">
<div class="d-flex position-relative">
<!-- Main Gantt Chart -->
<div class="flex-grow-1">
<div
ref="ganttContainer"
class="gantt-container"
style="height: 600px; width: 100%;"
/>
<VOverlay
v-model="isLoading"
contained
persistent
class="align-center justify-center"
>
<VProgressCircular
indeterminate
color="primary"
size="64"
/>
</VOverlay>
</div>
<!-- Task Details Sidebar -->
<VNavigationDrawer
v-model="showTaskDetails"
location="end"
temporary
width="400"
class="gantt-sidebar"
>
<VCardTitle class="d-flex align-center justify-space-between">
<span>Task Details</span>
<VBtn
icon="tabler-x"
variant="flat"
@click="closeTaskDetails"
rounded="lg"
size="small"
title="Close task details"
/>
</VCardTitle>
<VDivider />
<VCardText v-if="selectedTask">
<VForm @submit.prevent="updateTask(selectedTask)">
<VRow>
<VCol cols="12">
<VTextField
v-model="selectedTask.text"
label="Task Name"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Task name is required']"
title="Edit task name"
/>
</VCol>
<VCol cols="6">
<VTextField
v-model="selectedTask.start_date"
label="Start Date"
type="date"
variant="outlined"
density="compact"
title="Edit task start date"
/>
</VCol>
<VCol cols="6">
<VTextField
v-model.number="selectedTask.duration"
label="Duration (Days)"
type="number"
min="1"
variant="outlined"
density="compact"
:rules="[v => v > 0 || 'Duration must be positive']"
title="Edit task duration"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="selectedTask.priority"
:items="priorityOptions"
item-title="label"
item-value="value"
label="Priority"
variant="outlined"
density="compact"
title="Edit task priority"
>
<template #item="{ props, item }">
<VListItem v-bind="props">
<template #prepend>
<VIcon :icon="item.raw.icon" :color="item.raw.color" />
</template>
</VListItem>
</template>
</VSelect>
</VCol>
<VCol cols="12">
<VSelect
v-model="selectedTask.resource"
:items="projectData.resources"
item-title="name"
item-value="id"
label="Resource"
variant="outlined"
density="compact"
title="Edit task resource"
/>
</VCol>
<VCol cols="12">
<div class="mb-2">
<VLabel>Progress: {{ Math.round((selectedTask.progress || 0) * 100) }}%</VLabel>
</div>
<VSlider
v-model="selectedTask.progress"
min="0"
max="1"
step="0.1"
thumb-label
color="primary"
title="Edit task progress"
>
<template #thumb-label="{ modelValue }">
{{ Math.round(modelValue * 100) }}%
</template>
</VSlider>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
color="primary"
block
variant="flat"
rounded="lg"
prepend-icon="tabler-check"
title="Save task changes"
>
Save Changes
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VNavigationDrawer>
<!-- Resource Panel -->
<VNavigationDrawer
v-model="showResourcePanel"
location="start"
temporary
width="300"
class="gantt-sidebar"
>
<VCardTitle class="d-flex align-center justify-space-between">
<span>Resource Management</span>
<VBtn
icon="tabler-x"
variant="flat"
@click="showResourcePanel = false"
rounded="lg"
size="small"
title="Close resource panel"
/>
</VCardTitle>
<VDivider />
<VCardText>
<VList>
<VListItem
v-for="resource in projectData.resources"
:key="resource.id"
:title="resource.name"
:subtitle="`Capacity: ${resource.capacity}%`"
class="gantt-resource-item"
>
<template #prepend>
<VAvatar color="primary" size="small">
<VIcon>tabler-user</VIcon>
</VAvatar>
</template>
<template #append>
<VChip
:color="getResourceUtilization(resource.id) > 80 ? 'error' : 'success'"
size="small"
variant="flat"
>
{{ getResourceUtilization(resource.id) }}%
</VChip>
</template>
</VListItem>
</VList>
</VCardText>
</VNavigationDrawer>
<!-- Filter Panel -->
<VDialog v-model="showFilterPanel" max-width="500">
<VCard>
<VCardTitle class="d-flex align-center justify-space-between">
<span>Filter Tasks</span>
<VBtn
icon="tabler-x"
variant="flat"
@click="showFilterPanel = false"
rounded="lg"
size="small"
title="Close filter panel"
/>
</VCardTitle>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
v-model="filterOptions.priority"
label="Filter by Priority"
:items="[{ value: '', label: 'All Priorities' }, ...priorityOptions]"
item-title="label"
item-value="value"
variant="outlined"
density="compact"
clearable
>
<template #item="{ props, item }">
<VListItem v-bind="props">
<template #prepend v-if="item.raw.icon">
<VIcon :icon="item.raw.icon" :color="item.raw.color" />
</template>
</VListItem>
</template>
</VSelect>
</VCol>
<VCol cols="12">
<VSelect
v-model="filterOptions.resource"
label="Filter by Resource"
:items="[{ id: '', name: 'All Resources' }, ...projectData.resources]"
item-title="name"
item-value="id"
variant="outlined"
density="compact"
clearable
/>
</VCol>
<VCol cols="12">
<div class="mb-2">
<VLabel>Progress Range: {{ filterOptions.progressRange[0] }}% - {{ filterOptions.progressRange[1] }}%</VLabel>
</div>
<VRangeSlider
v-model="filterOptions.progressRange"
label="Progress Range"
min="0"
max="100"
step="10"
thumb-label
color="primary"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VBtn @click="clearFilters" variant="text" color="secondary">
Clear Filters
</VBtn>
<VSpacer />
<VBtn @click="showFilterPanel = false" variant="text">Cancel</VBtn>
<VBtn color="primary" @click="applyFilters" variant="flat">Apply Filters</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</VCardText>
</VCard>
<VSnackbar
v-model="snackbar"
:color="snackbarText.includes('Error') ? 'error' : snackbarText.includes('Warning') ? 'warning' : 'success'"
location="bottom"
variant="flat"
rounded="lg"
timeout="4000"
>
<template #default>
<div class="d-flex align-center">
<VIcon
:icon="snackbarText.includes('Error') ? 'tabler-alert-circle' :
snackbarText.includes('Warning') ? 'tabler-alert-triangle' :
'tabler-check'"
class="me-2"
/>
{{ snackbarText }}
</div>
</template>
<template #actions>
<VBtn
icon="tabler-x"
variant="text"
@click="snackbar = false"
size="small"
/>
</template>
</VSnackbar>
</template>
<style lang="scss" scoped>
.gantt-container {
:deep(.gantt_grid) {
background-color: rgb(var(--v-theme-surface));
border-right: 1px solid rgb(var(--v-theme-outline));
}
:deep(.gantt_task) {
background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary));
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
:deep(.gantt_task_progress) {
background-color: rgb(var(--v-theme-success));
border-radius: 0 8px 8px 0;
}
:deep(.gantt_task_line) {
border-color: rgb(var(--v-theme-primary));
background-color: rgb(var(--v-theme-primary));
}
:deep(.gantt_task_line .gantt_task_progress) {
background-color: rgb(var(--v-theme-success));
}
:deep(.gantt_row) {
background-color: rgb(var(--v-theme-surface));
border-bottom: 1px solid rgb(var(--v-theme-outline));
height: 40px; /* Increased row height */
align-items: center;
}
:deep(.gantt_row.gantt_selected) {
background-color: rgb(var(--v-theme-primary-container));
}
:deep(.gantt_cell) {
border-right: 1px solid rgb(var(--v-theme-outline));
}
:deep(.gantt_scale_cell) {
background-color: rgb(var(--v-theme-surface-variant));
border-right: 1px solid rgb(var(--v-theme-outline));
color: rgb(var(--v-theme-on-surface-variant));
}
:deep(.gantt_grid_head_cell) {
background-color: rgb(var(--v-theme-surface-variant));
border-right: 1px solid rgb(var(--v-theme-outline));
color: rgb(var(--v-theme-on-surface-variant));
font-weight: 600;
}
:deep(.gantt_link_control) {
background-color: rgb(var(--v-theme-secondary));
}
:deep(.gantt_link_point) {
background-color: rgb(var(--v-theme-secondary));
}
:deep(.gantt_drag_marker) {
background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary));
}
:deep(.gantt_drag_progress) {
background-color: rgb(var(--v-theme-success));
}
:deep(.gantt_drag_link) {
background-color: rgb(var(--v-theme-secondary));
}
:deep(.gantt_row_odd) {
background-color: rgb(var(--v-theme-surface));
}
:deep(.gantt_row_even) {
background-color: rgb(var(--v-theme-surface-variant));
}
:deep(.gantt_tree_icon) {
color: rgb(var(--v-theme-on-surface));
}
:deep(.gantt_tree_content) {
color: rgb(var(--v-theme-on-surface));
}
:deep(.gantt_tree_icon.gantt_open) {
color: rgb(var(--v-theme-primary));
}
:deep(.gantt_tree_icon.gantt_close) {
color: rgb(var(--v-theme-primary));
}
// Priority styling
:deep(.high-priority) {
background-color: rgb(var(--v-theme-error)) !important;
border-color: rgb(var(--v-theme-error)) !important;
}
:deep(.medium-priority) {
background-color: rgb(var(--v-theme-warning)) !important;
border-color: rgb(var(--v-theme-warning)) !important;
}
:deep(.low-priority) {
background-color: rgb(var(--v-theme-success)) !important;
border-color: rgb(var(--v-theme-success)) !important;
}
// Search highlight
:deep(.search-highlight) {
background-color: rgb(var(--v-theme-secondary)) !important;
border-color: rgb(var(--v-theme-secondary)) !important;
}
}
// RTL Support
:deep(.gantt_grid) {
direction: rtl;
}
:deep(.gantt_task) {
direction: ltr;
}
:deep(.gantt_scale) {
direction: ltr;
}
.gantt-sidebar {
.v-navigation-drawer__content {
padding: 0;
}
.v-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.v-list-item {
border-radius: 8px;
margin: 4px 8px;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
}
}
.gantt-resource-item {
border-radius: 8px;
margin: 4px 8px;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
}
}
}
.gantt-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.gantt-task-rounded {
border-radius: 8px;
}
</style>