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

1417 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
const props = defineProps({
initialTasks: {
type: Object,
default: () => ({
data: [],
links: []
})
},
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
},
{
id: 11,
text: "Office facing",
start_date: "01-08-2025",
duration: 5,
parent: 1,
progress: 1
},
{
id: 111,
text: "Interior office",
start_date: "01-08-2025",
duration: 3,
parent: 11,
progress: 1
},
{
id: 112,
text: "Air conditioner check",
start_date: "05-08-2025",
duration: 2,
parent: 11,
progress: 1
},
{
id: 12,
text: "Furniture installation",
start_date: "08-08-2025",
duration: 2,
parent: 1,
progress: 1
},
{
id: 13,
text: "Employee relocation",
start_date: "10-08-2025",
duration: 8,
parent: 1,
progress: 0.67,
open: true
},
{
id: 131,
text: "Preparing workplaces",
start_date: "10-08-2025",
duration: 3,
parent: 13,
progress: 1
},
{
id: 132,
text: "Workplaces importation",
start_date: "13-08-2025",
duration: 3,
parent: 13,
progress: 1
},
{
id: 133,
text: "Workplaces exportation",
start_date: "16-08-2025",
duration: 2,
parent: 13,
progress: 0
},
{
id: 2,
text: "Product launch",
start_date: "01-08-2025",
duration: 18,
progress: 0.73,
open: true
},
{
id: 21,
text: "Perform initial testing",
start_date: "01-08-2025",
duration: 5,
parent: 2,
progress: 1
},
{
id: 22,
text: "Development",
start_date: "03-08-2025",
duration: 16,
parent: 2,
progress: 0.68,
open: true
},
{
id: 221,
text: "Develop System",
start_date: "03-08-2025",
duration: 5,
parent: 22,
progress: 1
},
{
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
},
{
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
}
}
return gantt.templates.grid_cell_value_default(item, column)
}
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()
}
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()
}
})
</script>
<template>
<div class="gantt-container no-grid-drag">
<div class="gantt-toolbar">
<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>
</div>
<div ref="ganttContainer" class="gantt-chart no-grid-drag"></div>
<div v-if="showDeleteModal" class="modal-overlay" @click="closeModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
<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"/>
</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">
<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"/>
</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">
<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"/>
</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>
</div>
</template>
<style scoped>
.gantt-container {
width: 100%;
height: 650px;
background: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: rgb(var(--v-theme-on-surface));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
}
.gantt-toolbar {
padding: 16px 20px;
background: rgb(var(--v-theme-surface-variant));
border-bottom: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
display: flex;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
gap: 8px;
}
.btn-icon {
flex-shrink: 0;
}
.btn-primary {
background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
box-shadow: 0 2px 4px rgba(var(--v-theme-primary), 0.3);
}
.btn-primary:hover {
background: rgb(var(--v-theme-primary-darken-1));
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(var(--v-theme-primary), 0.4);
}
.btn-danger {
background: rgb(var(--v-theme-error));
color: rgb(var(--v-theme-on-error));
box-shadow: 0 2px 4px rgba(var(--v-theme-error), 0.3);
}
.btn-danger:hover {
background: rgb(var(--v-theme-error-darken-1));
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(var(--v-theme-error), 0.4);
}
.btn-success {
background: rgb(var(--v-theme-success));
color: rgb(var(--v-theme-on-success));
box-shadow: 0 2px 4px rgba(var(--v-theme-success), 0.3);
}
.btn-success:hover {
background: rgb(var(--v-theme-success-darken-1));
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(var(--v-theme-success), 0.4);
}
.btn-secondary {
background: rgb(var(--v-theme-surface-light));
color: rgb(var(--v-theme-on-surface-light));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
}
.btn-secondary:hover {
background: rgb(var(--v-theme-grey-200));
border-color: rgb(var(--v-theme-grey-400));
transform: translateY(-1px);
}
.gantt-chart {
height: calc(100% - 77px);
min-height: 500px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: modalOverlayFadeIn 0.3s ease;
}
@keyframes modalOverlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-container {
background: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 520px;
max-height: 80vh;
overflow: hidden;
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface-variant));
}
.modal-title {
display: flex;
align-items: center;
margin: 0;
font-size: 18px;
font-weight: 600;
color: rgb(var(--v-theme-on-surface));
}
.modal-icon {
margin-right: 10px;
flex-shrink: 0;
padding: 4px;
border-radius: 4px;
}
.delete-icon {
color: rgb(var(--v-theme-error));
background: rgba(var(--v-theme-error), 0.1);
}
.edit-icon {
color: rgb(var(--v-theme-primary));
background: rgba(var(--v-theme-primary), 0.1);
}
.add-icon {
color: rgb(var(--v-theme-success));
background: rgba(var(--v-theme-success), 0.1);
}
.modal-close {
background: rgb(var(--v-theme-grey-100));
border: none;
font-size: 18px;
color: rgb(var(--v-theme-on-surface));
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.modal-close:hover {
background: rgb(var(--v-theme-grey-200));
color: rgb(var(--v-theme-error));
}
.modal-body {
padding: 24px;
background: rgb(var(--v-theme-surface));
}
.modal-message {
font-size: 15px;
color: rgb(var(--v-theme-on-surface));
margin: 0 0 8px 0;
line-height: 1.5;
}
.modal-warning {
font-size: 13px;
color: rgba(var(--v-theme-on-surface), 0.6);
margin: 0;
font-style: italic;
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: rgb(var(--v-theme-on-surface));
font-size: 13px;
}
.form-input {
width: 100%;
padding: 12px 14px;
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
font-size: 14px;
color: rgb(var(--v-theme-on-surface));
background: rgb(var(--v-theme-surface));
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: rgb(var(--v-theme-primary));
box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.2);
}
.form-input:invalid {
border-color: rgb(var(--v-theme-error));
box-shadow: 0 0 0 2px rgba(var(--v-theme-error), 0.2);
}
.progress-input-container {
display: flex;
align-items: center;
gap: 12px;
}
.form-range {
flex: 1;
height: 4px;
background: rgb(var(--v-theme-grey-200));
border-radius: 2px;
outline: none;
}
.form-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: rgb(var(--v-theme-primary));
border-radius: 50%;
cursor: pointer;
border: 2px solid rgb(var(--v-theme-surface));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.form-range::-moz-range-thumb {
width: 16px;
height: 16px;
background: rgb(var(--v-theme-primary));
border-radius: 50%;
cursor: pointer;
border: 2px solid rgb(var(--v-theme-surface));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-value {
min-width: 40px;
text-align: right;
font-weight: 600;
color: rgb(var(--v-theme-primary));
font-size: 12px;
padding: 2px 6px;
background: rgba(var(--v-theme-primary), 0.1);
border-radius: 4px;
}
.modal-footer {
padding: 16px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
background: rgb(var(--v-theme-surface-variant));
border-top: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
}
.notification {
position: fixed;
top: 20px;
right: 20px;
min-width: 300px;
background: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 10001;
animation: notificationSlideIn 0.3s ease;
}
@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;
padding: 16px;
}
.notification-message {
font-size: 14px;
color: rgb(var(--v-theme-on-surface));
font-weight: 500;
}
.notification-close {
background: none;
border: none;
font-size: 16px;
color: rgba(var(--v-theme-on-surface), 0.6);
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: all 0.2s ease;
}
.notification-close:hover {
background: rgba(var(--v-theme-on-surface), 0.1);
color: rgb(var(--v-theme-on-surface));
}
.notification-success {
border-left: 3px solid rgb(var(--v-theme-success));
}
.notification-error {
border-left: 3px solid rgb(var(--v-theme-error));
}
.notification-warning {
border-left: 3px solid rgb(var(--v-theme-warning));
}
.notification-info {
border-left: 3px solid rgb(var(--v-theme-info));
}
.gantt-container:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
</style>
<style>
.gantt_container {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
background: rgb(var(--v-theme-surface));
color: rgb(var(--v-theme-on-surface));
}
.gantt_grid_head_cell,
.gantt_grid_data .gantt_cell {
border-right: 1px solid rgba(var(--v-border-color), 0.5);
border-bottom: 1px solid rgba(var(--v-border-color), 0.5);
}
.gantt_grid_head_cell {
background: rgb(var(--v-theme-surface-variant));
font-weight: 600;
text-align: center;
padding: 12px 10px;
color: rgb(var(--v-theme-on-surface-variant));
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.gantt_cell {
padding: 10px 12px;
vertical-align: middle;
line-height: 1.4;
color: rgb(var(--v-theme-on-surface));
background: rgb(var(--v-theme-surface));
}
.gantt_tree_content {
padding-left: 6px;
color: rgb(var(--v-theme-on-surface));
font-weight: 500;
}
.gantt_tree_icon {
width: 14px;
height: 14px;
margin-right: 6px;
opacity: 0.6;
color: rgb(var(--v-theme-on-surface));
}
.gantt_task_line.completed-task {
background: rgb(var(--v-theme-success));
border: 1px solid rgb(var(--v-theme-success-darken-1));
border-radius: 4px;
box-shadow: 0 1px 3px rgba(var(--v-theme-success), 0.3);
}
.gantt_task_line.high-progress-task {
background: rgb(var(--v-theme-primary));
border: 1px solid rgb(var(--v-theme-primary-darken-1));
border-radius: 4px;
box-shadow: 0 1px 3px rgba(var(--v-theme-primary), 0.3);
}
.gantt_task_line.medium-progress-task {
background: rgb(var(--v-theme-warning));
border: 1px solid rgb(var(--v-theme-warning-darken-1));
border-radius: 4px;
box-shadow: 0 1px 3px rgba(var(--v-theme-warning), 0.3);
}
.gantt_task_line.low-progress-task {
background: rgb(var(--v-theme-error));
border: 1px solid rgb(var(--v-theme-error-darken-1));
border-radius: 4px;
box-shadow: 0 1px 3px rgba(var(--v-theme-error), 0.3);
}
.summary-row {
background: rgba(var(--v-theme-primary), 0.1);
font-weight: 600;
color: rgb(var(--v-theme-on-surface));
border-left: 3px solid rgb(var(--v-theme-primary));
}
.gantt_task_progress {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.gantt_scale_cell {
border-bottom: 1px solid rgba(var(--v-border-color), 0.5);
border-right: 1px solid rgba(var(--v-border-color), 0.5);
background: rgb(var(--v-theme-surface-variant));
font-weight: 500;
color: rgb(var(--v-theme-on-surface-variant));
font-size: 11px;
text-align: center;
}
.gantt_task_line {
border-radius: 4px;
height: 20px;
transition: all 0.2s ease;
}
.gantt_task_line:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.gantt_side_content {
color: rgba(var(--v-theme-on-surface), 0.6);
font-size: 11px;
margin-top: 2px;
font-weight: 500;
}
.completed-mark {
color: rgb(var(--v-theme-success));
font-weight: bold;
font-size: 12px;
}
.gantt_grid_data .gantt_row:nth-child(odd) {
background: rgb(var(--v-theme-surface));
}
.gantt_grid_data .gantt_row:nth-child(even) {
background: rgba(var(--v-theme-surface-variant), 0.3);
}
.gantt_grid_data .gantt_row:hover {
background: rgba(var(--v-theme-primary), 0.05);
}
.gantt_selected .gantt_cell {
background: rgba(var(--v-theme-primary), 0.1);
color: rgb(var(--v-theme-on-surface));
border-left: 3px solid rgb(var(--v-theme-primary));
}
.gantt_task_link {
stroke: rgba(var(--v-theme-on-surface), 0.4);
stroke-width: 1.5;
}
.gantt_task_link .gantt_link_arrow {
fill: rgba(var(--v-theme-on-surface), 0.4);
}
.gantt_milestone {
background: rgb(var(--v-theme-secondary));
border: 2px solid rgb(var(--v-theme-secondary-darken-1));
box-shadow: 0 1px 3px rgba(var(--v-theme-secondary), 0.3);
}
.gantt-context-menu {
background: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
padding: 6px;
min-width: 180px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
z-index: 9999;
animation: contextMenuFadeIn 0.2s ease;
}
@keyframes contextMenuFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu-item {
display: flex;
align-items: center;
padding: 8px 10px;
cursor: pointer;
color: rgb(var(--v-theme-on-surface));
transition: background-color 0.1s ease;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
user-select: none;
}
.context-menu-item:hover {
background: rgba(var(--v-theme-primary), 0.1);
}
.context-menu-item:active {
background: rgba(var(--v-theme-primary), 0.15);
}
.menu-icon {
margin-right: 8px;
opacity: 0.6;
flex-shrink: 0;
color: rgb(var(--v-theme-on-surface));
}
.context-menu-separator {
height: 1px;
background: rgba(var(--v-border-color), 0.5);
margin: 4px 0;
}
.gantt_layout_root {
background: rgb(var(--v-theme-surface));
}
.gantt_layout_root .gantt_layout_cell {
background: rgb(var(--v-theme-surface));
}
.gantt_grid_scale .gantt_scale_line {
background: rgb(var(--v-theme-surface-variant));
border-bottom: 1px solid rgba(var(--v-border-color), 0.5);
}
.gantt_scale_line {
color: rgb(var(--v-theme-on-surface-variant));
}
.gantt_task_content {
color: rgb(var(--v-theme-on-primary));
font-weight: 500;
font-size: 12px;
}
.gantt_tooltip {
background: rgb(var(--v-theme-surface));
color: rgb(var(--v-theme-on-surface));
border: 1px solid rgb(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 8px 12px;
font-size: 12px;
}
</style>