/** * TASKMATE - Main Application * =========================== * Entry point and initialization */ import store from './store.js'; import api from './api.js'; import authManager from './auth.js'; import syncManager from './sync.js'; import offlineManager from './offline.js'; import boardManager from './board.js'; import taskModalManager from './task-modal.js'; import calendarViewManager from './calendar.js'; import listViewManager from './list.js'; import shortcutsManager from './shortcuts.js'; import undoManager from './undo.js'; // Tour/Tutorial entfernt import adminManager from './admin.js'; import proposalsManager from './proposals.js'; import notificationManager from './notifications.js'; import giteaManager from './gitea.js'; import knowledgeManager from './knowledge.js'; import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js'; class App { constructor() { this.isInitialized = false; } // ===================== // INITIALIZATION // ===================== async init() { console.log('[App] Initializing...'); // Initialize offline storage await offlineManager.init(); // Check authentication const isAuthenticated = await authManager.init(); if (isAuthenticated) { // Check if user is admin if (authManager.isAdmin()) { await this.initializeAdminApp(); } else { await this.initializeApp(); } } else { this.showLoginScreen(); } // Bind global events this.bindGlobalEvents(); // Register service worker this.registerServiceWorker(); console.log('[App] Initialized'); this.isInitialized = true; } async initializeApp() { this.showAppScreen(); // Connect to WebSocket syncManager.connect(); // Load initial data await this.loadInitialData(); // Initialize proposals manager await proposalsManager.init(); // Initialize notification manager await notificationManager.init(); // Initialize gitea manager await giteaManager.init(); // Initialize knowledge manager await knowledgeManager.init(); // Update UI this.updateUserMenu(); } async initializeAdminApp() { // Initialize admin manager FIRST (loads DOM elements) await adminManager.init(); // Then show the screen this.showAdminScreen(); } // ===================== // DATA LOADING // ===================== async loadInitialData() { store.setLoading(true); try { // Load users const users = await api.getUsers(); store.setUsers(users); // Load projects const projects = await api.getProjects(); store.setProjects(projects); // Set current project let currentProjectId = getFromStorage('current_project_id'); if (!currentProjectId && projects.length > 0) { currentProjectId = projects[0].id; } if (currentProjectId) { await this.loadProject(currentProjectId); } // Populate project selector this.populateProjectSelector(); } catch (error) { console.error('[App] Failed to load initial data:', error); // Try loading from offline cache if (!navigator.onLine || error.isOffline) { await this.loadFromCache(); } else { this.showError('Fehler beim Laden der Daten'); } } finally { store.setLoading(false); } } async loadProject(projectId) { store.setCurrentProject(projectId); try { const [columns, tasks, labels] = await Promise.all([ api.getColumns(projectId), api.getTasks(projectId), api.getLabels(projectId) ]); store.setColumns(columns); store.setTasks(tasks); store.setLabels(labels); // Update project selector const projectSelect = $('#project-select'); if (projectSelect) { projectSelect.value = projectId; } } catch (error) { console.error('[App] Failed to load project:', error); if (!navigator.onLine || error.isOffline) { await offlineManager.loadOfflineData(); } else { throw error; } } } async loadFromCache() { console.log('[App] Loading from offline cache'); const projects = await offlineManager.getCachedProjects(); store.setProjects(projects); const currentProjectId = getFromStorage('current_project_id'); if (currentProjectId) { store.setCurrentProject(currentProjectId); await offlineManager.loadOfflineData(); } this.populateProjectSelector(); store.setSyncStatus('offline'); } // ===================== // UI SCREENS // ===================== showLoginScreen() { $('#login-screen')?.classList.remove('hidden'); $('#app-screen')?.classList.add('hidden'); $('#admin-screen')?.classList.remove('active'); } showAppScreen() { $('#login-screen')?.classList.add('hidden'); $('#app-screen')?.classList.remove('hidden'); $('#admin-screen')?.classList.remove('active'); } showAdminScreen() { $('#login-screen')?.classList.add('hidden'); $('#app-screen')?.classList.add('hidden'); adminManager.show(); } // ===================== // EVENT BINDING // ===================== bindGlobalEvents() { // Auth events window.addEventListener('auth:login', () => this.handleLogin()); window.addEventListener('auth:logout', () => this.handleLogout()); // View switching $$('.view-tab')?.forEach(tab => { tab.addEventListener('click', () => { const view = tab.dataset.view; this.switchView(view); }); }); // Project selector $('#project-select')?.addEventListener('change', (e) => { this.loadProject(parseInt(e.target.value)); }); // Add project button $('#btn-new-project')?.addEventListener('click', () => { this.openProjectModal('create'); }); // Edit project button $('#btn-edit-project')?.addEventListener('click', () => { const currentProject = store.getCurrentProject(); if (currentProject) { this.openProjectModal('edit', currentProject); } else { this.showError('Kein Projekt ausgewählt'); } }); // Search - Hybrid search with client-side filtering and server search this.setupSearch(); // Filter changes $('#filter-priority')?.addEventListener('change', (e) => { store.setFilter('priority', e.target.value); }); $('#filter-assignee')?.addEventListener('change', (e) => { store.setFilter('assignee', e.target.value); }); $('#filter-labels')?.addEventListener('change', (e) => { store.setFilter('label', e.target.value); }); $('#filter-due')?.addEventListener('change', (e) => { store.setFilter('dueDate', e.target.value); }); // Reset filters $('#btn-reset-filters')?.addEventListener('click', () => { store.resetFilters(); this.resetFilterInputs(); }); // Open archive modal $('#btn-show-archived')?.addEventListener('click', () => { this.openArchiveModal(); }); // Modal events window.addEventListener('modal:open', (e) => this.handleModalOpen(e.detail)); window.addEventListener('modal:close', (e) => this.handleModalClose(e.detail)); // Toast events window.addEventListener('toast:show', (e) => this.showToast(e.detail)); // Confirm dialog events window.addEventListener('confirm:show', (e) => this.showConfirmDialog(e.detail)); // Lightbox events window.addEventListener('lightbox:open', (e) => this.openLightbox(e.detail)); // App refresh window.addEventListener('app:refresh', () => this.refresh()); // Notification navigation - open task from inbox window.addEventListener('notification:open-task', (e) => { const taskId = e.detail.taskId; if (taskId) { // Switch to board view first this.switchView('board'); // Open task modal setTimeout(() => { window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'task-modal', mode: 'edit', data: { taskId } } })); }, 100); } }); // Notification navigation - open proposal from inbox window.addEventListener('notification:open-proposal', (e) => { const proposalId = e.detail.proposalId; if (proposalId) { // Switch to proposals view this.switchView('proposals'); // Highlight the proposal after view is loaded setTimeout(() => { proposalsManager.scrollToAndHighlight(proposalId); }, 300); } }); // Online/Offline window.addEventListener('online', () => this.handleOnline()); window.addEventListener('offline', () => this.handleOffline()); // Close modal on overlay click $('.modal-overlay')?.addEventListener('click', () => { // Check if task-modal is open - let it handle its own close (with auto-save) if (store.isModalOpen('task-modal')) { window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'task-modal' } })); } else { store.closeAllModals(); this.hideAllModals(); } }); // Column modal form $('#column-form')?.addEventListener('submit', (e) => this.handleColumnFormSubmit(e)); // Column filter category dropdown - show/hide custom filter input $('#column-filter-category')?.addEventListener('change', (e) => { const customGroup = $('#column-custom-filter-group'); if (customGroup) { customGroup.classList.toggle('hidden', e.target.value !== 'custom'); } }); // Project modal form $('#project-form')?.addEventListener('submit', (e) => this.handleProjectFormSubmit(e)); // Delete project button $('#btn-delete-project')?.addEventListener('click', () => this.handleDeleteProject()); // Label modal form $('#label-form')?.addEventListener('submit', (e) => this.handleLabelFormSubmit(e)); // Export button $('#btn-export')?.addEventListener('click', () => this.exportProject()); // Import button $('#btn-import')?.addEventListener('click', () => this.importProject()); // User menu toggle $('#user-menu-btn')?.addEventListener('click', (e) => { e.stopPropagation(); this.toggleUserMenu(); }); // Close user menu when clicking outside document.addEventListener('click', (e) => { const userMenu = $('.user-menu'); if (userMenu && !userMenu.contains(e.target)) { $('#user-dropdown')?.classList.add('hidden'); } }); // Logout button $('#btn-logout')?.addEventListener('click', () => { authManager.logout(); }); // Settings button $('#btn-settings')?.addEventListener('click', () => { this.handleModalOpen({ modalId: 'settings-modal' }); $('#user-dropdown')?.classList.add('hidden'); this.initColorPicker(); }); // Color picker options (preset colors) $('#user-color-options')?.addEventListener('click', (e) => { const colorOption = e.target.closest('.color-option'); if (colorOption) { this.handleColorSelect(colorOption.dataset.color); } }); // Color picker custom color input - use 'change' to only save when selection is complete $('#user-color-custom')?.addEventListener('change', (e) => { this.handleColorSelect(e.target.value); }); // Also update preview in real-time while selecting (without saving) $('#user-color-custom')?.addEventListener('input', (e) => { const preview = $('#user-color-preview'); if (preview) { preview.style.backgroundColor = e.target.value; } }); // Modal close buttons (data-close-modal) $$('[data-close-modal]')?.forEach(btn => { btn.addEventListener('click', () => { const modal = btn.closest('.modal'); if (modal) { this.handleModalClose({ modalId: modal.id }); } }); }); // Change password form $('#change-password-form')?.addEventListener('submit', (e) => this.handleChangePassword(e)); } // ===================== // PASSWORD CHANGE // ===================== async handleChangePassword(e) { e.preventDefault(); const currentPassword = $('#current-password')?.value; const newPassword = $('#new-password')?.value; const confirmPassword = $('#confirm-password')?.value; const errorEl = $('#password-error'); // Validation if (!currentPassword || !newPassword || !confirmPassword) { this.showPasswordError('Bitte alle Felder ausfüllen.'); return; } if (newPassword.length < 8) { this.showPasswordError('Das neue Passwort muss mindestens 8 Zeichen lang sein.'); return; } if (newPassword !== confirmPassword) { this.showPasswordError('Die Passwörter stimmen nicht überein.'); return; } try { await authManager.changePassword(currentPassword, newPassword); // Success this.handleModalClose({ modalId: 'settings-modal' }); $('#change-password-form')?.reset(); if (errorEl) errorEl.classList.add('hidden'); this.showSuccess('Passwort erfolgreich geändert'); } catch (error) { this.showPasswordError(error.message || 'Fehler beim Ändern des Passworts.'); } } showPasswordError(message) { const errorEl = $('#password-error'); if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); } } // ===================== // COLOR PICKER // ===================== initColorPicker() { const currentUser = authManager.getUser(); const currentColor = currentUser?.color || authManager.getUserColor(); // Update preview const preview = $('#user-color-preview'); const previewInitial = $('#user-color-preview-initial'); if (preview) { preview.style.backgroundColor = currentColor; } if (previewInitial && currentUser) { previewInitial.textContent = currentUser.username || 'U'; } // Update custom color picker const customColorInput = $('#user-color-custom'); if (customColorInput) { customColorInput.value = currentColor; } // Mark selected color (if it matches a preset) $$('.color-option').forEach(option => { option.classList.toggle('selected', option.dataset.color.toUpperCase() === currentColor.toUpperCase()); }); } async handleColorSelect(color) { if (!color) return; // Update preview immediately const preview = $('#user-color-preview'); if (preview) { preview.style.backgroundColor = color; } // Update custom color picker const customColorInput = $('#user-color-custom'); if (customColorInput) { customColorInput.value = color; } // Update selected state for presets $$('.color-option').forEach(option => { option.classList.toggle('selected', option.dataset.color.toUpperCase() === color.toUpperCase()); }); try { // Save to backend await api.updateUserColor(color); // Update authManager (also updates localStorage) authManager.updateUserColor(color); // Get current user from authManager const currentUser = authManager.getUser(); // Update users list in store if (currentUser) { const users = store.get('users'); const updatedUsers = users.map(u => u.id === currentUser.id ? { ...u, color } : u ); store.setUsers(updatedUsers); } // Update user avatar in header const userAvatar = $('#user-menu-btn'); if (userAvatar) { userAvatar.style.backgroundColor = color; } // Re-render board to update task cards with new color boardManager.render(); // Re-render calendar if active if (store.get('currentView') === 'calendar') { calendarViewManager.render(); } this.showSuccess('Farbe erfolgreich geaendert'); } catch (error) { this.showError(error.message || 'Fehler beim Speichern der Farbe'); } } // ===================== // AUTH HANDLERS // ===================== async handleLogin() { // Check if user is admin if (authManager.isAdmin()) { await this.initializeAdminApp(); } else { await this.initializeApp(); } } handleLogout() { store.reset(); syncManager.disconnect(); adminManager.hide(); notificationManager.reset(); this.showLoginScreen(); } // ===================== // VIEW SWITCHING // ===================== switchView(view) { store.setCurrentView(view); // Update tabs $$('.view-tab')?.forEach(tab => { tab.classList.toggle('active', tab.dataset.view === view); }); // Show/hide views $$('.view')?.forEach(v => { const isActive = v.id === `view-${view}`; v.classList.toggle('active', isActive); v.classList.toggle('hidden', !isActive); }); // Clear search field when switching views const searchInput = $('#search-input'); if (searchInput && searchInput.value) { searchInput.value = ''; store.setFilter('search', ''); store.setState({ searchResultIds: [] }, 'CLEAR_SEARCH_RESULTS'); proposalsManager.setSearchQuery(''); knowledgeManager.setSearchQuery(''); $('#search-clear')?.classList.add('hidden'); $('.search-container')?.classList.remove('has-search'); } // Load proposals when switching to proposals view - reset to active (non-archived) if (view === 'proposals') { proposalsManager.resetToActiveView(); } // Show/hide gitea manager if (view === 'gitea') { giteaManager.show(); } else { giteaManager.hide(); } // Show/hide knowledge manager if (view === 'knowledge') { knowledgeManager.show(); } else { knowledgeManager.hide(); } } // ===================== // PROJECT SELECTOR // ===================== populateProjectSelector() { const select = $('#project-select'); if (!select) return; const projects = store.get('projects'); select.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.id; option.textContent = project.name; select.appendChild(option); }); // Set current value const currentProjectId = store.get('currentProjectId'); if (currentProjectId) { select.value = currentProjectId; } // Also populate filter assignee this.populateAssigneeFilter(); this.populateLabelFilter(); } populateAssigneeFilter() { const select = $('#filter-assignee'); if (!select) return; const users = store.get('users'); select.innerHTML = ''; users.forEach(user => { const option = document.createElement('option'); option.value = user.id; option.textContent = user.username; select.appendChild(option); }); } populateLabelFilter() { const select = $('#filter-labels'); if (!select) return; const labels = store.get('labels'); select.innerHTML = ''; labels.forEach(label => { const option = document.createElement('option'); option.value = label.id; option.textContent = label.name; select.appendChild(option); }); } resetFilterInputs() { const priority = $('#filter-priority'); const assignee = $('#filter-assignee'); const labels = $('#filter-labels'); const dueDate = $('#filter-due'); const search = $('#search-input'); const searchClear = $('#search-clear'); const searchContainer = $('.search-container'); if (priority) priority.value = 'all'; if (assignee) assignee.value = 'all'; if (labels) labels.value = 'all'; if (dueDate) dueDate.value = ''; if (search) search.value = ''; if (searchClear) searchClear.classList.add('hidden'); if (searchContainer) searchContainer.classList.remove('has-search'); } openArchiveModal() { this.handleModalOpen({ modalId: 'archive-modal' }); this.renderArchiveList(); } renderArchiveList() { const archiveList = $('#archive-list'); const archiveEmpty = $('#archive-empty'); if (!archiveList) return; // Get all archived tasks const tasks = store.get('tasks').filter(t => t.archived); const columns = store.get('columns'); if (tasks.length === 0) { archiveList.classList.add('hidden'); archiveEmpty?.classList.remove('hidden'); return; } archiveList.classList.remove('hidden'); archiveEmpty?.classList.add('hidden'); // Sort by archived date (most recent first) - use updatedAt as proxy tasks.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0)); const priorityLabels = { high: 'Hoch', medium: 'Mittel', low: 'Niedrig' }; archiveList.innerHTML = tasks.map(task => { const column = columns.find(c => c.id === task.columnId); const columnName = column?.name || 'Unbekannt'; return `