1502 Zeilen
42 KiB
JavaScript
1502 Zeilen
42 KiB
JavaScript
/**
|
||
* 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 = '<option value="all">Alle Bearbeiter</option>';
|
||
|
||
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 = '<option value="all">Alle Labels</option>';
|
||
|
||
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 `
|
||
<div class="archive-item" data-task-id="${task.id}">
|
||
<div class="archive-item-info" onclick="app.openArchivedTask('${task.id}')">
|
||
<div class="archive-item-title">${this.escapeHtml(task.title)}</div>
|
||
<div class="archive-item-meta">
|
||
<span class="priority-badge ${task.priority || 'medium'}">${priorityLabels[task.priority] || 'Mittel'}</span>
|
||
<span>Spalte: ${this.escapeHtml(columnName)}</span>
|
||
${task.dueDate ? `<span>Fällig: ${new Date(task.dueDate).toLocaleDateString('de-DE')}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="archive-item-actions">
|
||
<button class="btn btn-sm btn-primary" onclick="app.restoreTask('${task.id}')">Wiederherstellen</button>
|
||
<button class="btn btn-sm btn-text text-danger" onclick="app.deleteArchivedTask('${task.id}')">Löschen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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 = `
|
||
<span class="toast-icon">${icon}</span>
|
||
<span class="toast-message">${message}</span>
|
||
<button class="toast-close">×</button>
|
||
`;
|
||
|
||
// 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;
|