635 lines
23 KiB
PHP
635 lines
23 KiB
PHP
<?php include __DIR__ . "/../../layouts/header.php"; ?>
|
|
|
|
<style>
|
|
.kanban-container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.kanban-column {
|
|
flex: 1;
|
|
margin: 0 1rem;
|
|
background-color: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
transition: box-shadow 0.3s;
|
|
}
|
|
|
|
.kanban-column:hover {
|
|
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
|
|
}
|
|
|
|
.kanban-header {
|
|
background-color: #007bff;
|
|
color: white;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.kanban-task-container {
|
|
min-height: 40vh;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.kanban-task-item {
|
|
background-color: #e9ecef;
|
|
margin: 0.5rem 0;
|
|
padding: 0.5rem;
|
|
border-radius: 0.5rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: background-color 0.3s;
|
|
cursor: move;
|
|
position: relative;
|
|
}
|
|
|
|
.kanban-task-item:hover {
|
|
background-color: #d3d3d3;
|
|
}
|
|
|
|
.kanban-task-actions button {
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.task-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
background-color: #f9f9f9;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.task-form__input-group {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.task-form__input {
|
|
flex: 1;
|
|
}
|
|
|
|
.task-form__badge {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.sortable-ghost {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.color-container-html {
|
|
min-width: 5rem;
|
|
border-radius: 0.5rem;
|
|
border: 2px solid #ccc;
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
transition: border 0.3s ease, box-shadow 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.color-container-html:hover {
|
|
border-color: #33d17a;
|
|
}
|
|
|
|
.color-container-html:focus {
|
|
border-color: #33d17a;
|
|
box-shadow: 0 0 5px rgba(51, 209, 122, 0.5);
|
|
outline: none;
|
|
}
|
|
|
|
.priority-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.priority-high {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.priority-medium {
|
|
background-color: #ffc107;
|
|
color: black;
|
|
}
|
|
|
|
.priority-low {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
/* Estilo para la descripción en la tarjeta */
|
|
.task-description {
|
|
font-size: 0.875rem;
|
|
margin-top: 0.5rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
/* Estilo para la fecha de vencimiento */
|
|
.due-date {
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
/* Estilos para el formulario de tareas */
|
|
.task-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
background-color: #f9f9f9;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.swal2-input,
|
|
.swal2-textarea,
|
|
.swal2-select {
|
|
border: 1px solid #ced4da;
|
|
border-radius: 0.3rem;
|
|
padding: 0.5rem;
|
|
font-size: 1rem;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
.swal2-input:focus,
|
|
.swal2-textarea:focus,
|
|
.swal2-select:focus {
|
|
border-color: #007bff;
|
|
outline: none;
|
|
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
|
}
|
|
|
|
.color-picker {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
#taskColor,
|
|
#editTaskColor {
|
|
min-width: 5rem;
|
|
}
|
|
|
|
label {
|
|
font-weight: bold;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.task-form {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.swal2-input,
|
|
.swal2-textarea,
|
|
.swal2-select {
|
|
font-size: 0.9rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
.kanban-container {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.kanban-column {
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid px-3 px-md-4 mt-4">
|
|
<div class="text-center mb-4">
|
|
<h1 class="fw-bold">Kanban Dashboard</h1>
|
|
</div>
|
|
|
|
<div id="alertMessages" class="container mb-3"></div>
|
|
|
|
<div class="d-flex justify-content-center mb-3">
|
|
<button id="addTaskButton" class="btn btn-success">
|
|
<i class="fas fa-plus"></i> Añadir Tarea
|
|
</button>
|
|
</div>
|
|
|
|
<div class="kanban-container">
|
|
<div class="kanban-column" id="pendingList" data-status="1">
|
|
<div class="kanban-header">Pendiente</div>
|
|
<div class="kanban-task-container"></div>
|
|
</div>
|
|
<div class="kanban-column" id="inProgressList" data-status="2">
|
|
<div class="kanban-header">En Proceso</div>
|
|
<div class="kanban-task-container"></div>
|
|
</div>
|
|
<div class="kanban-column" id="completedList" data-status="3">
|
|
<div class="kanban-header">Completada</div>
|
|
<div class="kanban-task-container"></div>
|
|
</div>
|
|
<div class="kanban-column" id="canceledList" data-status="4">
|
|
<div class="kanban-header">Cancelada</div>
|
|
<div class="kanban-task-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts necesarios -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.4.2/dist/tinycolor-min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const apiEndpoints = {
|
|
fetch: '/tareas',
|
|
create: '/tarea/create',
|
|
update: '/tarea/update',
|
|
updateStatus: '/tarea/update-status',
|
|
delete: '/tarea/delete',
|
|
get: (id) => `/tarea/get/${id}`
|
|
};
|
|
|
|
const columns = Array.from(document.querySelectorAll('.kanban-column'));
|
|
const addTaskButton = document.getElementById('addTaskButton');
|
|
|
|
/**
|
|
* @function fetchTasks
|
|
* @description Obtiene las tareas desde el servidor y las carga en la UI y el StateManager
|
|
*/
|
|
const fetchTasks = async () => {
|
|
try {
|
|
const response = await fetch(apiEndpoints.fetch);
|
|
const { tasks } = await response.json();
|
|
|
|
const taskMap = tasks.reduce((acc, task) => {
|
|
acc[task.task_id] = task;
|
|
return acc;
|
|
}, {});
|
|
|
|
StateManager.set('tasks', taskMap);
|
|
|
|
tasks.forEach(task => addTaskToColumn(task));
|
|
} catch (error) {
|
|
console.error('Error al cargar las tareas:', error);
|
|
showMessage('Error al cargar las tareas', 'error');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function buildTaskHTML
|
|
* @description Crea el HTML para una tarea
|
|
* @param {Object} task - Objeto de tarea
|
|
* @returns {HTMLElement} Elemento DOM de la tarea
|
|
*/
|
|
const buildTaskHTML = (task) => {
|
|
const taskElement = document.createElement('div');
|
|
taskElement.className = 'kanban-task-item';
|
|
taskElement.setAttribute('data-task-id', task.task_id);
|
|
taskElement.style.backgroundColor = task.color;
|
|
taskElement.style.color = tinycolor(task.color).isLight() ? '#000000' : '#ffffff';
|
|
|
|
taskElement.innerHTML = `
|
|
<div>
|
|
<h5>${task.task_title}</h5>
|
|
<p class="task-description">${task.description || ''}</p>
|
|
<small class="due-date">Vence: ${formatDate(task.due_date)}</small>
|
|
</div>
|
|
<div class="kanban-task-actions">
|
|
<button class="btn btn-sm btn-outline-${tinycolor(task.color).isLight() ? 'dark' : 'light'} edit-task-btn">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-${tinycolor(task.color).isLight() ? 'danger' : 'light'} delete-task-btn">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
taskElement.querySelector('.edit-task-btn').addEventListener('click', () => handleEditTask(task.task_id));
|
|
taskElement.querySelector('.delete-task-btn').addEventListener('click', () => handleDeleteTask(task.task_id));
|
|
|
|
return taskElement;
|
|
};
|
|
|
|
/**
|
|
* @function addTaskToColumn
|
|
* @description Agrega una tarea a la columna correspondiente en la UI
|
|
* @param {Object} task - Objeto de tarea
|
|
*/
|
|
const addTaskToColumn = (task) => {
|
|
const column = document.querySelector(`[data-status="${task.status_id}"] .kanban-task-container`);
|
|
if (column) {
|
|
column.appendChild(buildTaskHTML(task));
|
|
} else {
|
|
console.warn(`Columna con status_id ${task.status_id} no encontrada.`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function updateTaskStatus
|
|
* @description Actualiza el estado de una tarea si ha cambiado
|
|
* @param {number} taskId - ID de la tarea
|
|
* @param {number} newStatus - Nuevo estado de la tarea
|
|
*/
|
|
const updateTaskStatus = async (taskId, newStatus) => {
|
|
const currentTasks = StateManager.get('tasks', {});
|
|
const currentTask = currentTasks[taskId];
|
|
|
|
if (currentTask && currentTask.status_id === newStatus) {
|
|
return;
|
|
}
|
|
|
|
StateManager.update('tasks', {
|
|
[taskId]: { ...currentTask, status_id: newStatus }
|
|
});
|
|
|
|
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
|
if (taskElement) {
|
|
const newColumn = document.querySelector(`[data-status="${newStatus}"] .kanban-task-container`);
|
|
if (newColumn) {
|
|
newColumn.appendChild(taskElement);
|
|
} else {
|
|
console.warn(`Columna con status_id ${newStatus} no encontrada.`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(apiEndpoints.updateStatus, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ task_id: taskId, status_id: newStatus })
|
|
});
|
|
const data = await response.json();
|
|
if (data.message) {
|
|
showMessage(data.message, 'success');
|
|
}
|
|
} catch (error) {
|
|
StateManager.update('tasks', {
|
|
[taskId]: { ...currentTask, status_id: currentTask.status_id }
|
|
});
|
|
if (taskElement) {
|
|
const originalColumn = document.querySelector(`[data-status="${currentTask.status_id}"] .kanban-task-container`);
|
|
if (originalColumn) {
|
|
originalColumn.appendChild(taskElement);
|
|
}
|
|
}
|
|
showMessage('Error al actualizar el estado de la tarea', 'error');
|
|
console.error('Error al actualizar el estado de la tarea:', error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function handleCreateTask
|
|
* @description Maneja la creación de una nueva tarea
|
|
*/
|
|
const handleCreateTask = () => {
|
|
Swal.fire({
|
|
title: 'Crear Nueva Tarea',
|
|
html: `
|
|
<div class="task-form">
|
|
<input type="text" id="taskTitle" class="swal2-input" placeholder="Título de la Tarea">
|
|
<textarea id="taskDescription" class="swal2-textarea" placeholder="Descripción"></textarea>
|
|
<input type="date" id="taskDueDate" class="swal2-input">
|
|
<div class="color-picker">
|
|
<input type="color" id="taskColor" class="swal2-input" value="#e9ecef">
|
|
</div>
|
|
</div>
|
|
`,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Crear',
|
|
cancelButtonText: 'Cancelar',
|
|
preConfirm: () => {
|
|
const title = document.getElementById('taskTitle').value.trim();
|
|
if (!title) {
|
|
Swal.showValidationMessage('El título de la tarea es obligatorio');
|
|
return false;
|
|
}
|
|
return {
|
|
task_title: title,
|
|
description: document.getElementById('taskDescription').value.trim(),
|
|
due_date: document.getElementById('taskDueDate').value,
|
|
color: document.getElementById('taskColor').value,
|
|
status_id: 1
|
|
};
|
|
}
|
|
}).then(async (result) => {
|
|
if (result.isConfirmed) {
|
|
try {
|
|
const response = await fetch(apiEndpoints.create, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(result.value)
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.task_id) {
|
|
const newTask = {
|
|
task_id: data.task_id,
|
|
...result.value
|
|
};
|
|
addTaskToColumn(newTask);
|
|
|
|
const currentTasks = StateManager.get('tasks', {});
|
|
StateManager.update('tasks', {
|
|
[data.task_id]: newTask
|
|
});
|
|
|
|
showMessage('Tarea creada exitosamente', 'success');
|
|
} else {
|
|
showMessage('Error al crear la tarea', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al crear la tarea:', error);
|
|
showMessage('Error al crear la tarea', 'error');
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function handleEditTask
|
|
* @description Maneja la edición de una tarea existente
|
|
* @param {number} taskId - ID de la tarea a editar
|
|
*/
|
|
const handleEditTask = async (taskId) => {
|
|
try {
|
|
const response = await fetch(apiEndpoints.get(taskId));
|
|
const task = await response.json();
|
|
|
|
if (!task) {
|
|
showMessage('Tarea no encontrada', 'error');
|
|
return;
|
|
}
|
|
|
|
const { task_title, description, due_date, color } = task;
|
|
Swal.fire({
|
|
title: 'Editar Tarea',
|
|
html: `
|
|
<div class="task-form">
|
|
<input type="text" id="editTaskTitle" class="swal2-input" placeholder="Título de la Tarea" value="${escapeHtml(task_title)}">
|
|
<textarea id="editTaskDescription" class="swal2-textarea" placeholder="Descripción">${escapeHtml(description)}</textarea>
|
|
<input type="date" id="editTaskDueDate" class="swal2-input" value="${due_date ? due_date.split('T')[0] : ''}">
|
|
<div class="color-picker">
|
|
<input type="color" id="editTaskColor" class="swal2-input" value="${color}">
|
|
</div>
|
|
</div>
|
|
`,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Guardar',
|
|
cancelButtonText: 'Cancelar',
|
|
preConfirm: () => {
|
|
const title = document.getElementById('editTaskTitle').value.trim();
|
|
if (!title) {
|
|
Swal.showValidationMessage('El título de la tarea es obligatorio');
|
|
return false;
|
|
}
|
|
return {
|
|
task_title: title,
|
|
description: document.getElementById('editTaskDescription').value.trim(),
|
|
due_date: document.getElementById('editTaskDueDate').value,
|
|
color: document.getElementById('editTaskColor').value
|
|
};
|
|
}
|
|
}).then(async (result) => {
|
|
if (result.isConfirmed) {
|
|
const updatedTask = {
|
|
task_id: taskId,
|
|
...result.value
|
|
};
|
|
|
|
try {
|
|
const updateResponse = await fetch(apiEndpoints.update, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(updatedTask)
|
|
});
|
|
|
|
const updateData = await updateResponse.json();
|
|
if (updateResponse.ok) {
|
|
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
|
if (taskElement) {
|
|
taskElement.querySelector('h5').textContent = updatedTask.task_title;
|
|
taskElement.querySelector('.task-description').textContent = updatedTask.description;
|
|
taskElement.querySelector('.due-date').textContent = `Vence: ${formatDate(updatedTask.due_date)}`;
|
|
taskElement.style.backgroundColor = updatedTask.color;
|
|
const textColor = tinycolor(updatedTask.color).isLight() ? '#000000' : '#ffffff';
|
|
taskElement.style.color = textColor;
|
|
}
|
|
|
|
StateManager.update('tasks', {
|
|
[taskId]: updatedTask
|
|
});
|
|
|
|
showMessage(updateData.message, 'success');
|
|
} else {
|
|
showMessage(updateData.message || 'Error al actualizar la tarea', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al actualizar la tarea:', error);
|
|
showMessage('Error al actualizar la tarea', 'error');
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error al obtener los datos de la tarea:', error);
|
|
showMessage('Error al cargar la tarea para editar', 'error');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function handleDeleteTask
|
|
* @description Maneja la eliminación de una tarea
|
|
* @param {number} taskId - ID de la tarea a eliminar
|
|
*/
|
|
const handleDeleteTask = async (taskId) => {
|
|
try {
|
|
const result = await Swal.fire({
|
|
title: '¿Estás seguro?',
|
|
text: 'Esta acción no se puede deshacer',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#dc3545',
|
|
cancelButtonColor: '#6c757d',
|
|
confirmButtonText: 'Sí, eliminar',
|
|
cancelButtonText: 'Cancelar',
|
|
});
|
|
|
|
if (result.isConfirmed) {
|
|
try {
|
|
const response = await fetch(apiEndpoints.delete, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ task_id: taskId })
|
|
});
|
|
const data = await response.json();
|
|
if (data.message) {
|
|
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
|
if (taskElement) {
|
|
taskElement.remove();
|
|
}
|
|
|
|
const currentTasks = StateManager.get('tasks', {});
|
|
if (currentTasks[taskId]) {
|
|
delete currentTasks[taskId];
|
|
StateManager.set('tasks', currentTasks);
|
|
}
|
|
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage('Error al eliminar la tarea', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al eliminar la tarea:', error);
|
|
showMessage('Error al eliminar la tarea', 'error');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al eliminar la tarea:', error);
|
|
showMessage('Error al eliminar la tarea', 'error');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function setupSortable
|
|
* @description Configura SortableJS para las columnas
|
|
* @param {HTMLElement} column - Elemento de columna
|
|
*/
|
|
const setupSortable = (column) => {
|
|
new Sortable(column.querySelector('.kanban-task-container'), {
|
|
group: 'tasks',
|
|
animation: 150,
|
|
ghostClass: 'sortable-ghost',
|
|
onEnd: ({ item, to }) => {
|
|
const taskId = item.getAttribute('data-task-id');
|
|
const newStatus = parseInt(to.closest('.kanban-column').getAttribute('data-status'), 10);
|
|
updateTaskStatus(taskId, newStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
if (!StateManager.has('tasks')) {
|
|
StateManager.set('tasks', {});
|
|
}
|
|
|
|
columns.forEach(column => {
|
|
setupSortable(column);
|
|
});
|
|
|
|
addTaskButton.addEventListener('click', handleCreateTask);
|
|
fetchTasks();
|
|
});
|
|
</script>
|
|
|
|
|
|
<?php include __DIR__ . "/../../layouts/footer.php"; ?>
|