/** * TekDek Command Center - UI Controller * Manages DOM interactions and state */ class UIController { constructor() { this.currentProjectId = null; this.currentEditingTaskId = null; this.draggedTaskId = null; this.draggedFromColumn = null; this.tasks = []; this.projects = []; this.taskStatusMap = { 'backlog': 'Backlog', 'in_progress': 'In Progress', 'blocked': 'Blocked', 'done': 'Done' }; this.iconMap = { 'rocket': '🚀', 'bug': '🐛', 'feature': '⭐', 'docs': '📚', 'deploy': '🚢', 'design': '🎨' }; } // ==================== INITIALIZATION ==================== init() { this.cacheElements(); this.attachEventListeners(); this.checkConnection(); } cacheElements() { // Views this.projectsListView = document.getElementById('projects-list-view'); this.projectDetailView = document.getElementById('project-detail-view'); // Projects list this.projectsList = document.getElementById('projects-list'); this.filterStatus = document.getElementById('filter-status'); // Project detail this.projectTitle = document.getElementById('project-title'); this.projectDescription = document.getElementById('project-description'); this.projectTaskCount = document.getElementById('project-task-count'); this.projectDoneCount = document.getElementById('project-done-count'); this.projectOverdueCount = document.getElementById('project-overdue-count'); this.taskFilterStatus = document.getElementById('task-filter-status'); this.kanbanBoard = document.getElementById('kanban-board'); // Modals this.modalNewProject = document.getElementById('modal-new-project'); this.formNewProject = document.getElementById('form-new-project'); this.modalNewTask = document.getElementById('modal-new-task'); this.formNewTask = document.getElementById('form-new-task'); this.modalEditTask = document.getElementById('modal-edit-task'); this.formEditTask = document.getElementById('form-edit-task'); // Buttons this.btnNewProject = document.getElementById('btn-new-project'); this.btnBackToProjects = document.getElementById('btn-back-to-projects'); this.btnAddTask = document.getElementById('btn-add-task'); this.btnDeleteTask = document.getElementById('btn-delete-task'); // Toast this.toastContainer = document.getElementById('toast-container'); // Status this.connectionStatus = document.getElementById('connection-status'); this.connectionStatusDot = this.connectionStatus.querySelector('.status-dot'); } attachEventListeners() { // Navigation this.btnNewProject.addEventListener('click', () => this.showNewProjectModal()); this.btnBackToProjects.addEventListener('click', () => this.showProjectsList()); this.btnAddTask.addEventListener('click', () => this.showNewTaskModal()); // Filters this.filterStatus.addEventListener('change', () => this.loadProjects()); this.taskFilterStatus.addEventListener('change', () => this.loadTasksForProject()); // New Project Form this.formNewProject.addEventListener('submit', (e) => this.handleNewProjectSubmit(e)); document.getElementById('project-color').addEventListener('change', (e) => { document.getElementById('color-display').style.backgroundColor = e.target.value; }); // New Task Form this.formNewTask.addEventListener('submit', (e) => this.handleNewTaskSubmit(e)); // Edit Task Form this.formEditTask.addEventListener('submit', (e) => this.handleEditTaskSubmit(e)); this.btnDeleteTask.addEventListener('click', () => this.handleDeleteTask()); // Modal closing document.querySelectorAll('.modal-close, .modal-backdrop, .btn-close').forEach(el => { el.addEventListener('click', (e) => { if (e.target.classList.contains('modal-close') || e.target.classList.contains('modal-backdrop') || e.target.classList.contains('btn-close')) { this.closeModals(); } }); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') this.closeModals(); }); } // ==================== CONNECTION ==================== async checkConnection() { const connected = await api.checkConnection(); if (connected) { this.setConnectionStatus(true); } else { this.setConnectionStatus(false); } } setConnectionStatus(connected) { if (connected) { this.connectionStatusDot.style.backgroundColor = '#27ae60'; this.connectionStatus.querySelector('.status-text').textContent = 'Connected'; } else { this.connectionStatusDot.style.backgroundColor = '#e74c3c'; this.connectionStatus.querySelector('.status-text').textContent = 'Disconnected'; } } // ==================== VIEW SWITCHING ==================== switchView(viewName) { document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.getElementById(viewName).classList.add('active'); } // ==================== PROJECTS LIST VIEW ==================== async loadProjects() { this.projectsList.innerHTML = '

Loading projects...

'; try { const status = this.filterStatus.value; const options = status ? { status } : {}; this.projects = await api.getProjects(options); this.renderProjectsList(); } catch (error) { this.showToast(`Error loading projects: ${error.message}`, 'error'); this.projectsList.innerHTML = '

Failed to load projects

'; } } renderProjectsList() { if (this.projects.length === 0) { this.projectsList.innerHTML = `

No projects yet

`; return; } this.projectsList.innerHTML = this.projects.map(project => `
${this.iconMap[project.icon_name] || '📋'}
${this.escapeHtml(project.name)}
${project.status}
${project.description ? `
${this.escapeHtml(project.description)}
` : ''}
Tasks ${project.task_count || 0}
Done ${project.completed_count || 0}
Overdue ${project.overdue_count || 0}
`).join(''); } showProjectsList() { this.switchView('projects-list-view'); this.loadProjects(); } // ==================== PROJECT DETAIL VIEW ==================== async showProjectDetail(projectId) { this.currentProjectId = projectId; this.switchView('project-detail-view'); try { const project = await api.getProject(projectId); this.projectTitle.textContent = project.name; this.projectDescription.textContent = project.description || ''; this.projectTaskCount.textContent = project.task_count || 0; this.projectDoneCount.textContent = project.completed_count || 0; this.projectOverdueCount.textContent = project.overdue_count || 0; await this.loadTasksForProject(); } catch (error) { this.showToast(`Error loading project: ${error.message}`, 'error'); this.showProjectsList(); } } async loadTasksForProject() { const status = this.taskFilterStatus.value; try { const options = status ? { status } : {}; this.tasks = await api.getTasks(this.currentProjectId, options); this.renderKanbanBoard(); } catch (error) { this.showToast(`Error loading tasks: ${error.message}`, 'error'); } } renderKanbanBoard() { const statuses = ['backlog', 'in_progress', 'blocked', 'done']; this.kanbanBoard.innerHTML = statuses.map(status => { const tasksByStatus = this.tasks.filter(t => t.status === status); return `
${this.taskStatusMap[status]}
${tasksByStatus.length}
${tasksByStatus.map(task => this.renderTaskCard(task)).join('')}
`; }).join(''); } renderTaskCard(task) { const dueDate = task.due_date ? new Date(task.due_date) : null; const today = new Date(); today.setHours(0, 0, 0, 0); let dueDateClass = ''; let dueDateText = ''; if (dueDate && task.status !== 'done') { dueDate.setHours(0, 0, 0, 0); if (dueDate < today) { dueDateClass = 'overdue'; dueDateText = `📅 ${dueDate.toLocaleDateString()} (OVERDUE)`; } else if ((dueDate - today) / (1000 * 60 * 60 * 24) <= 3) { dueDateClass = 'soon'; dueDateText = `📅 ${dueDate.toLocaleDateString()} (Soon)`; } else { dueDateText = `📅 ${dueDate.toLocaleDateString()}`; } } return `
${this.escapeHtml(task.title)}
${task.description ? `
${this.escapeHtml(task.description)}
` : ''}
${dueDateText ? `
${dueDateText}
` : ''}
`; } // ==================== DRAG AND DROP ==================== handleTaskDragStart(event) { this.draggedTaskId = parseInt(event.target.closest('.task-card').dataset.taskId); this.draggedFromColumn = event.target.closest('.kanban-list'); event.dataTransfer.effectAllowed = 'move'; event.target.closest('.task-card').classList.add('dragging'); } handleTaskDragEnd(event) { event.target.closest('.task-card').classList.remove('dragging'); document.querySelectorAll('.kanban-list').forEach(list => { list.classList.remove('dragging-over'); }); this.draggedTaskId = null; this.draggedFromColumn = null; } handleDragOver(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; event.currentTarget.classList.add('dragging-over'); } handleDragLeave(event) { if (event.currentTarget === event.target) { event.currentTarget.classList.remove('dragging-over'); } } async handleTaskDrop(event) { event.preventDefault(); event.currentTarget.classList.remove('dragging-over'); if (!this.draggedTaskId) return; const targetStatus = event.currentTarget.dataset.status; const targetTasks = Array.from(event.currentTarget.querySelectorAll('.task-card')).map(card => parseInt(card.dataset.taskId) ); try { // First update the task status await api.updateTask(this.currentProjectId, this.draggedTaskId, { status: targetStatus }); // Then reorder tasks if needed const allTasksInColumn = this.tasks.filter(t => t.status === targetStatus); const newOrder = allTasksInColumn.map(t => t.id); if (newOrder.length > 1) { await api.reorderTasks(this.currentProjectId, newOrder); } await this.loadTasksForProject(); this.showToast('Task updated', 'success'); } catch (error) { this.showToast(`Error moving task: ${error.message}`, 'error'); } } // ==================== MODALS ==================== showNewProjectModal() { this.formNewProject.reset(); document.getElementById('project-color').value = '#3498db'; document.getElementById('color-display').style.backgroundColor = '#3498db'; this.modalNewProject.classList.add('active'); } showNewTaskModal() { this.formNewTask.reset(); this.modalNewTask.classList.add('active'); } showEditTaskModal(taskId, event) { event.stopPropagation(); const task = this.tasks.find(t => t.id === taskId); if (!task) return; this.currentEditingTaskId = taskId; document.getElementById('edit-task-title').value = task.title; document.getElementById('edit-task-desc').value = task.description || ''; document.getElementById('edit-task-status').value = task.status; document.getElementById('edit-task-due').value = task.due_date || ''; this.modalEditTask.classList.add('active'); } closeModals() { this.modalNewProject.classList.remove('active'); this.modalNewTask.classList.remove('active'); this.modalEditTask.classList.remove('active'); } // ==================== FORM HANDLERS ==================== async handleNewProjectSubmit(e) { e.preventDefault(); const formData = new FormData(this.formNewProject); const data = { name: formData.get('name'), description: formData.get('description'), color_hex: formData.get('color_hex'), icon_name: formData.get('icon_name') }; try { await api.createProject(data); this.closeModals(); this.showToast('Project created successfully', 'success'); await this.loadProjects(); } catch (error) { this.showToast(`Error creating project: ${error.message}`, 'error'); } } async handleNewTaskSubmit(e) { e.preventDefault(); const formData = new FormData(this.formNewTask); const data = { title: formData.get('title'), description: formData.get('description'), status: formData.get('status'), due_date: formData.get('due_date') }; try { await api.createTask(this.currentProjectId, data); this.closeModals(); this.showToast('Task created successfully', 'success'); await this.loadTasksForProject(); } catch (error) { this.showToast(`Error creating task: ${error.message}`, 'error'); } } async handleEditTaskSubmit(e) { e.preventDefault(); const formData = new FormData(this.formEditTask); const data = { title: formData.get('title'), description: formData.get('description'), status: formData.get('status'), due_date: formData.get('due_date') }; try { await api.updateTask(this.currentProjectId, this.currentEditingTaskId, data); this.closeModals(); this.showToast('Task updated successfully', 'success'); await this.loadTasksForProject(); } catch (error) { this.showToast(`Error updating task: ${error.message}`, 'error'); } } async handleDeleteTask() { if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) { return; } try { await api.deleteTask(this.currentProjectId, this.currentEditingTaskId); this.closeModals(); this.showToast('Task deleted successfully', 'success'); await this.loadTasksForProject(); } catch (error) { this.showToast(`Error deleting task: ${error.message}`, 'error'); } } // ==================== UTILITIES ==================== showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `
${this.escapeHtml(message)}
`; this.toastContainer.appendChild(toast); setTimeout(() => { if (toast.parentElement) { toast.remove(); } }, 4000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Create global UI controller instance const ui = new UIController();