/** * 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 `
${this.escapeHtml(task.title)}
${priorityLabels[task.priority] || 'Mittel'} Spalte: ${this.escapeHtml(columnName)} ${task.dueDate ? `Fällig: ${new Date(task.dueDate).toLocaleDateString('de-DE')}` : ''}
`; }).join(''); } async restoreTask(taskId) { try { const projectId = store.get('currentProjectId'); await api.restoreTask(projectId, taskId); store.updateTask(taskId, { archived: false }); this.renderArchiveList(); this.showSuccess('Aufgabe wiederhergestellt'); // Re-render board if (this.board) { this.board.render(); } } catch (error) { console.error('Restore error:', error); this.showError('Fehler beim Wiederherstellen'); } } openArchivedTask(taskId) { // Close archive modal first this.handleModalClose({ modalId: 'archive-modal' }); // Open task modal if (this.taskModal) { this.taskModal.open(taskId); } } async deleteArchivedTask(taskId) { window.dispatchEvent(new CustomEvent('confirm:show', { detail: { message: 'Möchten Sie diese Aufgabe endgültig löschen?', confirmText: 'Löschen', confirmClass: 'btn-danger', onConfirm: async () => { try { const projectId = store.get('currentProjectId'); await api.deleteTask(projectId, taskId); store.removeTask(taskId); this.renderArchiveList(); this.showSuccess('Aufgabe gelöscht'); } catch (error) { this.showError('Fehler beim Löschen'); } } } })); } escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ===================== // SEARCH // ===================== setupSearch() { const searchInput = $('#search-input'); const searchClear = $('#search-clear'); const searchSpinner = $('#search-spinner'); const searchContainer = $('.search-container'); if (!searchInput) return; // Abort controller for canceling pending server requests let searchAbortController = null; // Update UI based on search state const updateSearchUI = (value) => { const hasValue = value && value.trim().length > 0; searchClear?.classList.toggle('hidden', !hasValue); searchContainer?.classList.toggle('has-search', hasValue); }; // Clear search function const clearSearch = () => { searchInput.value = ''; store.setFilter('search', ''); store.setState({ searchResultIds: [] }, 'CLEAR_SEARCH_RESULTS'); updateSearchUI(''); searchInput.focus(); // Clear view-specific search proposalsManager.setSearchQuery(''); knowledgeManager.setSearchQuery(''); // Cancel any pending server search if (searchAbortController) { searchAbortController.abort(); searchAbortController = null; } searchSpinner?.classList.add('hidden'); }; // Perform server search (for deep search in subtasks, links, etc.) const performServerSearch = debounce(async (query) => { if (!query || query.trim().length < 2) { searchSpinner?.classList.add('hidden'); // Clear server search results when query is too short store.setState({ searchResultIds: [] }, 'CLEAR_SEARCH_RESULTS'); return; } const projectId = store.get('currentProjectId'); if (!projectId) { searchSpinner?.classList.add('hidden'); return; } // Cancel previous request if (searchAbortController) { searchAbortController.abort(); } searchAbortController = new AbortController(); try { searchSpinner?.classList.remove('hidden'); const results = await api.searchTasks(projectId, query); // Store the IDs of tasks found by server search // These will bypass the client-side search filter const serverResultIds = results.map(t => t.id); store.setState({ searchResultIds: serverResultIds }, 'SET_SEARCH_RESULTS'); // Merge server results with current tasks (add any that aren't already loaded) const currentTasks = store.get('tasks'); const currentTaskIds = new Set(currentTasks.map(t => t.id)); const newTasks = results.filter(t => !currentTaskIds.has(t.id)); if (newTasks.length > 0) { // Add newly found tasks to the store store.setState({ tasks: [...currentTasks, ...newTasks] }, 'SEARCH_RESULTS_MERGED'); } } catch (error) { if (error.name !== 'AbortError') { console.error('Server search failed:', error); } } finally { searchSpinner?.classList.add('hidden'); searchAbortController = null; } }, 500); // Input event - immediate client-side filter + delayed server search searchInput.addEventListener('input', (e) => { const value = e.target.value; updateSearchUI(value); // Check current view const currentView = store.get('currentView'); if (currentView === 'proposals') { // Search proposals only proposalsManager.setSearchQuery(value); } else if (currentView === 'knowledge') { // Search knowledge base knowledgeManager.setSearchQuery(value); } else { // Immediate client-side filtering for tasks store.setFilter('search', value); // Delayed server search for deep content (subtasks, links, attachments, comments) performServerSearch(value); } }); // Clear button click searchClear?.addEventListener('click', clearSearch); // Escape key to clear search searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); clearSearch(); } }); // Global Escape key handler (when search input is not focused) document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && searchInput.value && document.activeElement !== searchInput) { clearSearch(); } }); // Initialize UI state (in case there's a value from before) updateSearchUI(searchInput.value); } // ===================== // USER MENU // ===================== updateUserMenu() { const user = authManager.getUser(); if (!user) return; const avatar = $('#user-initial'); if (avatar) { avatar.textContent = authManager.getUserInitials(); } const avatarBtn = $('#user-menu-btn'); if (avatarBtn) { avatarBtn.style.backgroundColor = authManager.getUserColor(); } const userName = $('#user-name'); if (userName) userName.textContent = user.username; const userRole = $('#user-role'); if (userRole) userRole.textContent = user.role === 'admin' ? 'Administrator' : 'Benutzer'; } toggleUserMenu() { const dropdown = $('#user-dropdown'); if (dropdown) { dropdown.classList.toggle('hidden'); } } // ===================== // MODALS // ===================== handleModalOpen(detail) { const { modalId, mode, data } = detail; const modal = $(`#${modalId}`); const overlay = $('.modal-overlay'); if (modal) { modal.classList.remove('hidden'); modal.classList.add('visible'); modal.dataset.mode = mode || 'create'; if (data) { modal.dataset.data = JSON.stringify(data); } // Handle column-modal specific setup if (modalId === 'column-modal') { const titleEl = $('#column-modal-title'); const nameInput = $('#column-name'); const colorInput = $('#column-color'); const deleteBtn = $('#btn-delete-column'); if (mode === 'edit' && data) { if (titleEl) titleEl.textContent = 'Spalte bearbeiten'; if (nameInput) nameInput.value = data.name || ''; if (colorInput) colorInput.value = data.color || '#00D4FF'; if (deleteBtn) deleteBtn.style.display = ''; } else { if (titleEl) titleEl.textContent = 'Neue Spalte'; if (nameInput) nameInput.value = ''; if (colorInput) colorInput.value = '#00D4FF'; if (deleteBtn) deleteBtn.style.display = 'none'; } } } if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } store.openModal(modalId); } handleModalClose(detail) { const { modalId } = detail; const modal = $(`#${modalId}`); const overlay = $('.modal-overlay'); if (modal) { modal.classList.remove('visible'); setTimeout(() => modal.classList.add('hidden'), 200); } // Only hide overlay if no other modals are open const openModals = store.get('openModals').filter(id => id !== modalId); if (openModals.length === 0 && overlay) { overlay.classList.remove('visible'); setTimeout(() => overlay.classList.add('hidden'), 200); } store.closeModal(modalId); } hideAllModals() { $$('.modal').forEach(modal => { modal.classList.remove('visible'); setTimeout(() => modal.classList.add('hidden'), 200); }); const overlay = $('.modal-overlay'); if (overlay) { overlay.classList.remove('visible'); setTimeout(() => overlay.classList.add('hidden'), 200); } } // ===================== // TOAST NOTIFICATIONS // ===================== showToast({ message, type = 'info', duration = 4000 }) { const container = $('#toast-container') || this.createToastContainer(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icon = this.getToastIcon(type); toast.innerHTML = ` ${icon} ${message} `; // Close button handler toast.querySelector('.toast-close')?.addEventListener('click', () => { this.removeToast(toast); }); container.appendChild(toast); // Trigger animation requestAnimationFrame(() => { toast.classList.add('visible'); }); // Auto remove if (duration > 0) { setTimeout(() => this.removeToast(toast), duration); } } createToastContainer() { const container = document.createElement('div'); container.id = 'toast-container'; container.className = 'toast-container'; document.body.appendChild(container); return container; } removeToast(toast) { toast.classList.remove('visible'); setTimeout(() => toast.remove(), 300); } getToastIcon(type) { const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; return icons[type] || icons.info; } showError(message) { this.showToast({ message, type: 'error' }); } showSuccess(message) { this.showToast({ message, type: 'success' }); } // ===================== // CONFIRM DIALOG // ===================== showConfirmDialog({ message, confirmText = 'OK', confirmClass = '', onConfirm }) { const modal = $('#confirm-modal'); const messageEl = $('#confirm-message'); const confirmBtn = $('#confirm-ok'); const cancelBtn = $('#confirm-cancel'); if (messageEl) messageEl.textContent = message; if (confirmBtn) { confirmBtn.textContent = confirmText; confirmBtn.className = `btn ${confirmClass || 'btn-primary'}`; } // Show modal this.handleModalOpen({ modalId: 'confirm-modal' }); // One-time event handlers const handleConfirm = () => { this.handleModalClose({ modalId: 'confirm-modal' }); if (onConfirm) onConfirm(); cleanup(); }; const handleCancel = () => { this.handleModalClose({ modalId: 'confirm-modal' }); cleanup(); }; const cleanup = () => { confirmBtn?.removeEventListener('click', handleConfirm); cancelBtn?.removeEventListener('click', handleCancel); }; confirmBtn?.addEventListener('click', handleConfirm); cancelBtn?.addEventListener('click', handleCancel); } // ===================== // LIGHTBOX // ===================== openLightbox({ imageUrl, filename }) { const lightbox = $('#lightbox'); const image = $('#lightbox-image'); if (image) { image.src = imageUrl; image.alt = filename || ''; } if (lightbox) { lightbox.classList.remove('hidden'); } // Close handlers const closeHandler = () => { lightbox?.classList.add('hidden'); document.removeEventListener('keydown', escHandler); }; const escHandler = (e) => { if (e.key === 'Escape') closeHandler(); }; $('#lightbox-close')?.addEventListener('click', closeHandler, { once: true }); lightbox?.addEventListener('click', (e) => { if (e.target === lightbox) closeHandler(); }, { once: true }); document.addEventListener('keydown', escHandler); } // ===================== // FORM HANDLERS // ===================== async handleColumnFormSubmit(e) { e.preventDefault(); const modal = $('#column-modal'); const mode = modal?.dataset.mode; const nameInput = $('#column-name'); const colorInput = $('#column-color'); const filterCategorySelect = $('#column-filter-category'); const customFilterInput = $('#column-custom-filter'); const name = nameInput?.value.trim(); const color = colorInput?.value || '#00D4FF'; // Filter-Kategorie ermitteln let filterCategory = filterCategorySelect?.value || 'in_progress'; if (filterCategory === 'custom') { filterCategory = customFilterInput?.value.trim() || 'in_progress'; } if (!name) { this.showError('Bitte einen Namen eingeben'); return; } try { if (mode === 'create') { await boardManager.createColumn(name, color, filterCategory); this.showSuccess('Statuskarte erstellt'); } else { const data = JSON.parse(modal?.dataset.data || '{}'); await boardManager.updateColumn(data.id, { name, color, filterCategory }); this.showSuccess('Statuskarte aktualisiert'); } this.handleModalClose({ modalId: 'column-modal' }); nameInput.value = ''; if (customFilterInput) customFilterInput.value = ''; } catch (error) { this.showError('Fehler beim Speichern'); } } async handleProjectFormSubmit(e) { e.preventDefault(); const modal = $('#project-modal'); const mode = modal?.dataset.mode; const nameInput = $('#project-name'); const descInput = $('#project-description'); const name = nameInput?.value.trim(); const description = descInput?.value.trim(); if (!name) { this.showError('Bitte einen Namen eingeben'); return; } try { if (mode === 'create') { const project = await api.createProject({ name, description }); store.addProject(project); await this.loadProject(project.id); this.populateProjectSelector(); this.showSuccess('Projekt erstellt'); } else { const data = JSON.parse(modal?.dataset.data || '{}'); await api.updateProject(data.id, { name, description }); store.updateProject(data.id, { name, description }); this.populateProjectSelector(); this.showSuccess('Projekt aktualisiert'); } this.handleModalClose({ modalId: 'project-modal' }); nameInput.value = ''; descInput.value = ''; } catch (error) { this.showError('Fehler beim Speichern'); } } async handleLabelFormSubmit(e) { e.preventDefault(); const modal = $('#label-modal'); const mode = modal?.dataset.mode; const nameInput = $('#label-name'); const colorInput = $('#label-color'); const name = nameInput?.value.trim(); const color = colorInput?.value; if (!name) { this.showError('Bitte einen Namen eingeben'); return; } const projectId = store.get('currentProjectId'); try { if (mode === 'create') { const label = await api.createLabel(projectId, { name, color }); store.addLabel(label); this.showSuccess('Label erstellt'); } else { const data = JSON.parse(modal?.dataset.data || '{}'); await api.updateLabel(projectId, data.id, { name, color }); store.updateLabel(data.id, { name, color }); this.showSuccess('Label aktualisiert'); } this.handleModalClose({ modalId: 'label-modal' }); this.populateLabelFilter(); nameInput.value = ''; } catch (error) { this.showError('Fehler beim Speichern'); } } openProjectModal(mode, data = {}) { this.handleModalOpen({ modalId: 'project-modal', mode, data }); const deleteBtn = $('#btn-delete-project'); const modalTitle = $('#project-modal-title'); if (mode === 'edit' && data) { $('#project-name').value = data.name || ''; $('#project-description').value = data.description || ''; $('#project-id').value = data.id || ''; if (deleteBtn) deleteBtn.classList.remove('hidden'); if (modalTitle) modalTitle.textContent = 'Projekt bearbeiten'; } else { $('#project-name').value = ''; $('#project-description').value = ''; $('#project-id').value = ''; if (deleteBtn) deleteBtn.classList.add('hidden'); if (modalTitle) modalTitle.textContent = 'Neues Projekt'; } } async handleDeleteProject() { const projectId = parseInt($('#project-id')?.value); if (!projectId) return; const project = store.get('projects').find(p => p.id === projectId); if (!project) return; this.showConfirmDialog({ message: `Möchten Sie das Projekt "${project.name}" und alle zugehörigen Aufgaben wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`, confirmText: 'Löschen', confirmClass: 'btn-danger', onConfirm: async () => { try { const result = await api.deleteProject(projectId, true); store.removeProject(projectId); // Falls das aktuelle Projekt gelöscht wurde, zu einem anderen wechseln if (store.get('currentProjectId') === projectId) { const projects = store.get('projects'); if (projects.length > 0) { await this.loadProject(projects[0].id); } else { store.setTasks([]); store.setColumns([]); store.setLabels([]); } } this.populateProjectSelector(); this.handleModalClose({ modalId: 'project-modal' }); this.showSuccess(`Projekt "${project.name}" wurde gelöscht`); } catch (error) { this.showError('Fehler beim Löschen des Projekts: ' + (error.message || 'Unbekannter Fehler')); } } }); } // ===================== // EXPORT/IMPORT // ===================== async exportProject() { const projectId = store.get('currentProjectId'); if (!projectId) return; try { const data = await api.exportProject(projectId, 'json'); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `project_${projectId}_export.json`; a.click(); URL.revokeObjectURL(url); this.showSuccess('Export erfolgreich'); } catch (error) { this.showError('Export fehlgeschlagen'); } } async importProject() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); await api.importProject(data); await this.loadInitialData(); this.showSuccess('Import erfolgreich'); } catch (error) { this.showError('Import fehlgeschlagen: ' + error.message); } }; input.click(); } // ===================== // ONLINE/OFFLINE // ===================== handleOnline() { store.setOnline(true); this.showToast({ message: 'Verbindung wiederhergestellt', type: 'success' }); // Sync pending operations offlineManager.syncPendingOperations(); } handleOffline() { store.setOnline(false); this.showToast({ message: 'Offline-Modus aktiv', type: 'warning' }); } // ===================== // REFRESH // ===================== async refresh() { const projectId = store.get('currentProjectId'); if (projectId) { await this.loadProject(projectId); } } // ===================== // SERVICE WORKER // ===================== async registerServiceWorker() { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register('/sw.js'); console.log('[App] Service Worker registered:', registration.scope); } catch (error) { console.error('[App] Service Worker registration failed:', error); } } } } // Create app instance and initialize const app = new App(); // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => app.init()); } else { app.init(); } export default app;