diff --git a/resources/js/pages/dashboards/gantt.vue b/resources/js/pages/dashboards/gantt.vue index c2930ec..2578c22 100644 --- a/resources/js/pages/dashboards/gantt.vue +++ b/resources/js/pages/dashboards/gantt.vue @@ -6,12 +6,23 @@ export default { name: 'GanttChart', data() { return { + showDeleteModal: false, + showEditModal: false, + showAddModal: false, + currentTaskId: null, + currentTask: null, + taskForm: { + text: '', + start_date: '', + duration: 3, + progress: 0 + }, tasks: { data: [ { id: 1, text: "Office Itinerary", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 17, progress: 0.95, open: true @@ -19,7 +30,7 @@ export default { { id: 11, text: "Office facing", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 5, parent: 1, progress: 1 @@ -27,7 +38,7 @@ export default { { id: 111, text: "Interior office", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 3, parent: 11, progress: 1 @@ -35,7 +46,7 @@ export default { { id: 112, text: "Air conditioner check", - start_date: "05-04-2023", + start_date: "05-08-2025", duration: 2, parent: 11, progress: 1 @@ -43,7 +54,7 @@ export default { { id: 12, text: "Furniture installation", - start_date: "08-04-2023", + start_date: "08-08-2025", duration: 2, parent: 1, progress: 1 @@ -51,7 +62,7 @@ export default { { id: 13, text: "Employee relocation", - start_date: "10-04-2023", + start_date: "10-08-2025", duration: 8, parent: 1, progress: 0.67, @@ -60,7 +71,7 @@ export default { { id: 131, text: "Preparing workplaces", - start_date: "10-04-2023", + start_date: "10-08-2025", duration: 3, parent: 13, progress: 1 @@ -68,7 +79,7 @@ export default { { id: 132, text: "Workplaces importation", - start_date: "13-04-2023", + start_date: "13-08-2025", duration: 3, parent: 13, progress: 1 @@ -76,7 +87,7 @@ export default { { id: 133, text: "Workplaces exportation", - start_date: "16-04-2023", + start_date: "16-08-2025", duration: 2, parent: 13, progress: 0 @@ -84,7 +95,7 @@ export default { { id: 2, text: "Product launch", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 18, progress: 0.73, open: true @@ -92,7 +103,7 @@ export default { { id: 21, text: "Perform initial testing", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 5, parent: 2, progress: 1 @@ -100,7 +111,7 @@ export default { { id: 22, text: "Development", - start_date: "03-04-2023", + start_date: "03-08-2025", duration: 16, parent: 2, progress: 0.68, @@ -109,7 +120,7 @@ export default { { id: 221, text: "Develop System", - start_date: "03-04-2023", + start_date: "03-08-2025", duration: 5, parent: 22, progress: 1 @@ -117,7 +128,7 @@ export default { { id: 222, text: "Beta Release", - start_date: "08-04-2023", + start_date: "08-08-2025", duration: 1, parent: 22, progress: 1 @@ -125,7 +136,7 @@ export default { { id: 223, text: "Integrate System", - start_date: "09-04-2023", + start_date: "09-08-2025", duration: 4, parent: 22, progress: 1 @@ -133,7 +144,7 @@ export default { { id: 224, text: "Test", - start_date: "13-04-2023", + start_date: "13-08-2025", duration: 3, parent: 22, progress: 0.67 @@ -141,7 +152,7 @@ export default { { id: 225, text: "Marketing", - start_date: "16-04-2023", + start_date: "16-08-2025", duration: 3, parent: 22, progress: 0 @@ -149,7 +160,7 @@ export default { { id: 23, text: "Analysis", - start_date: "01-04-2023", + start_date: "01-08-2025", duration: 4, parent: 2, progress: 1 @@ -157,7 +168,7 @@ export default { { id: 24, text: "Design", - start_date: "06-04-2023", + start_date: "06-08-2025", duration: 6, parent: 2, progress: 0.75, @@ -166,7 +177,7 @@ export default { { id: 241, text: "Design database", - start_date: "06-04-2023", + start_date: "06-08-2025", duration: 4, parent: 24, progress: 1 @@ -174,7 +185,7 @@ export default { { id: 242, text: "Software design", - start_date: "10-04-2023", + start_date: "10-08-2025", duration: 2, parent: 24, progress: 0.5 @@ -207,6 +218,10 @@ export default { methods: { 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 }, @@ -220,7 +235,7 @@ export default { 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" } @@ -233,23 +248,19 @@ export default { gantt.config.details_on_create = true gantt.config.details_on_dblclick = true - gantt.templates.task_class = function(start, end, task) { + 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) { + gantt.templates.grid_row_class = function (start, end, task) { if (task.$level === 0) return "summary-row" return "" } - gantt.templates.leftside_text = function(start, end, task) { - return "" - } - - gantt.templates.rightside_text = function(start, end, task) { + gantt.templates.rightside_text = function (start, end, task) { if (task.progress === 1) { return "" } @@ -257,12 +268,7 @@ export default { } let taskCounter = 1 - gantt.templates.grid_row_class = function(start, end, task) { - if (task.$level === 0) return "summary-row" - return "" - } - - gantt.templates.grid_cell_value = function(item, column) { + gantt.templates.grid_cell_value = function (item, column) { if (column === "wbs") { if (item.$level === 0) { return taskCounter++ @@ -274,7 +280,6 @@ export default { return parent.$wbs + "." + childIndex } else if (item.$level === 2) { let parentIndex = gantt.getParent(item.id) - let grandParentIndex = gantt.getParent(parentIndex) let parent = gantt.getTask(parentIndex) let siblings = gantt.getChildren(parentIndex) let childIndex = siblings.indexOf(item.id) + 1 @@ -284,7 +289,7 @@ export default { return gantt.templates.grid_cell_value_default(item, column) } - gantt.attachEvent("onTaskLoading", function(task) { + gantt.attachEvent("onTaskLoading", function (task) { if (task.$level === 0) { task.$wbs = taskCounter taskCounter++ @@ -303,58 +308,213 @@ export default { return true }) - gantt.attachEvent("onAfterTaskAdd", function(id, item) { - this.saveToServer() - }.bind(this)) - - gantt.attachEvent("onAfterTaskDelete", function(id, item) { - this.saveToServer() - }.bind(this)) - - gantt.attachEvent("onAfterTaskUpdate", function(id, item) { - this.saveToServer() - }.bind(this)) - - gantt.attachEvent("onAfterLinkAdd", function(id, item) { - this.saveToServer() - }.bind(this)) - - gantt.attachEvent("onAfterLinkDelete", function(id, item) { - this.saveToServer() + gantt.attachEvent("onContextMenu", function(taskId, linkId, e) { + if (taskId) { + this.showContextMenu(e, taskId) + return false + } + return true }.bind(this)) gantt.init(this.$refs.ganttContainer) gantt.parse(this.tasks) }, + showContextMenu(e, taskId) { + this.hideContextMenu() + + const contextMenu = document.createElement('div') + contextMenu.id = 'gantt-context-menu' + contextMenu.className = 'gantt-context-menu' + contextMenu.innerHTML = ` +
+ + + + Add Sub Task +
+
+ + + + + Edit Task +
+
+ + + + + + + Delete Task +
+
+
+ + + + + Toggle Complete +
+ ` + + 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() + }, addTask() { const selectedId = gantt.getSelectedId() - const newTaskId = gantt.uid() - const today = new Date() - const formattedDate = gantt.date.date_to_str("%d-%m-%Y")(today) - - const newTask = { - id: newTaskId, - text: "New Task", - start_date: formattedDate, + this.currentTaskId = selectedId + this.taskForm = { + text: 'New Task', + start_date: new Date().toISOString().split('T')[0], duration: 3, - progress: 0, - parent: selectedId || 0 + progress: 0 } - - gantt.addTask(newTask, selectedId || "") - gantt.selectTask(newTaskId) - gantt.showTask(newTaskId) + this.showAddModal = true }, deleteTask() { const selectedTask = gantt.getSelectedId() if (selectedTask) { - if (confirm('Are you sure you want to delete this task?')) { - gantt.deleteTask(selectedTask) - } + this.currentTaskId = selectedTask + this.currentTask = gantt.getTask(selectedTask) + this.showDeleteModal = true } else { - alert('Please select a task to delete') + this.showNotification('Please select a task to delete', 'warning') } }, + 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 = ` +
+ ${message} + +
+ ` + document.body.appendChild(notification) + + setTimeout(() => { + if (notification.parentElement) { + notification.remove() + } + }, 4000) + }, markCompleted() { const selectedTask = gantt.getSelectedId() if (selectedTask) { @@ -362,12 +522,13 @@ export default { task.progress = task.progress === 1 ? 0 : 1 gantt.updateTask(selectedTask) gantt.render() + this.showNotification( + `Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`, + 'success' + ) } else { - alert('Please select a task to mark as completed') + this.showNotification('Please select a task to mark as completed', 'warning') } - }, - saveToServer() { - console.log("Saving tasks to server...") } } } @@ -381,6 +542,179 @@ export default {
+ + + + + + + + + @@ -559,4 +893,389 @@ export default { background: #6f42c1; border: 2px solid #5a2d91; } + +.gantt-context-menu { + background: #ffffff; + border: 1px solid #e1e5e9; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.08); + padding: 8px 0; + min-width: 200px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 14px; + z-index: 9999; + backdrop-filter: blur(10px); + animation: contextMenuFadeIn 0.2s ease-out; +} + +@keyframes contextMenuFadeIn { + from { + opacity: 0; + transform: translateY(-8px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.context-menu-item { + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + color: #374151; + transition: all 0.15s ease; + border: none; + background: none; + font-size: 14px; + line-height: 1.4; + text-decoration: none; + user-select: none; +} + +.context-menu-item:hover { + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); + color: #1e293b; + transform: translateX(2px); +} + +.context-menu-item:active { + background: #e2e8f0; + transform: translateX(1px); +} + +.menu-icon { + margin-right: 12px; + opacity: 0.7; + transition: opacity 0.15s ease; + flex-shrink: 0; +} + +.context-menu-item:hover .menu-icon { + opacity: 1; +} + +.context-menu-separator { + height: 1px; + background: #e2e8f0; + margin: 8px 16px; + opacity: 0.6; +} + +.context-menu-item:first-child { + margin-top: 4px; +} + +.context-menu-item:last-child { + margin-bottom: 4px; +} + +.context-menu-item:nth-child(1) .menu-icon { + color: #10b981; +} + +.context-menu-item:nth-child(2) .menu-icon { + color: #3b82f6; +} + +.context-menu-item:nth-child(3) .menu-icon { + color: #ef4444; +} + +.context-menu-item:nth-child(5) .menu-icon { + color: #8b5cf6; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: modalOverlayFadeIn 0.3s ease-out; +} + +@keyframes modalOverlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal-container { + background: #ffffff; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow: hidden; + animation: modalSlideIn 0.3s ease-out; +} + +@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: 24px 24px 16px; + border-bottom: 1px solid #e5e7eb; +} + +.modal-title { + display: flex; + align-items: center; + margin: 0; + font-size: 20px; + font-weight: 600; + color: #111827; +} + +.modal-icon { + margin-right: 12px; + flex-shrink: 0; +} + +.delete-icon { + color: #ef4444; +} + +.edit-icon { + color: #3b82f6; +} + +.add-icon { + color: #10b981; +} + +.modal-close { + background: none; + border: none; + font-size: 28px; + color: #6b7280; + 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: #f3f4f6; + color: #374151; +} + +.modal-body { + padding: 24px; +} + +.modal-message { + font-size: 16px; + color: #374151; + margin: 0 0 8px 0; + line-height: 1.5; +} + +.modal-warning { + font-size: 14px; + color: #6b7280; + margin: 0; +} + +.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: 500; + color: #374151; + font-size: 14px; +} + +.form-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e5e7eb; + border-radius: 8px; + font-size: 14px; + color: #374151; + background: #ffffff; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.form-input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-input:invalid { + border-color: #ef4444; +} + +.progress-input-container { + display: flex; + align-items: center; + gap: 12px; +} + +.form-range { + flex: 1; + height: 6px; + background: #e5e7eb; + border-radius: 3px; + outline: none; + -webkit-appearance: none; +} + +.form-range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: #3b82f6; + border-radius: 50%; + cursor: pointer; + border: 2px solid #ffffff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.form-range::-moz-range-thumb { + width: 20px; + height: 20px; + background: #3b82f6; + border-radius: 50%; + cursor: pointer; + border: 2px solid #ffffff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.progress-value { + min-width: 45px; + text-align: right; + font-weight: 600; + color: #3b82f6; + font-size: 14px; +} + +.modal-footer { + padding: 16px 24px 24px; + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.btn-secondary { + background: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.btn-secondary:hover { + background: #e5e7eb; + color: #1f2937; +} + +.notification { + position: fixed; + top: 20px; + right: 20px; + min-width: 300px; + background: #ffffff; + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); + z-index: 10001; + animation: notificationSlideIn 0.3s ease-out; +} + +@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 20px; +} + +.notification-message { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.notification-close { + background: none; + border: none; + font-size: 20px; + color: #6b7280; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.notification-close:hover { + background: #f3f4f6; + color: #374151; +} + +.notification-success { + border-left: 4px solid #10b981; +} + +.notification-error { + border-left: 4px solid #ef4444; +} + +.notification-warning { + border-left: 4px solid #f59e0b; +} + +.notification-info { + border-left: 4px solid #3b82f6; +} \ No newline at end of file