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

1281 lines
32 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>
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
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-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" }
]
}
}
},
mounted() {
this.initGantt()
},
beforeUnmount() {
if (gantt.$container) {
gantt.clearAll()
}
},
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 },
{ 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) {
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 = `
<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">
<path d="M12 5v14m-7-7h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</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">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2"/>
</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">
<path d="m3 6 3 16h12l3-16" stroke="currentColor" stroke-width="2"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2"/>
<line x1="10" y1="11" x2="10" y2="17" stroke="currentColor" stroke-width="2"/>
<line x1="14" y1="11" x2="14" y2="17" stroke="currentColor" stroke-width="2"/>
</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">
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
</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()
},
addTask() {
const selectedId = gantt.getSelectedId()
this.currentTaskId = selectedId
this.taskForm = {
text: 'New Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
this.showAddModal = true
},
deleteTask() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
this.currentTaskId = selectedTask
this.currentTask = gantt.getTask(selectedTask)
this.showDeleteModal = true
} else {
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 = `
<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)
},
markCompleted() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
let task = gantt.getTask(selectedTask)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(selectedTask)
gantt.render()
this.showNotification(
`Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`,
'success'
)
} else {
this.showNotification('Please select a task to mark as completed', 'warning')
}
}
}
}
</script>
<template>
<div class="gantt-container">
<div class="gantt-toolbar">
<button @click="addTask" class="btn btn-primary">Add Task</button>
<button @click="deleteTask" class="btn btn-danger">Delete Selected</button>
<button @click="markCompleted" class="btn btn-success">Toggle Complete</button>
</div>
<div ref="ganttContainer" class="gantt-chart"></div>
<!-- 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">
<svg class="modal-icon delete-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="m3 6 3 16h12l3-16" stroke="currentColor" stroke-width="2"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2"/>
<line x1="10" y1="11" x2="10" y2="17" stroke="currentColor" stroke-width="2"/>
<line x1="14" y1="11" x2="14" y2="17" stroke="currentColor" stroke-width="2"/>
</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">
<svg class="modal-icon edit-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2"/>
</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">
<svg class="modal-icon add-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14m-7-7h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</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: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.gantt-toolbar {
padding: 10px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
display: flex;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.gantt-chart {
height: calc(100% - 60px);
min-height: 500px;
}
</style>
<style>
.gantt_container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 13px;
}
.gantt_grid_head_cell,
.gantt_grid_data .gantt_cell {
border-right: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
}
.gantt_grid_head_cell {
background: #f8f9fa;
font-weight: 600;
text-align: center;
padding: 12px 8px;
color: #495057;
}
.gantt_cell {
padding: 10px 8px;
vertical-align: middle;
line-height: 1.4;
}
.gantt_tree_content {
padding-left: 5px;
}
.gantt_tree_icon {
width: 16px;
height: 16px;
margin-right: 5px;
}
.gantt_task_line.completed-task {
background: #28a745;
border: 1px solid #1e7e34;
}
.gantt_task_line.high-progress-task {
background: #17a2b8;
border: 1px solid #138496;
}
.gantt_task_line.medium-progress-task {
background: #ffc107;
border: 1px solid #e0a800;
}
.gantt_task_line.low-progress-task {
background: #dc3545;
border: 1px solid #c82333;
}
.summary-row {
background: #f1f3f4;
font-weight: 600;
}
.gantt_task_progress {
background: rgba(255, 255, 255, 0.4);
}
.gantt_scale_cell {
border-bottom: 1px solid #ced4da;
border-right: 1px solid #ced4da;
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.gantt_task_line {
border-radius: 3px;
}
.gantt_side_content {
color: #212529;
font-size: 11px;
margin-top: 2px;
font-weight: 500;
}
.completed-mark {
color: #28a745;
font-weight: bold;
font-size: 14px;
}
.gantt_grid_data .gantt_row:nth-child(odd) {
background: #fafafa;
}
.gantt_grid_data .gantt_row:hover {
background: #e3f2fd;
}
.gantt_selected .gantt_cell {
background: #007bff;
color: white;
}
.gantt_task_link {
stroke: #007bff;
stroke-width: 2;
}
.gantt_task_link .gantt_link_arrow {
fill: #007bff;
}
.gantt_milestone {
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;
}
</style>