2025-08-30 16:44:46 +03:30
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
2025-08-04 16:33:07 +03:30
|
|
|
|
import { gantt } from 'dhtmlx-gantt'
|
|
|
|
|
|
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
|
|
|
|
|
|
2025-08-30 16:44:46 +03:30
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
initialTasks: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
data: [],
|
|
|
|
|
|
links: []
|
|
|
|
|
|
})
|
2025-08-25 12:19:58 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
config: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const ganttContainer = ref(null)
|
|
|
|
|
|
const showDeleteModal = ref(false)
|
|
|
|
|
|
const showEditModal = ref(false)
|
|
|
|
|
|
const showAddModal = ref(false)
|
|
|
|
|
|
const currentTaskId = ref(null)
|
|
|
|
|
|
const currentTask = ref(null)
|
|
|
|
|
|
const taskForm = ref({
|
|
|
|
|
|
text: '',
|
|
|
|
|
|
start_date: '',
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tasks = props.initialTasks.data.length > 0 ? props.initialTasks : {
|
|
|
|
|
|
data: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 1,
|
|
|
|
|
|
text: "Office Itinerary",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 17,
|
|
|
|
|
|
progress: 0.95,
|
|
|
|
|
|
open: true
|
2025-08-04 16:33:07 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 11,
|
|
|
|
|
|
text: "Office facing",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 5,
|
|
|
|
|
|
parent: 1,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 111,
|
|
|
|
|
|
text: "Interior office",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
parent: 11,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 112,
|
|
|
|
|
|
text: "Air conditioner check",
|
|
|
|
|
|
start_date: "05-08-2025",
|
|
|
|
|
|
duration: 2,
|
|
|
|
|
|
parent: 11,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 12,
|
|
|
|
|
|
text: "Furniture installation",
|
|
|
|
|
|
start_date: "08-08-2025",
|
|
|
|
|
|
duration: 2,
|
|
|
|
|
|
parent: 1,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 13,
|
|
|
|
|
|
text: "Employee relocation",
|
|
|
|
|
|
start_date: "10-08-2025",
|
|
|
|
|
|
duration: 8,
|
|
|
|
|
|
parent: 1,
|
|
|
|
|
|
progress: 0.67,
|
|
|
|
|
|
open: true
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 131,
|
|
|
|
|
|
text: "Preparing workplaces",
|
|
|
|
|
|
start_date: "10-08-2025",
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
parent: 13,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 132,
|
|
|
|
|
|
text: "Workplaces importation",
|
|
|
|
|
|
start_date: "13-08-2025",
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
parent: 13,
|
|
|
|
|
|
progress: 1
|
2025-08-04 16:33:07 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 133,
|
|
|
|
|
|
text: "Workplaces exportation",
|
|
|
|
|
|
start_date: "16-08-2025",
|
|
|
|
|
|
duration: 2,
|
|
|
|
|
|
parent: 13,
|
|
|
|
|
|
progress: 0
|
2025-08-25 12:19:58 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 2,
|
|
|
|
|
|
text: "Product launch",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 18,
|
|
|
|
|
|
progress: 0.73,
|
|
|
|
|
|
open: true
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 21,
|
|
|
|
|
|
text: "Perform initial testing",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 5,
|
|
|
|
|
|
parent: 2,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 22,
|
|
|
|
|
|
text: "Development",
|
|
|
|
|
|
start_date: "03-08-2025",
|
|
|
|
|
|
duration: 16,
|
|
|
|
|
|
parent: 2,
|
|
|
|
|
|
progress: 0.68,
|
|
|
|
|
|
open: true
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 221,
|
|
|
|
|
|
text: "Develop System",
|
|
|
|
|
|
start_date: "03-08-2025",
|
|
|
|
|
|
duration: 5,
|
|
|
|
|
|
parent: 22,
|
|
|
|
|
|
progress: 1
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 222,
|
|
|
|
|
|
text: "Beta Release",
|
|
|
|
|
|
start_date: "08-08-2025",
|
|
|
|
|
|
duration: 1,
|
|
|
|
|
|
parent: 22,
|
|
|
|
|
|
progress: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 223,
|
|
|
|
|
|
text: "Integrate System",
|
|
|
|
|
|
start_date: "09-08-2025",
|
|
|
|
|
|
duration: 4,
|
|
|
|
|
|
parent: 22,
|
|
|
|
|
|
progress: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 224,
|
|
|
|
|
|
text: "Test",
|
|
|
|
|
|
start_date: "13-08-2025",
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
parent: 22,
|
|
|
|
|
|
progress: 0.67
|
2025-08-26 16:03:11 +03:30
|
|
|
|
},
|
2025-08-30 16:44:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
id: 225,
|
|
|
|
|
|
text: "Marketing",
|
|
|
|
|
|
start_date: "16-08-2025",
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
parent: 22,
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 23,
|
|
|
|
|
|
text: "Analysis",
|
|
|
|
|
|
start_date: "01-08-2025",
|
|
|
|
|
|
duration: 4,
|
|
|
|
|
|
parent: 2,
|
|
|
|
|
|
progress: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 24,
|
|
|
|
|
|
text: "Design",
|
|
|
|
|
|
start_date: "06-08-2025",
|
|
|
|
|
|
duration: 6,
|
|
|
|
|
|
parent: 2,
|
|
|
|
|
|
progress: 0.75,
|
|
|
|
|
|
open: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 241,
|
|
|
|
|
|
text: "Design database",
|
|
|
|
|
|
start_date: "06-08-2025",
|
|
|
|
|
|
duration: 4,
|
|
|
|
|
|
parent: 24,
|
|
|
|
|
|
progress: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 242,
|
|
|
|
|
|
text: "Software design",
|
|
|
|
|
|
start_date: "10-08-2025",
|
|
|
|
|
|
duration: 2,
|
|
|
|
|
|
parent: 24,
|
|
|
|
|
|
progress: 0.5
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
links: [
|
|
|
|
|
|
{ id: 1, source: 111, target: 112, type: "0" },
|
|
|
|
|
|
{ id: 2, source: 112, target: 12, type: "0" },
|
|
|
|
|
|
{ id: 3, source: 12, target: 13, type: "0" },
|
|
|
|
|
|
{ id: 4, source: 131, target: 132, type: "0" },
|
|
|
|
|
|
{ id: 5, source: 132, target: 133, type: "0" },
|
|
|
|
|
|
{ id: 6, source: 221, target: 222, type: "0" },
|
|
|
|
|
|
{ id: 7, source: 222, target: 223, type: "0" },
|
|
|
|
|
|
{ id: 8, source: 223, target: 224, type: "0" },
|
|
|
|
|
|
{ id: 9, source: 224, target: 225, type: "0" },
|
|
|
|
|
|
{ id: 10, source: 23, target: 24, type: "0" },
|
|
|
|
|
|
{ id: 11, source: 241, target: 242, type: "0" }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const initGantt = () => {
|
|
|
|
|
|
gantt.config.date_format = "%d-%m-%Y"
|
|
|
|
|
|
gantt.config.fit_tasks = true
|
|
|
|
|
|
gantt.config.start_date = null
|
|
|
|
|
|
gantt.config.end_date = null
|
|
|
|
|
|
|
|
|
|
|
|
gantt.config.columns = [
|
|
|
|
|
|
{ name: "wbs", label: "WBS", width: 50, align: "center" },
|
|
|
|
|
|
{ name: "text", label: "TASK NAME", width: 250, tree: true },
|
|
|
|
|
|
{ name: "start_date", label: "START TIME", width: 100, align: "center" },
|
|
|
|
|
|
{ name: "duration", label: "DURATION", width: 80, align: "center" }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
gantt.config.scale_unit = "day"
|
|
|
|
|
|
gantt.config.date_scale = "%d %M"
|
|
|
|
|
|
gantt.config.scale_height = 50
|
|
|
|
|
|
gantt.config.row_height = 40
|
|
|
|
|
|
gantt.config.bar_height = 24
|
|
|
|
|
|
gantt.config.grid_width = 450
|
|
|
|
|
|
|
|
|
|
|
|
gantt.config.subscales = [
|
|
|
|
|
|
{ unit: "month", step: 1, date: "%M %Y" },
|
|
|
|
|
|
{ unit: "day", step: 1, date: "%j" }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
gantt.config.drag_move = true
|
|
|
|
|
|
gantt.config.drag_resize = true
|
|
|
|
|
|
gantt.config.drag_progress = true
|
|
|
|
|
|
gantt.config.drag_links = true
|
|
|
|
|
|
gantt.config.details_on_create = true
|
|
|
|
|
|
gantt.config.details_on_dblclick = true
|
|
|
|
|
|
|
|
|
|
|
|
gantt.templates.task_class = function (start, end, task) {
|
|
|
|
|
|
if (task.progress === 1) return "completed-task"
|
|
|
|
|
|
if (task.progress >= 0.75) return "high-progress-task"
|
|
|
|
|
|
if (task.progress >= 0.25) return "medium-progress-task"
|
|
|
|
|
|
return "low-progress-task"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gantt.templates.grid_row_class = function (start, end, task) {
|
|
|
|
|
|
if (task.$level === 0) return "summary-row"
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gantt.templates.rightside_text = function (start, end, task) {
|
|
|
|
|
|
if (task.progress === 1) {
|
|
|
|
|
|
return "<span class='completed-mark'>✓</span>"
|
|
|
|
|
|
}
|
|
|
|
|
|
return Math.round(task.progress * 100) + "%"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let taskCounter = 1
|
|
|
|
|
|
gantt.templates.grid_cell_value = function (item, column) {
|
|
|
|
|
|
if (column === "wbs") {
|
|
|
|
|
|
if (item.$level === 0) {
|
|
|
|
|
|
return taskCounter++
|
|
|
|
|
|
} else if (item.$level === 1) {
|
|
|
|
|
|
let parentIndex = gantt.getParent(item.id)
|
|
|
|
|
|
let parent = gantt.getTask(parentIndex)
|
|
|
|
|
|
let siblings = gantt.getChildren(parentIndex)
|
|
|
|
|
|
let childIndex = siblings.indexOf(item.id) + 1
|
|
|
|
|
|
return parent.$wbs + "." + childIndex
|
|
|
|
|
|
} else if (item.$level === 2) {
|
|
|
|
|
|
let parentIndex = gantt.getParent(item.id)
|
|
|
|
|
|
let parent = gantt.getTask(parentIndex)
|
|
|
|
|
|
let siblings = gantt.getChildren(parentIndex)
|
|
|
|
|
|
let childIndex = siblings.indexOf(item.id) + 1
|
|
|
|
|
|
return parent.$wbs + "." + childIndex
|
2025-08-04 16:33:07 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-30 16:44:46 +03:30
|
|
|
|
return gantt.templates.grid_cell_value_default(item, column)
|
2025-08-04 16:33:07 +03:30
|
|
|
|
}
|
2025-08-30 16:44:46 +03:30
|
|
|
|
|
|
|
|
|
|
gantt.attachEvent("onTaskLoading", function (task) {
|
|
|
|
|
|
if (task.$level === 0) {
|
|
|
|
|
|
task.$wbs = taskCounter
|
|
|
|
|
|
taskCounter++
|
|
|
|
|
|
} else if (task.$level === 1) {
|
|
|
|
|
|
let parent = gantt.getTask(gantt.getParent(task.id))
|
|
|
|
|
|
let siblings = gantt.getChildren(gantt.getParent(task.id))
|
|
|
|
|
|
let index = siblings.indexOf(task.id) + 1
|
|
|
|
|
|
task.$wbs = parent.$wbs + "." + index
|
|
|
|
|
|
} else if (task.$level === 2) {
|
|
|
|
|
|
let parentId = gantt.getParent(task.id)
|
|
|
|
|
|
let parent = gantt.getTask(parentId)
|
|
|
|
|
|
let siblings = gantt.getChildren(parentId)
|
|
|
|
|
|
let index = siblings.indexOf(task.id) + 1
|
|
|
|
|
|
task.$wbs = parent.$wbs + "." + index
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
gantt.attachEvent("onContextMenu", function(taskId, linkId, e) {
|
|
|
|
|
|
if (taskId) {
|
|
|
|
|
|
showContextMenu(e, taskId)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
gantt.init(ganttContainer.value)
|
|
|
|
|
|
gantt.parse(tasks)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const showContextMenu = (e, taskId) => {
|
|
|
|
|
|
hideContextMenu()
|
|
|
|
|
|
|
|
|
|
|
|
const contextMenu = document.createElement('div')
|
|
|
|
|
|
contextMenu.id = 'gantt-context-menu'
|
|
|
|
|
|
contextMenu.className = 'gantt-context-menu'
|
|
|
|
|
|
contextMenu.innerHTML = `
|
|
|
|
|
|
<div class="context-menu-item" onclick="window.ganttComponent.addSubTask('${taskId}')">
|
|
|
|
|
|
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M12 5v14m-7-7h14"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Add Sub Task
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-item" onclick="window.ganttComponent.editTask('${taskId}')">
|
|
|
|
|
|
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>
|
|
|
|
|
|
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Edit Task
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-item" onclick="window.ganttComponent.deleteTaskFromContext('${taskId}')">
|
|
|
|
|
|
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M3 6h18"/>
|
|
|
|
|
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
|
|
|
|
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Delete Task
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-separator"></div>
|
|
|
|
|
|
<div class="context-menu-item" onclick="window.ganttComponent.markTaskComplete('${taskId}')">
|
|
|
|
|
|
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<polyline points="9,11 12,14 22,4"/>
|
|
|
|
|
|
<path d="M21,12v7a2,2 0 0,1 -2,2H5a2,2 0 0,1 -2,-2V5a2,2 0 0,1 2,-2h11"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Toggle Complete
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
contextMenu.style.position = 'fixed'
|
|
|
|
|
|
contextMenu.style.left = e.clientX + 'px'
|
|
|
|
|
|
contextMenu.style.top = e.clientY + 'px'
|
|
|
|
|
|
contextMenu.style.zIndex = '9999'
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(contextMenu)
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', hideContextMenu)
|
|
|
|
|
|
window.ganttComponent = {
|
|
|
|
|
|
addSubTask,
|
|
|
|
|
|
editTask,
|
|
|
|
|
|
deleteTaskFromContext,
|
|
|
|
|
|
markTaskComplete
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hideContextMenu = () => {
|
|
|
|
|
|
const existingMenu = document.getElementById('gantt-context-menu')
|
|
|
|
|
|
if (existingMenu) {
|
|
|
|
|
|
existingMenu.remove()
|
|
|
|
|
|
}
|
|
|
|
|
|
document.removeEventListener('click', hideContextMenu)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const addSubTask = (parentId) => {
|
|
|
|
|
|
currentTaskId.value = parentId
|
|
|
|
|
|
taskForm.value = {
|
|
|
|
|
|
text: 'New Sub Task',
|
|
|
|
|
|
start_date: new Date().toISOString().split('T')[0],
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
showAddModal.value = true
|
|
|
|
|
|
hideContextMenu()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const editTask = (taskId) => {
|
|
|
|
|
|
currentTaskId.value = taskId
|
|
|
|
|
|
const task = gantt.getTask(taskId)
|
|
|
|
|
|
currentTask.value = task
|
|
|
|
|
|
taskForm.value = {
|
|
|
|
|
|
text: task.text,
|
|
|
|
|
|
start_date: gantt.date.date_to_str("%Y-%m-%d")(task.start_date),
|
|
|
|
|
|
duration: task.duration,
|
|
|
|
|
|
progress: task.progress
|
|
|
|
|
|
}
|
|
|
|
|
|
showEditModal.value = true
|
|
|
|
|
|
hideContextMenu()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteTaskFromContext = (taskId) => {
|
|
|
|
|
|
currentTaskId.value = taskId
|
|
|
|
|
|
currentTask.value = gantt.getTask(taskId)
|
|
|
|
|
|
showDeleteModal.value = true
|
|
|
|
|
|
hideContextMenu()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const markTaskComplete = (taskId) => {
|
|
|
|
|
|
let task = gantt.getTask(taskId)
|
|
|
|
|
|
task.progress = task.progress === 1 ? 0 : 1
|
|
|
|
|
|
gantt.updateTask(taskId)
|
|
|
|
|
|
gantt.render()
|
|
|
|
|
|
hideContextMenu()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const addTask = () => {
|
|
|
|
|
|
currentTaskId.value = null
|
|
|
|
|
|
taskForm.value = {
|
|
|
|
|
|
text: 'New Task',
|
|
|
|
|
|
start_date: new Date().toISOString().split('T')[0],
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
showAddModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteTask = () => {
|
|
|
|
|
|
const selectedTask = gantt.getSelectedId()
|
|
|
|
|
|
if (selectedTask) {
|
|
|
|
|
|
currentTaskId.value = selectedTask
|
|
|
|
|
|
currentTask.value = gantt.getTask(selectedTask)
|
|
|
|
|
|
showDeleteModal.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showNotification('Please select a task to delete', 'warning')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const confirmDelete = () => {
|
|
|
|
|
|
if (currentTaskId.value) {
|
|
|
|
|
|
gantt.deleteTask(currentTaskId.value)
|
|
|
|
|
|
showDeleteModal.value = false
|
|
|
|
|
|
currentTaskId.value = null
|
|
|
|
|
|
currentTask.value = null
|
|
|
|
|
|
showNotification('Task deleted successfully', 'success')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveTask = () => {
|
|
|
|
|
|
if (!taskForm.value.text.trim()) {
|
|
|
|
|
|
showNotification('Task name is required', 'error')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const taskData = {
|
|
|
|
|
|
text: taskForm.value.text,
|
|
|
|
|
|
start_date: gantt.date.str_to_date("%Y-%m-%d")(taskForm.value.start_date),
|
|
|
|
|
|
duration: parseInt(taskForm.value.duration),
|
|
|
|
|
|
progress: parseFloat(taskForm.value.progress)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showEditModal.value) {
|
|
|
|
|
|
Object.assign(currentTask.value, taskData)
|
|
|
|
|
|
gantt.updateTask(currentTaskId.value)
|
|
|
|
|
|
gantt.render()
|
|
|
|
|
|
showEditModal.value = false
|
|
|
|
|
|
showNotification('Task updated successfully', 'success')
|
|
|
|
|
|
} else if (showAddModal.value) {
|
|
|
|
|
|
const newTaskId = gantt.uid()
|
|
|
|
|
|
const newTask = {
|
|
|
|
|
|
id: newTaskId,
|
|
|
|
|
|
...taskData
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentTaskId.value) {
|
|
|
|
|
|
newTask.parent = currentTaskId.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gantt.addTask(newTask)
|
|
|
|
|
|
gantt.refreshData()
|
|
|
|
|
|
gantt.selectTask(newTaskId)
|
|
|
|
|
|
gantt.showTask(newTaskId)
|
|
|
|
|
|
showAddModal.value = false
|
|
|
|
|
|
showNotification('Task added successfully', 'success')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
currentTaskId.value = null
|
|
|
|
|
|
currentTask.value = null
|
|
|
|
|
|
taskForm.value = {
|
|
|
|
|
|
text: '',
|
|
|
|
|
|
start_date: '',
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
|
showDeleteModal.value = false
|
|
|
|
|
|
showEditModal.value = false
|
|
|
|
|
|
showAddModal.value = false
|
|
|
|
|
|
resetForm()
|
2025-08-04 16:33:07 +03:30
|
|
|
|
}
|
2025-08-30 16:44:46 +03:30
|
|
|
|
|
|
|
|
|
|
const showNotification = (message, type = 'info') => {
|
|
|
|
|
|
const notification = document.createElement('div')
|
|
|
|
|
|
notification.className = `notification notification-${type}`
|
|
|
|
|
|
notification.innerHTML = `
|
|
|
|
|
|
<div class="notification-content">
|
|
|
|
|
|
<span class="notification-message">${message}</span>
|
|
|
|
|
|
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
document.body.appendChild(notification)
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentElement) {
|
|
|
|
|
|
notification.remove()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 4000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const markCompleted = () => {
|
|
|
|
|
|
const selectedTask = gantt.getSelectedId()
|
|
|
|
|
|
if (selectedTask) {
|
|
|
|
|
|
let task = gantt.getTask(selectedTask)
|
|
|
|
|
|
task.progress = task.progress === 1 ? 0 : 1
|
|
|
|
|
|
gantt.updateTask(selectedTask)
|
|
|
|
|
|
gantt.render()
|
|
|
|
|
|
showNotification(
|
|
|
|
|
|
`Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`,
|
|
|
|
|
|
'success'
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showNotification('Please select a task to mark as completed', 'warning')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
initGantt()
|
|
|
|
|
|
|
|
|
|
|
|
// Prevent GridStack drag when interacting with Gantt chart
|
|
|
|
|
|
const ganttElement = ganttContainer.value
|
|
|
|
|
|
if (ganttElement) {
|
|
|
|
|
|
ganttElement.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
ganttElement.addEventListener('dragstart', (e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
ganttElement.addEventListener('drag', (e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
if (gantt.$container) {
|
|
|
|
|
|
gantt.clearAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-08-25 12:19:58 +03:30
|
|
|
|
</script>
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
2025-08-25 12:19:58 +03:30
|
|
|
|
<template>
|
2025-08-30 16:44:46 +03:30
|
|
|
|
<div class="gantt-container no-grid-drag">
|
2025-08-25 12:19:58 +03:30
|
|
|
|
<div class="gantt-toolbar">
|
2025-08-27 11:11:41 +03:30
|
|
|
|
<button @click="addTask" class="btn btn-primary">
|
|
|
|
|
|
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M12 5v14m-7-7h14"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Add Task
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="deleteTask" class="btn btn-danger">
|
|
|
|
|
|
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M3 6h18"/>
|
|
|
|
|
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
|
|
|
|
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Delete Selected
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="markCompleted" class="btn btn-success">
|
|
|
|
|
|
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<polyline points="9,11 12,14 22,4"/>
|
|
|
|
|
|
<path d="M21,12v7a2,2 0 0,1 -2,2H5a2,2 0 0,1 -2,-2V5a2,2 0 0,1 2,-2h11"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Toggle Complete
|
|
|
|
|
|
</button>
|
2025-08-25 12:19:58 +03:30
|
|
|
|
</div>
|
2025-08-30 16:44:46 +03:30
|
|
|
|
<div ref="ganttContainer" class="gantt-chart no-grid-drag"></div>
|
2025-08-26 16:03:11 +03:30
|
|
|
|
|
|
|
|
|
|
<div v-if="showDeleteModal" class="modal-overlay" @click="closeModal">
|
|
|
|
|
|
<div class="modal-container" @click.stop>
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h3 class="modal-title">
|
2025-08-27 11:11:41 +03:30
|
|
|
|
<svg class="modal-icon delete-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M3 6h18"/>
|
|
|
|
|
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
|
|
|
|
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
2025-08-26 16:03:11 +03:30
|
|
|
|
</svg>
|
|
|
|
|
|
Delete Task
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button class="modal-close" @click="closeModal">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<p class="modal-message">
|
|
|
|
|
|
Are you sure you want to delete <strong>"{{ currentTask?.text }}"</strong>?
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="modal-warning">This action cannot be undone.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-danger" @click="confirmDelete">Delete</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="showEditModal" class="modal-overlay" @click="closeModal">
|
|
|
|
|
|
<div class="modal-container" @click.stop>
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h3 class="modal-title">
|
2025-08-27 11:11:41 +03:30
|
|
|
|
<svg class="modal-icon edit-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>
|
|
|
|
|
|
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
2025-08-26 16:03:11 +03:30
|
|
|
|
</svg>
|
|
|
|
|
|
Edit Task
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button class="modal-close" @click="closeModal">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<form @submit.prevent="saveTask">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="taskName">Task Name</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="taskName"
|
|
|
|
|
|
v-model="taskForm.text"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
placeholder="Enter task name"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="startDate">Start Date</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="startDate"
|
|
|
|
|
|
v-model="taskForm.start_date"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="duration">Duration (days)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="duration"
|
|
|
|
|
|
v-model="taskForm.duration"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="progress">Progress (%)</label>
|
|
|
|
|
|
<div class="progress-input-container">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="progress"
|
|
|
|
|
|
v-model="taskForm.progress"
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
class="form-range"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
max="1"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="progress-value">{{ Math.round(taskForm.progress * 100) }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-primary" @click="saveTask">Save Changes</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="showAddModal" class="modal-overlay" @click="closeModal">
|
|
|
|
|
|
<div class="modal-container" @click.stop>
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h3 class="modal-title">
|
2025-08-27 11:11:41 +03:30
|
|
|
|
<svg class="modal-icon add-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
|
<path d="M12 5v14m-7-7h14"/>
|
2025-08-26 16:03:11 +03:30
|
|
|
|
</svg>
|
|
|
|
|
|
Add New Task
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button class="modal-close" @click="closeModal">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<form @submit.prevent="saveTask">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="newTaskName">Task Name</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="newTaskName"
|
|
|
|
|
|
v-model="taskForm.text"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
placeholder="Enter task name"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="newStartDate">Start Date</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="newStartDate"
|
|
|
|
|
|
v-model="taskForm.start_date"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="newDuration">Duration (days)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="newDuration"
|
|
|
|
|
|
v-model="taskForm.duration"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
class="form-input"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="newProgress">Progress (%)</label>
|
|
|
|
|
|
<div class="progress-input-container">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="newProgress"
|
|
|
|
|
|
v-model="taskForm.progress"
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
class="form-range"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
max="1"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="progress-value">{{ Math.round(taskForm.progress * 100) }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-primary" @click="saveTask">Add Task</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-25 12:19:58 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
2025-09-08 10:45:29 +03:30
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
@import '../../../styles/gantt-chart.scss';
|
2025-08-25 12:19:58 +03:30
|
|
|
|
</style>
|