let currentSectionId = null; let currentAreaId = null; let prioritiesMap = {}; let statusesMap = {}; let taskTypesMap = {}; let isTodoFilterActive = false; let isForTodayFilterActive = false; document.addEventListener('DOMContentLoaded', () => { loadSidebar(); loadDropdowns(); }); let openModals = []; function showModal(id) { document.getElementById(id).style.display = 'flex'; if (!openModals.includes(id)) openModals.push(id); } function closeModal(id) { document.getElementById(id).style.display = 'none'; openModals = openModals.filter(m => m !== id); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && openModals.length > 0) { const topModalId = openModals.pop(); document.getElementById(topModalId).style.display = 'none'; } }); let searchTimeout = null; function handleSearch(e) { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { loadTasks(); }, 300); } async function fetchAPI(endpoint, method = 'GET', body = null) { const options = { method, headers: { 'Content-Type': 'application/json' } }; if (body) options.body = JSON.stringify(body); const res = await fetch(`/api/${endpoint}`, options); if (!res.ok) throw new Error('API Error'); return res.json(); } let expandedAreas = new Set(); let expandedProjects = new Set(); function toggleArea(areaId) { const el = document.getElementById(`area-sections-${areaId}`); const icon = document.getElementById(`area-icon-${areaId}`); if (el) { if (el.style.display === 'none') { el.style.display = 'block'; icon.innerText = '▼'; expandedAreas.add(areaId); } else { el.style.display = 'none'; icon.innerText = '▶'; expandedAreas.delete(areaId); } } } function toggleProject(projId) { const el = document.getElementById(`project-content-${projId}`); const icon = document.getElementById(`project-icon-${projId}`); if (el) { if (el.style.display === 'none') { el.style.display = 'block'; icon.innerText = '▼'; expandedProjects.add(projId); } else { el.style.display = 'none'; icon.innerText = '▶'; expandedProjects.delete(projId); } } } async function loadSidebar() { const projects = await fetchAPI('projects'); const container = document.getElementById('sidebar-content'); container.innerHTML = ''; for (const proj of projects) { const pDiv = document.createElement('div'); pDiv.className = 'tree-item tree-project'; const isProjExpanded = expandedProjects.has(proj.id); const projIconChar = isProjExpanded ? '▼' : '▶'; pDiv.innerHTML = ` ${projIconChar} 📂 ${proj.name} `; container.appendChild(pDiv); const projectContentContainer = document.createElement('div'); projectContentContainer.id = `project-content-${proj.id}`; projectContentContainer.style.display = isProjExpanded ? 'block' : 'none'; const dDiv = document.createElement('div'); dDiv.className = 'tree-item tree-area'; dDiv.innerHTML = `📚 Documentación`; projectContentContainer.appendChild(dDiv); const areas = await fetchAPI(`areas?project_id=${proj.id}`); for (const area of areas) { const aDiv = document.createElement('div'); aDiv.className = 'tree-item tree-area'; const isExpanded = expandedAreas.has(area.id); const iconChar = isExpanded ? '▼' : '▶'; aDiv.innerHTML = ` ${iconChar} 📁 ${area.name}
`; projectContentContainer.appendChild(aDiv); const sectionsContainer = document.createElement('div'); sectionsContainer.id = `area-sections-${area.id}`; sectionsContainer.style.display = isExpanded ? 'block' : 'none'; const sections = await fetchAPI(`sections?area_id=${area.id}`); for (const sec of sections) { const sDiv = document.createElement('div'); sDiv.className = 'tree-item tree-section'; const taskCount = sec.tasks ? sec.tasks.length : 0; sDiv.innerHTML = ` 📄 ${sec.name} ${taskCount} `; sectionsContainer.appendChild(sDiv); } projectContentContainer.appendChild(sectionsContainer); } container.appendChild(projectContentContainer); } } async function loadDropdowns() { const priorities = await fetchAPI('priorities'); const statuses = await fetchAPI('statuses'); const taskTypes = await fetchAPI('task_types'); priorities.forEach(p => prioritiesMap[p.id] = p); statuses.forEach(s => statusesMap[s.id] = s); taskTypes.forEach(t => taskTypesMap[t.id] = t); const pSelect = document.getElementById('taskPriority'); const sSelect = document.getElementById('taskStatus'); const pEditSelect = document.getElementById('editTaskPriority'); const sEditSelect = document.getElementById('editTaskStatus'); const pFilter = document.getElementById('filterPriority'); const sFilter = document.getElementById('filterStatus'); const tFilter = document.getElementById('filterType'); const pOptions = priorities.map(p => ``).join(''); const sOptions = statuses.map(s => ``).join(''); const tOptions = taskTypes.map(t => ``).join(''); pSelect.innerHTML = pOptions; sSelect.innerHTML = sOptions; pEditSelect.innerHTML = pOptions; sEditSelect.innerHTML = sOptions; pFilter.innerHTML = '' + pOptions; sFilter.innerHTML = '' + sOptions; tFilter.innerHTML = '' + tOptions; } async function selectSection(id, name, areaId, projId) { currentSectionId = id; currentAreaId = areaId; currentDocProjectId = projId; // Update global project context document.getElementById('docs-board').style.display = 'none'; if (document.getElementById('admin-board')) document.getElementById('admin-board').style.display = 'none'; document.getElementById('board').style.display = 'block'; document.getElementById('current-view-title').innerText = `Sección: ${name}`; document.getElementById('btn-add-task').style.display = 'flex'; document.getElementById('taskSectionId').value = id; await loadFavorites(); const tTypes = await fetchAPI(`task_types?area_id=${areaId}`); let optionsHtml = ''; tTypes.forEach(t => { taskTypesMap[t.id] = t; optionsHtml += ``; }); document.getElementById('taskType').innerHTML = optionsHtml; document.getElementById('editTaskType').innerHTML = optionsHtml; loadTasks(); } let allProjectsData = []; async function loadAllTasksDropdowns() { allProjectsData = await fetchAPI('projects'); const filterP = document.getElementById('filterProject'); let html = ''; allProjectsData.forEach(p => { html += ``; }); filterP.innerHTML = html; } async function onFilterProjectChange() { const pId = document.getElementById('filterProject').value; const filterA = document.getElementById('filterArea'); const filterS = document.getElementById('filterSection'); filterA.innerHTML = ''; filterS.innerHTML = ''; if (pId) { const areas = await fetchAPI(`areas?project_id=${pId}`); areas.forEach(a => { filterA.innerHTML += ``; }); } loadTasks(); } async function onFilterAreaChange() { const aId = document.getElementById('filterArea').value; const filterS = document.getElementById('filterSection'); filterS.innerHTML = ''; if (aId) { const sections = await fetchAPI(`sections?area_id=${aId}`); sections.forEach(s => { filterS.innerHTML += ``; }); } loadTasks(); } function showAllTasks() { currentSectionId = null; currentAreaId = null; if (typeof currentDocProjectId !== 'undefined') currentDocProjectId = null; document.getElementById('docs-board').style.display = 'none'; if (document.getElementById('admin-board')) document.getElementById('admin-board').style.display = 'none'; document.getElementById('board').style.display = 'block'; document.getElementById('current-view-title').innerText = `Todas las Tareas`; document.getElementById('btn-add-task').style.display = 'none'; document.getElementById('filterProject').style.display = 'inline-block'; document.getElementById('filterArea').style.display = 'inline-block'; document.getElementById('filterSection').style.display = 'inline-block'; document.getElementById('filterProject').value = ""; document.getElementById('filterArea').innerHTML = ''; document.getElementById('filterSection').innerHTML = ''; loadAllTasksDropdowns(); loadTasks(); } async function loadTasks() { const query = document.getElementById('taskSearchInput') ? document.getElementById('taskSearchInput').value.trim() : ''; const showHidden = document.getElementById('showHiddenTasks').checked; const fVisibility = document.getElementById('filterVisibility') ? document.getElementById('filterVisibility').value : 'visible'; const fType = document.getElementById('filterType').value; const fStatus = document.getElementById('filterStatus').value; const fPriority = document.getElementById('filterPriority').value; const isGlobalView = (document.getElementById('current-view-title').innerText.includes('Todas las Tareas') || document.getElementById('current-view-title').innerText.includes('Global')); if (!currentSectionId && !isGlobalView && !query && !fType && !fStatus && !fPriority) { document.getElementById('board').innerHTML = '
Selecciona una sección en el panel izquierdo para ver o añadir tareas.
'; return; } if (currentSectionId) { document.getElementById('filterProject').style.display = 'none'; document.getElementById('filterArea').style.display = 'none'; document.getElementById('filterSection').style.display = 'none'; } let url = 'tasks?'; if (currentSectionId) { url += `section_id=${currentSectionId}&`; } else if (isGlobalView) { const pId = document.getElementById('filterProject').value; const aId = document.getElementById('filterArea').value; const sId = document.getElementById('filterSection').value; if (pId) url += `project_id=${pId}&`; if (aId) url += `area_id=${aId}&`; if (sId) url += `section_id=${sId}&`; } if (query) url += `search=${encodeURIComponent(query)}&`; if (showHidden) url += `show_hidden=true&`; if (fVisibility) url += `visibility=${fVisibility}&`; if (fType) url += `task_type_id=${fType}&`; if (fStatus) url += `status_id=${fStatus}&`; if (fPriority) url += `priority_id=${fPriority}&`; if (isTodoFilterActive) url += `todo=true&`; if (isForTodayFilterActive) url += `for_today=true&`; if (query && !isGlobalView) { document.getElementById('current-view-title').innerText = currentSectionId ? `Búsqueda en Sección: "${query}"` : `Búsqueda Global: "${query}"`; } else if (currentSectionId) { const activeNode = document.querySelector(`.tree-section span[onclick*="(${currentSectionId},"]`); if (activeNode) { const name = activeNode.innerText.replace('📄 ', ''); document.getElementById('current-view-title').innerText = `Sección: ${name}`; } } const tasks = await fetchAPI(url); const sortOrder = document.getElementById('sortOrder') ? document.getElementById('sortOrder').value : 'created_desc'; tasks.sort((a, b) => { if (sortOrder.startsWith('recent_comment')) { const maxA = a.comments && a.comments.length > 0 ? Math.max(...a.comments.map(c => new Date(c.created_at).getTime())) : 0; const maxB = b.comments && b.comments.length > 0 ? Math.max(...b.comments.map(c => new Date(c.created_at).getTime())) : 0; return sortOrder === 'recent_comment_desc' ? maxB - maxA : maxA - maxB; } else { const dateA = new Date(a.created_at).getTime(); const dateB = new Date(b.created_at).getTime(); return sortOrder === 'created_desc' ? dateB - dateA : dateA - dateB; } }); const board = document.getElementById('board'); board.innerHTML = ''; if (tasks.length === 0) { board.innerHTML = '
No hay tareas.
'; return; } const showLocation = (!currentSectionId && isGlobalView); let tableHtml = ` ${showLocation ? '' : ''} ${isTodoFilterActive ? '' : ''} `; tasks.forEach(task => { const prio = prioritiesMap[task.priority_id]; const status = statusesMap[task.status_id]; const tType = taskTypesMap[task.task_type_id]; let prioBadge = prio ? `${prio.name}` : '-'; let statusBadge = status ? `${status.name}` : '-'; let typeBadge = tType ? `${tType.name}` : '-'; let locationHtml = ''; if (showLocation) { if (task.section && task.section.area && task.section.area.project) { locationHtml = ``; } else { locationHtml = ``; } } let dynamicColumns = ''; if (isTodoFilterActive) { const startDateStr = task.start_date ? new Date(task.start_date).toLocaleDateString() : '-'; const endDateStr = task.end_date ? new Date(task.end_date).toLocaleDateString() : '-'; const todayStr = new Date().toISOString().split('T')[0]; const isForToday = task.for_today_date === todayStr; dynamicColumns = ` `; } else { const totalHours = (task.time_logs || []).reduce((acc, log) => acc + log.hours, 0); dynamicColumns = ` `; } tableHtml += ` ${locationHtml} ${dynamicColumns} `; }); tableHtml += `
Tipo NombreUbicaciónFecha Creación Responsable Prioridad EstadoFecha InicioFecha FinPara HoyHorasAdjuntosComentarios
${task.section.area.project.name} > ${task.section.area.name} > ${task.section.name}-${startDateStr} ${endDateStr} ⏱️ ${totalHours} 📎 ${task.files ? task.files.length : 0} 💬 ${task.comments ? task.comments.length : 0}
${typeBadge} ${task.name}${new Date(task.created_at).toLocaleDateString()} ${task.responsible || 'N/A'} ${prioBadge} ${statusBadge}
`; board.innerHTML = tableHtml; } // Creations async function createProject(e) { e.preventDefault(); const res = await fetchAPI('projects', 'POST', { name: document.getElementById('projName').value }); document.getElementById('projName').value = ''; if (res && res.id) expandedProjects.add(res.id); closeModal('projectModal'); loadSidebar(); } function openAreaModal(projId) { document.getElementById('areaProjId').value = projId; showModal('areaModal'); } async function createArea(e) { e.preventDefault(); const projId = parseInt(document.getElementById('areaProjId').value); await fetchAPI('areas', 'POST', { name: document.getElementById('areaName').value, project_id: projId }); document.getElementById('areaName').value = ''; expandedProjects.add(projId); closeModal('areaModal'); loadSidebar(); } function openTaskTypeModal(areaId) { document.getElementById('taskTypeAreaId').value = areaId; showModal('taskTypeModal'); } async function createTaskType(e) { e.preventDefault(); await fetchAPI('task_types', 'POST', { name: document.getElementById('taskTypeName').value, color: document.getElementById('taskTypeColor').value, area_id: parseInt(document.getElementById('taskTypeAreaId').value) }); document.getElementById('taskTypeName').value = ''; closeModal('taskTypeModal'); loadDropdowns(); if (currentAreaId) { const tTypes = await fetchAPI(`task_types?area_id=${currentAreaId}`); let optionsHtml = ''; tTypes.forEach(t => optionsHtml += ``); document.getElementById('taskType').innerHTML = optionsHtml; document.getElementById('editTaskType').innerHTML = optionsHtml; } } function openSectionModal(areaId) { document.getElementById('sectionAreaId').value = areaId; showModal('sectionModal'); } async function createSection(e) { e.preventDefault(); const areaId = parseInt(document.getElementById('sectionAreaId').value); await fetchAPI('sections', 'POST', { name: document.getElementById('sectionName').value, area_id: areaId }); document.getElementById('sectionName').value = ''; expandedAreas.add(areaId); closeModal('sectionModal'); loadSidebar(); } async function createTask(e) { e.preventDefault(); const startDate = document.getElementById('taskStartDate').value; const endDate = document.getElementById('taskEndDate').value; const task = { name: document.getElementById('taskName').value, description: document.getElementById('taskDesc').value, responsible: document.getElementById('taskResp').value, section_id: parseInt(document.getElementById('taskSectionId').value), task_type_id: parseInt(document.getElementById('taskType').value) || null, priority_id: parseInt(document.getElementById('taskPriority').value) || null, status_id: parseInt(document.getElementById('taskStatus').value) || null, start_date: startDate ? new Date(startDate).toISOString() : null, end_date: endDate ? new Date(endDate).toISOString() : null, visible: document.getElementById('taskVisible').checked }; await fetchAPI('tasks', 'POST', task); document.getElementById('taskName').value = ''; document.getElementById('taskDesc').value = ''; document.getElementById('taskResp').value = ''; document.getElementById('taskStartDate').value = ''; document.getElementById('taskEndDate').value = ''; document.getElementById('taskVisible').checked = true; closeModal('taskModal'); loadTasks(); loadSidebar(); } // Task Edit & Comments function toggleTaskVisibilityIcon() { const cb = document.getElementById('editTaskVisible'); cb.checked = !cb.checked; updateVisibilityIcon(cb.checked); } function updateVisibilityIcon(isVisible) { const btn = document.getElementById('visibilityToggleBtn'); if (isVisible) { btn.innerHTML = ``; btn.title = "Tarea Visible"; } else { btn.innerHTML = ``; btn.title = "Tarea Oculta"; } } async function openTaskDetails(taskId) { const task = await fetchAPI(`tasks/${taskId}`); document.getElementById('editTaskId').value = task.id; document.getElementById('editTaskSectionId').value = task.section_id; document.getElementById('editTaskCreatedAt').innerText = new Date(task.created_at).toLocaleString(); document.getElementById('editTaskName').value = task.name; document.getElementById('editTaskDesc').value = task.description || ''; document.getElementById('editTaskResp').value = task.responsible || ''; if (task.section && task.section.area) { const tTypes = await fetchAPI(`task_types?area_id=${task.section.area.id}`); let optionsHtml = ''; tTypes.forEach(t => optionsHtml += ``); document.getElementById('editTaskType').innerHTML = optionsHtml; } document.getElementById('editTaskType').value = task.task_type_id || ''; document.getElementById('editTaskPriority').value = task.priority_id || ''; document.getElementById('editTaskStatus').value = task.status_id || ''; document.getElementById('editTaskStartDate').value = task.start_date ? task.start_date.split('T')[0] : ''; document.getElementById('editTaskEndDate').value = task.end_date ? task.end_date.split('T')[0] : ''; document.getElementById('editTaskVisible').checked = task.visible; updateVisibilityIcon(task.visible); const totalHours = (task.time_logs || []).reduce((acc, log) => acc + log.hours, 0); document.getElementById('editTaskTotalHours').innerText = totalHours; window.currentTaskTimeLogs = task.time_logs || []; renderComments(task.comments); renderFiles(task.files); showModal('taskDetailModal'); } async function updateTask(e) { e.preventDefault(); const taskId = document.getElementById('editTaskId').value; const startDate = document.getElementById('editTaskStartDate').value; const endDate = document.getElementById('editTaskEndDate').value; const task = { name: document.getElementById('editTaskName').value, description: document.getElementById('editTaskDesc').value, responsible: document.getElementById('editTaskResp').value, section_id: parseInt(document.getElementById('editTaskSectionId').value), task_type_id: parseInt(document.getElementById('editTaskType').value) || null, priority_id: parseInt(document.getElementById('editTaskPriority').value) || null, status_id: parseInt(document.getElementById('editTaskStatus').value) || null, start_date: startDate ? new Date(startDate).toISOString() : null, end_date: endDate ? new Date(endDate).toISOString() : null, visible: document.getElementById('editTaskVisible').checked }; await fetchAPI(`tasks/${taskId}`, 'PUT', task); closeModal('taskDetailModal'); loadTasks(); loadSidebar(); } function renderComments(comments) { const list = document.getElementById('commentsList'); list.innerHTML = ''; if (!comments || comments.length === 0) { list.innerHTML = '
No hay comentarios aún.
'; return; } // Sort comments descending (newest first) const sortedComments = [...comments].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); sortedComments.forEach(c => { const div = document.createElement('div'); div.style.background = 'rgba(255,255,255,0.05)'; div.style.padding = '10px'; div.style.borderRadius = '8px'; div.style.marginBottom = '10px'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; const date = new Date(c.created_at).toLocaleString(); div.innerHTML = `
📅 ${date}
`; div.querySelector('.comment-text').innerHTML = marked.parse(c.content, { breaks: true }); div.querySelector('.btn-edit-comment').onclick = () => { document.getElementById('editCommentId').value = c.id; document.getElementById('editCommentTaskId').value = c.task_id; document.getElementById('editCommentContent').value = c.content; showModal('editCommentModal'); }; div.querySelector('.btn-del-comment').onclick = () => deleteComment(c.id, c.task_id); list.appendChild(div); }); } async function submitEditComment(e) { e.preventDefault(); const commentId = document.getElementById('editCommentId').value; const taskId = document.getElementById('editCommentTaskId').value; const content = document.getElementById('editCommentContent').value; await fetchAPI(`comments/${commentId}`, 'PUT', { content }); closeModal('editCommentModal'); openTaskDetails(taskId); loadTasks(); } async function deleteComment(commentId, taskId) { if (!confirm('¿Seguro que deseas eliminar este comentario?')) return; await fetchAPI(`comments/${commentId}`, 'DELETE'); openTaskDetails(taskId); loadTasks(); } async function addComment(e) { e.preventDefault(); const taskId = document.getElementById('editTaskId').value; const content = document.getElementById('newCommentContent').value; await fetchAPI(`tasks/${taskId}/comments`, 'POST', { content }); document.getElementById('newCommentContent').value = ''; openTaskDetails(taskId); loadTasks(); } function renderFiles(files) { const list = document.getElementById('filesList'); list.innerHTML = ''; if (!files || files.length === 0) { list.innerHTML = '
No hay archivos adjuntos.
'; return; } files.forEach(f => { const div = document.createElement('div'); div.style.background = 'rgba(255,255,255,0.05)'; div.style.padding = '10px'; div.style.borderRadius = '8px'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; div.style.alignItems = 'center'; div.innerHTML = `
📎 ${f.filename} ${new Date(f.created_at).toLocaleString()}
Descargar
`; list.appendChild(div); }); } function openFileLocal(filePath) { window.open(filePath, '_blank'); } let currentUploadMode = null; function updateFileCount(mode) { currentUploadMode = mode; const fileInput = document.getElementById('newTaskFile'); const folderInput = document.getElementById('newTaskFolder'); const textDiv = document.getElementById('fileSelectionText'); if (mode === 'file' && fileInput.files.length > 0) { folderInput.value = ''; const count = fileInput.files.length; textDiv.innerText = `${count} archivo(s) seleccionado(s)`; } else if (mode === 'folder' && folderInput.files.length > 0) { fileInput.value = ''; const count = folderInput.files.length; textDiv.innerText = `Carpeta con ${count} archivo(s) seleccionada`; } else { currentUploadMode = null; textDiv.innerText = 'Ningún archivo seleccionado'; } } async function uploadFile(e) { e.preventDefault(); const taskId = document.getElementById('editTaskId').value; let filesToUpload = []; if (currentUploadMode === 'file') { filesToUpload = document.getElementById('newTaskFile').files; } else if (currentUploadMode === 'folder') { filesToUpload = document.getElementById('newTaskFolder').files; } if (filesToUpload.length === 0) { alert('Por favor selecciona archivos o una carpeta primero.'); return; } const formData = new FormData(); for (let i = 0; i < filesToUpload.length; i++) { formData.append('files', filesToUpload[i]); } const res = await fetch(`/api/tasks/${taskId}/files`, { method: 'POST', body: formData }); if (res.ok) { document.getElementById('newTaskFile').value = ''; document.getElementById('newTaskFolder').value = ''; updateFileCount(null); openTaskDetails(taskId); } else { alert('Error al subir los archivos'); } } async function deleteFile(fileId, taskId) { if (!confirm('¿Seguro que deseas eliminar este archivo?')) return; const res = await fetch(`/api/tasks/files/${fileId}`, { method: 'DELETE' }); if (res.ok) { openTaskDetails(taskId); } else { alert('Error al eliminar el archivo'); } } async function deleteSection(sectionId) { if (!confirm('¿Seguro que deseas eliminar esta sección y TODAS sus tareas?')) return; const res = await fetch(`/api/sections/${sectionId}`, { method: 'DELETE' }); if (res.ok) { if (currentSectionId === sectionId) { currentSectionId = null; document.getElementById('current-view-title').innerText = 'Selecciona una Sección'; document.getElementById('btn-add-task').style.display = 'none'; document.getElementById('board').innerHTML = '
Selecciona una sección en el panel izquierdo para ver o añadir tareas.
'; } loadSidebar(); } else { alert('Error al eliminar la sección'); } } async function deleteTask(taskId) { if (!confirm('¿Seguro que deseas eliminar esta tarea?')) return; const res = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' }); if (res.ok) { closeModal('taskDetailModal'); loadTasks(); loadSidebar(); } else { alert('Error al eliminar la tarea'); } } // --- Admin Panel --- async function openAdminPanel() { document.getElementById('board').style.display = 'none'; if (document.getElementById('docs-board')) document.getElementById('docs-board').style.display = 'none'; document.getElementById('btn-add-task').style.display = 'none'; document.getElementById('admin-board').style.display = 'block'; document.getElementById('current-view-title').innerText = 'Administración'; await loadAdminProjects(); await loadAdminTaskTypeDropdowns(); } async function loadAdminProjects() { const projects = await fetchAPI('projects?include_hidden=true'); const container = document.getElementById('admin-projects-list'); let html = ''; projects.forEach(p => { html += `
${p.name}
${p.description || 'Sin descripción'}
`; }); if (projects.length === 0) { html = '
No hay proyectos.
'; } container.innerHTML = html; } async function toggleProjectVisibility(projectId, isVisible) { await fetchAPI(`projects/${projectId}/visibility?visible=${isVisible}`, 'PUT'); loadSidebar(); // Refresh sidebar automatically loadAdminProjects(); // Refresh UI to update colors } async function loadAdminTaskTypeDropdowns() { const projects = await fetchAPI('projects'); const projectSelect = document.getElementById('adminTaskTypeProject'); let html = ''; projects.forEach(p => { html += ``; }); projectSelect.innerHTML = html; document.getElementById('adminTaskTypeArea').innerHTML = ''; document.getElementById('admin-task-types-list').innerHTML = '
Selecciona un área para ver sus tipos de tareas.
'; } async function onAdminTaskTypeProjectChange() { const pId = document.getElementById('adminTaskTypeProject').value; const areaSelect = document.getElementById('adminTaskTypeArea'); const taskTypeList = document.getElementById('admin-task-types-list'); areaSelect.innerHTML = ''; taskTypeList.innerHTML = '
Selecciona un área para ver sus tipos de tareas.
'; if (pId) { const areas = await fetchAPI(`areas?project_id=${pId}`); let html = ''; areas.forEach(a => { html += ``; }); areaSelect.innerHTML = html; } } async function loadAdminTaskTypesFiltered() { const aId = document.getElementById('adminTaskTypeArea').value; const container = document.getElementById('admin-task-types-list'); if (!aId) { container.innerHTML = '
Selecciona un área para ver sus tipos de tareas.
'; return; } const taskTypes = await fetchAPI(`task_types?area_id=${aId}`); let html = ''; taskTypes.forEach(t => { html += `
${t.name}
`; }); if (taskTypes.length === 0) { html = '
No hay tipos de tareas en esta área.
'; } container.innerHTML = html; } function openEditTaskTypeAdmin(id, name, color) { document.getElementById('editTaskTypeIdAdmin').value = id; document.getElementById('editTaskTypeNameAdmin').value = name; document.getElementById('editTaskTypeColorAdmin').value = color; showModal('editTaskTypeModal'); } async function submitEditTaskType(e) { e.preventDefault(); const id = document.getElementById('editTaskTypeIdAdmin').value; const name = document.getElementById('editTaskTypeNameAdmin').value; const color = document.getElementById('editTaskTypeColorAdmin').value; await fetchAPI(`task_types/${id}`, 'PUT', { name, color }); closeModal('editTaskTypeModal'); loadAdminTaskTypesFiltered(); await loadDropdowns(); if (currentAreaId) { const tTypes = await fetchAPI(`task_types?area_id=${currentAreaId}`); let optionsHtml = ''; tTypes.forEach(t => optionsHtml += ``); document.getElementById('taskType').innerHTML = optionsHtml; document.getElementById('editTaskType').innerHTML = optionsHtml; } loadTasks(); } async function deleteTaskTypeAdmin(id) { if (!confirm('¿Seguro que deseas eliminar este tipo de tarea? Las tareas que lo tengan asignado quedarán sin tipo.')) return; const res = await fetch(`/api/task_types/${id}`, { method: 'DELETE' }); if (res.ok) { loadAdminTaskTypesFiltered(); await loadDropdowns(); if (currentAreaId) { const tTypes = await fetchAPI(`task_types?area_id=${currentAreaId}`); let optionsHtml = ''; tTypes.forEach(t => optionsHtml += ``); document.getElementById('taskType').innerHTML = optionsHtml; document.getElementById('editTaskType').innerHTML = optionsHtml; } loadTasks(); } else { alert('Error al eliminar el tipo de tarea'); } } // Task File Drag and Drop document.addEventListener('DOMContentLoaded', () => { const fileForm = document.getElementById('fileForm'); const dragOverlay = document.getElementById('task-drag-overlay'); if (!fileForm || !dragOverlay) return; let dragCounter = 0; fileForm.addEventListener('dragenter', (e) => { e.preventDefault(); dragCounter++; dragOverlay.style.display = 'flex'; }); fileForm.addEventListener('dragleave', (e) => { e.preventDefault(); dragCounter--; if (dragCounter === 0) { dragOverlay.style.display = 'none'; } }); fileForm.addEventListener('dragover', (e) => { e.preventDefault(); }); fileForm.addEventListener('drop', async (e) => { e.preventDefault(); dragCounter = 0; dragOverlay.style.display = 'none'; const taskId = document.getElementById('editTaskId').value; if (!taskId) return; const files = e.dataTransfer.files; if (!files || files.length === 0) return; const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } try { const res = await fetch(`/api/tasks/${taskId}/files`, { method: 'POST', body: formData }); if (res.ok) { document.getElementById('newTaskFile').value = ''; document.getElementById('newTaskFolder').value = ''; updateFileCount(null); openTaskDetails(taskId); } else { alert('Error al subir archivos'); } } catch (error) { alert('Error al subir archivos'); } }); }); // --- Time Logs --- function openTimeLogModal() { const today = new Date().toISOString().split('T')[0]; document.getElementById('timeLogDate').value = today; document.getElementById('timeLogHours').value = ''; renderTimeLogs(window.currentTaskTimeLogs); showModal('timeLogModal'); } function renderTimeLogs(logs) { const list = document.getElementById('timeLogsList'); list.innerHTML = ''; if (!logs || logs.length === 0) { list.innerHTML = '
No hay registros de horas.
'; return; } const sortedLogs = [...logs].sort((a, b) => new Date(b.date) - new Date(a.date)); sortedLogs.forEach(log => { const div = document.createElement('div'); div.style.background = 'rgba(255,255,255,0.05)'; div.style.padding = '8px'; div.style.borderRadius = '6px'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; div.style.alignItems = 'center'; const dateStr = new Date(log.date).toLocaleDateString(); div.innerHTML = `
${dateStr} ${log.hours}h
`; list.appendChild(div); }); } async function addTimeLog(e) { e.preventDefault(); const taskId = document.getElementById('editTaskId').value; const date = document.getElementById('timeLogDate').value; const hours = parseFloat(document.getElementById('timeLogHours').value); const isoDate = new Date(date).toISOString(); const res = await fetchAPI(`tasks/${taskId}/time_logs`, 'POST', { date: isoDate, hours: hours }); if (res) { await openTaskDetails(taskId); loadTasks(); renderTimeLogs(window.currentTaskTimeLogs); document.getElementById('timeLogHours').value = ''; } } async function deleteTimeLog(logId) { if (!confirm('¿Seguro que deseas eliminar este registro de horas?')) return; const taskId = document.getElementById('editTaskId').value; const res = await fetch(`/api/time_logs/${logId}`, { method: 'DELETE' }); if (res.ok) { await openTaskDetails(taskId); loadTasks(); renderTimeLogs(window.currentTaskTimeLogs); } else { alert('Error al eliminar el registro de horas.'); } } // --- Reportes de Administración --- async function generateTimeReport() { const startDate = document.getElementById('reportStartDate').value; const endDate = document.getElementById('reportEndDate').value; if (!startDate || !endDate) { alert('Por favor selecciona la fecha de inicio y fin.'); return; } const resultsDiv = document.getElementById('reportResults'); resultsDiv.style.display = 'block'; resultsDiv.innerHTML = '
Cargando...
'; const logs = await fetchAPI(`reports/time_logs?start_date=${startDate}&end_date=${endDate}`); if (!logs || logs.length === 0) { resultsDiv.innerHTML = '
No hay registros de horas en este periodo.
'; return; } let totalHoursPeriod = 0; const tasksMap = {}; logs.forEach(log => { totalHoursPeriod += log.hours; if (!tasksMap[log.task.id]) { tasksMap[log.task.id] = { name: log.task.name, totalHours: 0, logs: [] }; } tasksMap[log.task.id].totalHours += log.hours; tasksMap[log.task.id].logs.push(log); }); let html = `
Total del periodo: ${totalHoursPeriod} horas
`; Object.values(tasksMap).forEach(task => { html += `
📄 ${task.name} ${task.totalHours} horas
`; task.logs.forEach(log => { const dateStr = new Date(log.date).toLocaleDateString(); html += `
📅 ${dateStr} ${log.hours} h
`; }); html += `
`; }); html += `
`; resultsDiv.innerHTML = html; } // --- Todo Filter --- async function toggleForToday(taskId, checkbox) { const todayStr = new Date().toISOString().split('T')[0]; const newDate = checkbox.checked ? todayStr : null; await fetchAPI(`tasks/${taskId}/for_today`, 'PATCH', { for_today_date: newDate }); } function toggleTodoFilter() { isTodoFilterActive = !isTodoFilterActive; const btn = document.getElementById('btnTodoFilter'); if (isTodoFilterActive) { btn.style.background = 'rgba(16, 185, 129, 0.2)'; btn.style.color = '#10b981'; btn.style.borderColor = '#10b981'; // Ensure we are in a view where the project can be chosen const isGlobal = (document.getElementById('current-view-title').innerText.includes('Todas las Tareas') || document.getElementById('current-view-title').innerText.includes('Global')); if (!isGlobal || currentSectionId) { showAllTasks(); return; // showAllTasks already calls loadTasks } } else { btn.style.background = 'rgba(255,255,255,0.05)'; btn.style.color = 'white'; btn.style.borderColor = 'var(--border-color)'; } loadTasks(); } function toggleForTodayFilter() { isForTodayFilterActive = !isForTodayFilterActive; const btn = document.getElementById('btnForTodayFilter'); if (isForTodayFilterActive) { btn.style.background = 'rgba(236, 72, 153, 0.2)'; btn.style.color = '#ec4899'; btn.style.borderColor = '#ec4899'; const isGlobal = (document.getElementById('current-view-title').innerText.includes('Todas las Tareas') || document.getElementById('current-view-title').innerText.includes('Global')); if (!isGlobal || currentSectionId) { showAllTasks(); return; } } else { btn.style.background = 'rgba(255,255,255,0.05)'; btn.style.color = 'white'; btn.style.borderColor = 'var(--border-color)'; } loadTasks(); }