VanguardAI/app/Views/modules/tareas/kanban.php
2024-10-27 12:50:51 -06:00

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"; ?>