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

1292 lines
32 KiB
Vue
Raw Normal View History

2025-08-25 12:19:58 +03:30
<script>
2025-08-04 16:33:07 +03:30
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
2025-08-25 12:19:58 +03:30
export default {
name: 'GanttChart',
data() {
return {
2025-08-26 16:03:11 +03:30
showDeleteModal: false,
showEditModal: false,
showAddModal: false,
currentTaskId: null,
currentTask: null,
taskForm: {
text: '',
start_date: '',
duration: 3,
progress: 0
},
2025-08-25 12:19:58 +03:30
tasks: {
data: [
{
id: 1,
text: "Office Itinerary",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 17,
progress: 0.95,
open: true
},
{
id: 11,
text: "Office facing",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 5,
parent: 1,
progress: 1
},
{
id: 111,
text: "Interior office",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 3,
parent: 11,
progress: 1
},
{
id: 112,
text: "Air conditioner check",
2025-08-26 16:03:11 +03:30
start_date: "05-08-2025",
2025-08-25 12:19:58 +03:30
duration: 2,
parent: 11,
progress: 1
},
{
id: 12,
text: "Furniture installation",
2025-08-26 16:03:11 +03:30
start_date: "08-08-2025",
2025-08-25 12:19:58 +03:30
duration: 2,
parent: 1,
progress: 1
},
{
id: 13,
text: "Employee relocation",
2025-08-26 16:03:11 +03:30
start_date: "10-08-2025",
2025-08-25 12:19:58 +03:30
duration: 8,
parent: 1,
progress: 0.67,
open: true
},
{
id: 131,
text: "Preparing workplaces",
2025-08-26 16:03:11 +03:30
start_date: "10-08-2025",
2025-08-25 12:19:58 +03:30
duration: 3,
parent: 13,
progress: 1
},
{
id: 132,
text: "Workplaces importation",
2025-08-26 16:03:11 +03:30
start_date: "13-08-2025",
2025-08-25 12:19:58 +03:30
duration: 3,
parent: 13,
progress: 1
},
{
id: 133,
text: "Workplaces exportation",
2025-08-26 16:03:11 +03:30
start_date: "16-08-2025",
2025-08-25 12:19:58 +03:30
duration: 2,
parent: 13,
progress: 0
},
{
id: 2,
text: "Product launch",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 18,
progress: 0.73,
open: true
},
{
id: 21,
text: "Perform initial testing",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 5,
parent: 2,
progress: 1
},
{
id: 22,
text: "Development",
2025-08-26 16:03:11 +03:30
start_date: "03-08-2025",
2025-08-25 12:19:58 +03:30
duration: 16,
parent: 2,
progress: 0.68,
open: true
},
{
id: 221,
text: "Develop System",
2025-08-26 16:03:11 +03:30
start_date: "03-08-2025",
2025-08-25 12:19:58 +03:30
duration: 5,
parent: 22,
progress: 1
},
{
id: 222,
text: "Beta Release",
2025-08-26 16:03:11 +03:30
start_date: "08-08-2025",
2025-08-25 12:19:58 +03:30
duration: 1,
parent: 22,
progress: 1
},
{
id: 223,
text: "Integrate System",
2025-08-26 16:03:11 +03:30
start_date: "09-08-2025",
2025-08-25 12:19:58 +03:30
duration: 4,
parent: 22,
progress: 1
},
{
id: 224,
text: "Test",
2025-08-26 16:03:11 +03:30
start_date: "13-08-2025",
2025-08-25 12:19:58 +03:30
duration: 3,
parent: 22,
progress: 0.67
},
{
id: 225,
text: "Marketing",
2025-08-26 16:03:11 +03:30
start_date: "16-08-2025",
2025-08-25 12:19:58 +03:30
duration: 3,
parent: 22,
progress: 0
},
{
id: 23,
text: "Analysis",
2025-08-26 16:03:11 +03:30
start_date: "01-08-2025",
2025-08-25 12:19:58 +03:30
duration: 4,
parent: 2,
progress: 1
},
{
id: 24,
text: "Design",
2025-08-26 16:03:11 +03:30
start_date: "06-08-2025",
2025-08-25 12:19:58 +03:30
duration: 6,
parent: 2,
progress: 0.75,
open: true
},
{
id: 241,
text: "Design database",
2025-08-26 16:03:11 +03:30
start_date: "06-08-2025",
2025-08-25 12:19:58 +03:30
duration: 4,
parent: 24,
progress: 1
},
{
id: 242,
text: "Software design",
2025-08-26 16:03:11 +03:30
start_date: "10-08-2025",
2025-08-25 12:19:58 +03:30
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" }
]
}
}
},
mounted() {
this.initGantt()
2025-08-04 16:33:07 +03:30
},
2025-08-25 12:19:58 +03:30
beforeUnmount() {
if (gantt.$container) {
gantt.clearAll()
}
},
methods: {
initGantt() {
gantt.config.date_format = "%d-%m-%Y"
2025-08-26 16:03:11 +03:30
gantt.config.fit_tasks = true
gantt.config.start_date = null
gantt.config.end_date = null
2025-08-25 12:19:58 +03:30
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" }
]
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
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
2025-08-26 16:03:11 +03:30
2025-08-25 12:19:58 +03:30
gantt.config.subscales = [
{ unit: "month", step: 1, date: "%M %Y" },
{ unit: "day", step: 1, date: "%j" }
]
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
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
2025-08-26 16:03:11 +03:30
gantt.templates.task_class = function (start, end, task) {
2025-08-25 12:19:58 +03:30
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"
}
2025-08-04 16:33:07 +03:30
2025-08-26 16:03:11 +03:30
gantt.templates.grid_row_class = function (start, end, task) {
2025-08-25 12:19:58 +03:30
if (task.$level === 0) return "summary-row"
return ""
}
2025-08-04 16:33:07 +03:30
2025-08-26 16:03:11 +03:30
gantt.templates.rightside_text = function (start, end, task) {
2025-08-25 12:19:58 +03:30
if (task.progress === 1) {
return "<span class='completed-mark'>✓</span>"
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
return Math.round(task.progress * 100) + "%"
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
let taskCounter = 1
2025-08-26 16:03:11 +03:30
gantt.templates.grid_cell_value = function (item, column) {
2025-08-25 12:19:58 +03:30
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
}
}
return gantt.templates.grid_cell_value_default(item, column)
}
2025-08-26 16:03:11 +03:30
gantt.attachEvent("onTaskLoading", function (task) {
2025-08-25 12:19:58 +03:30
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
})
2025-08-26 16:03:11 +03:30
gantt.attachEvent("onContextMenu", function(taskId, linkId, e) {
if (taskId) {
this.showContextMenu(e, taskId)
return false
}
return true
2025-08-25 12:19:58 +03:30
}.bind(this))
gantt.init(this.$refs.ganttContainer)
gantt.parse(this.tasks)
2025-08-04 16:33:07 +03:30
},
2025-08-26 16:03:11 +03:30
showContextMenu(e, taskId) {
this.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}')">
2025-08-27 11:11:41 +03:30
<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"/>
2025-08-26 16:03:11 +03:30
</svg>
Add Sub Task
</div>
<div class="context-menu-item" onclick="window.ganttComponent.editTask('${taskId}')">
2025-08-27 11:11:41 +03:30
<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"/>
2025-08-26 16:03:11 +03:30
</svg>
Edit Task
</div>
<div class="context-menu-item" onclick="window.ganttComponent.deleteTaskFromContext('${taskId}')">
2025-08-27 11:11:41 +03:30
<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"/>
2025-08-26 16:03:11 +03:30
</svg>
Delete Task
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-item" onclick="window.ganttComponent.markTaskComplete('${taskId}')">
2025-08-27 11:11:41 +03:30
<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"/>
2025-08-26 16:03:11 +03:30
</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', this.hideContextMenu)
window.ganttComponent = this
},
hideContextMenu() {
const existingMenu = document.getElementById('gantt-context-menu')
if (existingMenu) {
existingMenu.remove()
}
document.removeEventListener('click', this.hideContextMenu)
},
addSubTask(parentId) {
this.currentTaskId = parentId
this.taskForm = {
text: 'New Sub Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
this.showAddModal = true
this.hideContextMenu()
},
editTask(taskId) {
this.currentTaskId = taskId
const task = gantt.getTask(taskId)
this.currentTask = task
this.taskForm = {
text: task.text,
start_date: gantt.date.date_to_str("%Y-%m-%d")(task.start_date),
duration: task.duration,
progress: task.progress
}
this.showEditModal = true
this.hideContextMenu()
},
deleteTaskFromContext(taskId) {
this.currentTaskId = taskId
this.currentTask = gantt.getTask(taskId)
this.showDeleteModal = true
this.hideContextMenu()
},
markTaskComplete(taskId) {
let task = gantt.getTask(taskId)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(taskId)
gantt.render()
this.hideContextMenu()
},
2025-08-25 12:19:58 +03:30
addTask() {
2025-08-27 11:11:41 +03:30
this.currentTaskId = null
2025-08-26 16:03:11 +03:30
this.taskForm = {
text: 'New Task',
start_date: new Date().toISOString().split('T')[0],
2025-08-25 12:19:58 +03:30
duration: 3,
2025-08-26 16:03:11 +03:30
progress: 0
2025-08-04 16:33:07 +03:30
}
2025-08-26 16:03:11 +03:30
this.showAddModal = true
2025-08-04 16:33:07 +03:30
},
2025-08-25 12:19:58 +03:30
deleteTask() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
2025-08-26 16:03:11 +03:30
this.currentTaskId = selectedTask
this.currentTask = gantt.getTask(selectedTask)
this.showDeleteModal = true
2025-08-25 12:19:58 +03:30
} else {
2025-08-26 16:03:11 +03:30
this.showNotification('Please select a task to delete', 'warning')
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
},
2025-08-26 16:03:11 +03:30
confirmDelete() {
if (this.currentTaskId) {
gantt.deleteTask(this.currentTaskId)
this.showDeleteModal = false
this.currentTaskId = null
this.currentTask = null
this.showNotification('Task deleted successfully', 'success')
}
},
saveTask() {
if (!this.taskForm.text.trim()) {
this.showNotification('Task name is required', 'error')
return
}
const taskData = {
text: this.taskForm.text,
start_date: gantt.date.str_to_date("%Y-%m-%d")(this.taskForm.start_date),
duration: parseInt(this.taskForm.duration),
progress: parseFloat(this.taskForm.progress)
}
if (this.showEditModal) {
Object.assign(this.currentTask, taskData)
gantt.updateTask(this.currentTaskId)
gantt.render()
this.showEditModal = false
this.showNotification('Task updated successfully', 'success')
} else if (this.showAddModal) {
const newTaskId = gantt.uid()
const newTask = {
id: newTaskId,
...taskData
}
if (this.currentTaskId) {
newTask.parent = this.currentTaskId
}
gantt.addTask(newTask)
gantt.refreshData()
gantt.selectTask(newTaskId)
gantt.showTask(newTaskId)
this.showAddModal = false
this.showNotification('Task added successfully', 'success')
}
this.resetForm()
},
resetForm() {
this.currentTaskId = null
this.currentTask = null
this.taskForm = {
text: '',
start_date: '',
duration: 3,
progress: 0
}
},
closeModal() {
this.showDeleteModal = false
this.showEditModal = false
this.showAddModal = false
this.resetForm()
},
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)
},
2025-08-25 12:19:58 +03:30
markCompleted() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
let task = gantt.getTask(selectedTask)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(selectedTask)
gantt.render()
2025-08-26 16:03:11 +03:30
this.showNotification(
`Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`,
'success'
)
2025-08-25 12:19:58 +03:30
} else {
2025-08-26 16:03:11 +03:30
this.showNotification('Please select a task to mark as completed', 'warning')
2025-08-04 16:33:07 +03:30
}
}
}
}
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>
<div class="gantt-container">
<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>
<div ref="ganttContainer" class="gantt-chart"></div>
2025-08-26 16:03:11 +03:30
<!-- Delete Modal -->
<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>
<!-- Edit Modal -->
<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>
<!-- Add Modal -->
<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-08-25 12:19:58 +03:30
<style scoped>
.gantt-container {
width: 100%;
height: 650px;
background: white;
2025-08-27 11:11:41 +03:30
border: 1px solid #e5e7eb;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt-toolbar {
2025-08-27 11:11:41 +03:30
padding: 12px 16px;
background: #fafbfc;
border-bottom: 1px solid #e5e7eb;
2025-08-25 12:19:58 +03:30
display: flex;
2025-08-27 11:11:41 +03:30
gap: 8px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn {
2025-08-27 11:11:41 +03:30
display: inline-flex;
align-items: center;
padding: 8px 14px;
border: 1px solid transparent;
border-radius: 6px;
2025-08-25 12:19:58 +03:30
cursor: pointer;
2025-08-27 11:11:41 +03:30
font-size: 13px;
2025-08-25 12:19:58 +03:30
font-weight: 500;
2025-08-27 11:11:41 +03:30
transition: all 0.15s ease;
gap: 6px;
}
.btn-icon {
flex-shrink: 0;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-primary {
2025-08-27 11:11:41 +03:30
background: #0969da;
2025-08-25 12:19:58 +03:30
color: white;
2025-08-27 11:11:41 +03:30
border-color: #0969da;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-primary:hover {
2025-08-27 11:11:41 +03:30
background: #0860ca;
border-color: #0860ca;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-danger {
2025-08-27 11:11:41 +03:30
background: #d1242f;
2025-08-25 12:19:58 +03:30
color: white;
2025-08-27 11:11:41 +03:30
border-color: #d1242f;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-danger:hover {
2025-08-27 11:11:41 +03:30
background: #b91c1c;
border-color: #b91c1c;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-success {
2025-08-27 11:11:41 +03:30
background: #1a7f37;
2025-08-25 12:19:58 +03:30
color: white;
2025-08-27 11:11:41 +03:30
border-color: #1a7f37;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.btn-success:hover {
2025-08-27 11:11:41 +03:30
background: #166a2e;
border-color: #166a2e;
}
.btn-secondary {
background: #f6f8fa;
color: #24292f;
border-color: #d1d9e0;
}
.btn-secondary:hover {
background: #f3f4f6;
border-color: #c7d2da;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt-chart {
height: calc(100% - 60px);
min-height: 500px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
</style>
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
<style>
.gantt_container {
2025-08-27 11:11:41 +03:30
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2025-08-25 12:19:58 +03:30
font-size: 13px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_grid_head_cell,
.gantt_grid_data .gantt_cell {
2025-08-27 11:11:41 +03:30
border-right: 1px solid #e1e5e9;
border-bottom: 1px solid #e1e5e9;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_grid_head_cell {
2025-08-27 11:11:41 +03:30
background: #f6f8fa;
2025-08-25 12:19:58 +03:30
font-weight: 600;
text-align: center;
2025-08-27 11:11:41 +03:30
padding: 10px 8px;
color: #24292f;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_cell {
2025-08-27 11:11:41 +03:30
padding: 8px;
2025-08-25 12:19:58 +03:30
vertical-align: middle;
line-height: 1.4;
2025-08-27 11:11:41 +03:30
color: #24292f;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_tree_content {
2025-08-27 11:11:41 +03:30
padding-left: 4px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_tree_icon {
2025-08-27 11:11:41 +03:30
width: 14px;
height: 14px;
margin-right: 4px;
opacity: 0.7;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_task_line.completed-task {
2025-08-27 11:11:41 +03:30
background: #1f883d;
border: 1px solid #1a7f37;
border-radius: 3px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_task_line.high-progress-task {
2025-08-27 11:11:41 +03:30
background: #0969da;
border: 1px solid #0860ca;
border-radius: 3px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_task_line.medium-progress-task {
2025-08-27 11:11:41 +03:30
background: #bf8700;
border: 1px solid #9a6700;
border-radius: 3px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_task_line.low-progress-task {
2025-08-27 11:11:41 +03:30
background: #d1242f;
border: 1px solid #b91c1c;
border-radius: 3px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.summary-row {
2025-08-27 11:11:41 +03:30
background: #f6f8fa;
2025-08-25 12:19:58 +03:30
font-weight: 600;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_task_progress {
2025-08-27 11:11:41 +03:30
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_scale_cell {
2025-08-27 11:11:41 +03:30
border-bottom: 1px solid #e1e5e9;
border-right: 1px solid #e1e5e9;
background: #f6f8fa;
font-weight: 500;
color: #24292f;
font-size: 11px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_task_line {
border-radius: 3px;
2025-08-27 11:11:41 +03:30
height: 20px;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_side_content {
2025-08-27 11:11:41 +03:30
color: #656d76;
2025-08-25 12:19:58 +03:30
font-size: 11px;
margin-top: 2px;
font-weight: 500;
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.completed-mark {
2025-08-27 11:11:41 +03:30
color: #1f883d;
2025-08-25 12:19:58 +03:30
font-weight: bold;
2025-08-27 11:11:41 +03:30
font-size: 12px;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_grid_data .gantt_row:nth-child(odd) {
2025-08-27 11:11:41 +03:30
background: #fafbfc;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_grid_data .gantt_row:hover {
2025-08-27 11:11:41 +03:30
background: #f3f4f6;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_selected .gantt_cell {
2025-08-27 11:11:41 +03:30
background: #ddf4ff;
color: #24292f;
2025-08-25 12:19:58 +03:30
}
2025-08-04 16:33:07 +03:30
2025-08-25 12:19:58 +03:30
.gantt_task_link {
2025-08-27 11:11:41 +03:30
stroke: #656d76;
stroke-width: 1.5;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_task_link .gantt_link_arrow {
2025-08-27 11:11:41 +03:30
fill: #656d76;
2025-08-04 16:33:07 +03:30
}
2025-08-25 12:19:58 +03:30
.gantt_milestone {
2025-08-27 11:11:41 +03:30
background: #8250df;
border: 2px solid #6f42c1;
2025-08-04 16:33:07 +03:30
}
2025-08-26 16:03:11 +03:30
.gantt-context-menu {
background: #ffffff;
2025-08-27 11:11:41 +03:30
border: 1px solid #d1d9e0;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
padding: 4px;
min-width: 180px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
2025-08-26 16:03:11 +03:30
z-index: 9999;
2025-08-27 11:11:41 +03:30
animation: contextMenuFadeIn 0.15s ease-out;
2025-08-26 16:03:11 +03:30
}
@keyframes contextMenuFadeIn {
from {
opacity: 0;
2025-08-27 11:11:41 +03:30
transform: scale(0.95);
2025-08-26 16:03:11 +03:30
}
to {
opacity: 1;
2025-08-27 11:11:41 +03:30
transform: scale(1);
2025-08-26 16:03:11 +03:30
}
}
.context-menu-item {
display: flex;
align-items: center;
2025-08-27 11:11:41 +03:30
padding: 6px 8px;
2025-08-26 16:03:11 +03:30
cursor: pointer;
2025-08-27 11:11:41 +03:30
color: #24292f;
transition: background-color 0.1s ease;
border-radius: 3px;
font-size: 13px;
2025-08-26 16:03:11 +03:30
line-height: 1.4;
user-select: none;
}
.context-menu-item:hover {
2025-08-27 11:11:41 +03:30
background: #f3f4f6;
2025-08-26 16:03:11 +03:30
}
.context-menu-item:active {
2025-08-27 11:11:41 +03:30
background: #eaeef2;
2025-08-26 16:03:11 +03:30
}
.menu-icon {
2025-08-27 11:11:41 +03:30
margin-right: 8px;
opacity: 0.8;
2025-08-26 16:03:11 +03:30
flex-shrink: 0;
2025-08-27 11:11:41 +03:30
color: #656d76;
2025-08-26 16:03:11 +03:30
}
.context-menu-separator {
height: 1px;
2025-08-27 11:11:41 +03:30
background: #d1d9e0;
margin: 4px 0;
2025-08-26 16:03:11 +03:30
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
2025-08-27 11:11:41 +03:30
background: rgba(31, 35, 40, 0.5);
backdrop-filter: blur(3px);
2025-08-26 16:03:11 +03:30
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
2025-08-27 11:11:41 +03:30
animation: modalOverlayFadeIn 0.2s ease-out;
2025-08-26 16:03:11 +03:30
}
@keyframes modalOverlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-container {
background: #ffffff;
2025-08-27 11:11:41 +03:30
border: 1px solid #d1d9e0;
border-radius: 8px;
box-shadow: 0 16px 32px rgba(31, 35, 40, 0.15);
2025-08-26 16:03:11 +03:30
width: 90%;
2025-08-27 11:11:41 +03:30
max-width: 480px;
2025-08-26 16:03:11 +03:30
max-height: 80vh;
overflow: hidden;
2025-08-27 11:11:41 +03:30
animation: modalSlideIn 0.2s ease-out;
2025-08-26 16:03:11 +03:30
}
@keyframes modalSlideIn {
from {
opacity: 0;
2025-08-27 11:11:41 +03:30
transform: translateY(-16px) scale(0.98);
2025-08-26 16:03:11 +03:30
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
2025-08-27 11:11:41 +03:30
padding: 16px 20px;
border-bottom: 1px solid #d1d9e0;
background: #f6f8fa;
2025-08-26 16:03:11 +03:30
}
.modal-title {
display: flex;
align-items: center;
margin: 0;
2025-08-27 11:11:41 +03:30
font-size: 16px;
2025-08-26 16:03:11 +03:30
font-weight: 600;
2025-08-27 11:11:41 +03:30
color: #24292f;
2025-08-26 16:03:11 +03:30
}
.modal-icon {
2025-08-27 11:11:41 +03:30
margin-right: 8px;
2025-08-26 16:03:11 +03:30
flex-shrink: 0;
}
.delete-icon {
2025-08-27 11:11:41 +03:30
color: #d1242f;
2025-08-26 16:03:11 +03:30
}
.edit-icon {
2025-08-27 11:11:41 +03:30
color: #0969da;
2025-08-26 16:03:11 +03:30
}
.add-icon {
2025-08-27 11:11:41 +03:30
color: #1f883d;
2025-08-26 16:03:11 +03:30
}
.modal-close {
background: none;
border: none;
2025-08-27 11:11:41 +03:30
font-size: 20px;
color: #656d76;
2025-08-26 16:03:11 +03:30
cursor: pointer;
2025-08-27 11:11:41 +03:30
width: 28px;
height: 28px;
2025-08-26 16:03:11 +03:30
display: flex;
align-items: center;
justify-content: center;
2025-08-27 11:11:41 +03:30
border-radius: 3px;
transition: all 0.1s ease;
2025-08-26 16:03:11 +03:30
}
.modal-close:hover {
2025-08-27 11:11:41 +03:30
background: #eaeef2;
color: #24292f;
2025-08-26 16:03:11 +03:30
}
.modal-body {
2025-08-27 11:11:41 +03:30
padding: 20px;
2025-08-26 16:03:11 +03:30
}
.modal-message {
2025-08-27 11:11:41 +03:30
font-size: 14px;
color: #24292f;
margin: 0 0 6px 0;
2025-08-26 16:03:11 +03:30
line-height: 1.5;
}
.modal-warning {
2025-08-27 11:11:41 +03:30
font-size: 12px;
color: #656d76;
2025-08-26 16:03:11 +03:30
margin: 0;
}
.form-group {
2025-08-27 11:11:41 +03:30
margin-bottom: 16px;
2025-08-26 16:03:11 +03:30
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
2025-08-27 11:11:41 +03:30
gap: 12px;
2025-08-26 16:03:11 +03:30
}
.form-group label {
display: block;
2025-08-27 11:11:41 +03:30
margin-bottom: 6px;
font-weight: 600;
color: #24292f;
font-size: 12px;
2025-08-26 16:03:11 +03:30
}
.form-input {
width: 100%;
2025-08-27 11:11:41 +03:30
padding: 6px 8px;
border: 1px solid #d1d9e0;
border-radius: 4px;
font-size: 13px;
color: #24292f;
2025-08-26 16:03:11 +03:30
background: #ffffff;
2025-08-27 11:11:41 +03:30
transition: border-color 0.1s ease;
2025-08-26 16:03:11 +03:30
box-sizing: border-box;
}
.form-input:focus {
outline: none;
2025-08-27 11:11:41 +03:30
border-color: #0969da;
box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.1);
2025-08-26 16:03:11 +03:30
}
.form-input:invalid {
2025-08-27 11:11:41 +03:30
border-color: #d1242f;
2025-08-26 16:03:11 +03:30
}
.progress-input-container {
display: flex;
align-items: center;
2025-08-27 11:11:41 +03:30
gap: 10px;
2025-08-26 16:03:11 +03:30
}
.form-range {
flex: 1;
2025-08-27 11:11:41 +03:30
height: 4px;
background: #d1d9e0;
border-radius: 2px;
2025-08-26 16:03:11 +03:30
outline: none;
}
.form-range::-webkit-slider-thumb {
-webkit-appearance: none;
2025-08-27 11:11:41 +03:30
width: 16px;
height: 16px;
background: #0969da;
2025-08-26 16:03:11 +03:30
border-radius: 50%;
cursor: pointer;
border: 2px solid #ffffff;
2025-08-27 11:11:41 +03:30
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
2025-08-26 16:03:11 +03:30
}
.form-range::-moz-range-thumb {
2025-08-27 11:11:41 +03:30
width: 16px;
height: 16px;
background: #0969da;
2025-08-26 16:03:11 +03:30
border-radius: 50%;
cursor: pointer;
border: 2px solid #ffffff;
2025-08-27 11:11:41 +03:30
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
2025-08-26 16:03:11 +03:30
}
.progress-value {
2025-08-27 11:11:41 +03:30
min-width: 40px;
2025-08-26 16:03:11 +03:30
text-align: right;
font-weight: 600;
2025-08-27 11:11:41 +03:30
color: #0969da;
font-size: 12px;
2025-08-26 16:03:11 +03:30
}
.modal-footer {
2025-08-27 11:11:41 +03:30
padding: 12px 20px 16px;
2025-08-26 16:03:11 +03:30
display: flex;
2025-08-27 11:11:41 +03:30
gap: 8px;
2025-08-26 16:03:11 +03:30
justify-content: flex-end;
2025-08-27 11:11:41 +03:30
background: #f6f8fa;
border-top: 1px solid #d1d9e0;
2025-08-26 16:03:11 +03:30
}
.notification {
position: fixed;
2025-08-27 11:11:41 +03:30
top: 16px;
right: 16px;
min-width: 280px;
2025-08-26 16:03:11 +03:30
background: #ffffff;
2025-08-27 11:11:41 +03:30
border: 1px solid #d1d9e0;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(31, 35, 40, 0.1);
2025-08-26 16:03:11 +03:30
z-index: 10001;
2025-08-27 11:11:41 +03:30
animation: notificationSlideIn 0.2s ease-out;
2025-08-26 16:03:11 +03:30
}
@keyframes notificationSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.notification-content {
display: flex;
align-items: center;
justify-content: space-between;
2025-08-27 11:11:41 +03:30
padding: 12px 16px;
2025-08-26 16:03:11 +03:30
}
.notification-message {
2025-08-27 11:11:41 +03:30
font-size: 13px;
color: #24292f;
2025-08-26 16:03:11 +03:30
font-weight: 500;
}
.notification-close {
background: none;
border: none;
2025-08-27 11:11:41 +03:30
font-size: 16px;
color: #656d76;
2025-08-26 16:03:11 +03:30
cursor: pointer;
padding: 0;
2025-08-27 11:11:41 +03:30
width: 20px;
height: 20px;
2025-08-26 16:03:11 +03:30
display: flex;
align-items: center;
justify-content: center;
2025-08-27 11:11:41 +03:30
border-radius: 3px;
transition: all 0.1s ease;
2025-08-26 16:03:11 +03:30
}
.notification-close:hover {
background: #f3f4f6;
2025-08-27 11:11:41 +03:30
color: #24292f;
2025-08-26 16:03:11 +03:30
}
.notification-success {
2025-08-27 11:11:41 +03:30
border-left: 3px solid #1f883d;
2025-08-26 16:03:11 +03:30
}
.notification-error {
2025-08-27 11:11:41 +03:30
border-left: 3px solid #d1242f;
2025-08-26 16:03:11 +03:30
}
.notification-warning {
2025-08-27 11:11:41 +03:30
border-left: 3px solid #bf8700;
2025-08-26 16:03:11 +03:30
}
.notification-info {
2025-08-27 11:11:41 +03:30
border-left: 3px solid #0969da;
2025-08-26 16:03:11 +03:30
}
2025-08-25 12:19:58 +03:30
</style>