/** * Main Application Controller * Handles authentication, data loading, and view rendering */ class MailingListApp { constructor() { this.isAuthenticated = false; this.currentUser = null; this.lists = []; this.members = []; this.users = []; this.subscriptions = new Map(); // list_id -> members[] this.initializeApp(); } /** * Initialize the application */ async initializeApp() { this.setupEventListeners(); // Check for saved token const savedToken = localStorage.getItem('authToken'); if (savedToken) { apiClient.setToken(savedToken); await this.checkCurrentUser(); } } /** * Setup event listeners */ setupEventListeners() { // Login form submission document.getElementById('loginForm').addEventListener('submit', (e) => { e.preventDefault(); this.handleLogin(); }); // Clear error when user starts typing document.getElementById('username').addEventListener('input', () => { this.clearLoginError(); }); document.getElementById('password').addEventListener('input', () => { this.clearLoginError(); }); document.getElementById('logoutBtn').addEventListener('click', () => { this.logout(); }); // User dropdown functionality const userDropdownTrigger = document.getElementById('userDropdownTrigger'); const userDropdown = document.getElementById('userDropdown'); userDropdownTrigger.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); userDropdown.classList.toggle('active'); userDropdownTrigger.classList.toggle('active'); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!userDropdown.contains(e.target)) { userDropdown.classList.remove('active'); userDropdownTrigger.classList.remove('active'); } }); // Close dropdown on escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { userDropdown.classList.remove('active'); userDropdownTrigger.classList.remove('active'); } }); // Bulk import button document.getElementById('showBulkImportBtn').addEventListener('click', () => { uiManager.showBulkImportModal(); }); // Add user button (admin only) document.getElementById('addUserBtn').addEventListener('click', () => { uiManager.showUserModal(); }); // User management dropdown item document.getElementById('userManagementBtn').addEventListener('click', () => { // Close the dropdown userDropdown.classList.remove('active'); userDropdownTrigger.classList.remove('active'); // Switch to users tab this.switchToUsersTab(); }); } /** * Check if current user is still valid */ async checkCurrentUser() { try { const user = await apiClient.getCurrentUser(); this.currentUser = user; this.isAuthenticated = true; this.showAuthenticatedUI(); await this.loadData(); } catch (error) { // Token is invalid, clear it this.logout(); } } /** * Handle login button click */ async handleLogin() { const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); const username = usernameInput.value.trim(); const password = passwordInput.value.trim(); // Clear previous error states this.clearLoginError(); if (!username || !password) { this.showLoginError('Please enter both username and password'); return; } await this.login(username, password); } /** * Show login error message */ showLoginError(message) { const errorDiv = document.getElementById('loginError'); const errorMessage = document.getElementById('loginErrorMessage'); const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); errorMessage.textContent = message; errorDiv.style.display = 'flex'; // Add error class to inputs usernameInput.classList.add('error'); passwordInput.classList.add('error'); // Remove animation class and re-add to trigger animation errorDiv.style.animation = 'none'; setTimeout(() => { errorDiv.style.animation = ''; }, 10); } /** * Clear login error message */ clearLoginError() { const errorDiv = document.getElementById('loginError'); const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); errorDiv.style.display = 'none'; usernameInput.classList.remove('error'); passwordInput.classList.remove('error'); } /** * Authenticate with API */ async login(username, password) { try { uiManager.setLoading(true); // Login and get token const response = await apiClient.login(username, password); this.currentUser = response.user; this.isAuthenticated = true; // Save token localStorage.setItem('authToken', response.access_token); this.showAuthenticatedUI(); await this.loadData(); uiManager.showNotification(`Welcome back, ${this.currentUser.username}!`, 'success'); } catch (error) { this.isAuthenticated = false; this.currentUser = null; apiClient.clearToken(); localStorage.removeItem('authToken'); // Show error on login form let errorMessage = 'Login failed'; if (error.message) { if (error.message.includes('401') || error.message.includes('Unauthorized') || error.message.includes('Invalid') || error.message.includes('credentials')) { errorMessage = 'Invalid username or password'; } else if (error.message.includes('network') || error.message.includes('fetch')) { errorMessage = 'Unable to connect to server'; } else { errorMessage = error.message; } } this.showLoginError(errorMessage); } finally { uiManager.setLoading(false); } } /** * Logout */ async logout() { try { await apiClient.logout(); } catch (error) { // Ignore logout errors } this.isAuthenticated = false; this.currentUser = null; apiClient.clearToken(); localStorage.removeItem('authToken'); this.showUnauthenticatedUI(); uiManager.showNotification('Logged out successfully', 'info'); } /** * Show authenticated UI */ showAuthenticatedUI() { document.getElementById('loginPage').style.display = 'none'; document.getElementById('mainHeader').style.display = 'block'; document.getElementById('mainContent').style.display = 'block'; // Clear login inputs document.getElementById('username').value = ''; document.getElementById('password').value = ''; // Update user info display if (this.currentUser) { document.getElementById('currentUsername').textContent = this.currentUser.username; document.getElementById('currentUserRole').textContent = this.currentUser.role; document.getElementById('dropdownUsername').textContent = this.currentUser.username; document.getElementById('dropdownUserRole').textContent = this.currentUser.role; // Show/hide admin-only features const isAdmin = this.currentUser.role === 'administrator'; document.getElementById('userManagementBtn').style.display = isAdmin ? 'block' : 'none'; document.getElementById('userManagementDivider').style.display = isAdmin ? 'block' : 'none'; // Show/hide write access features const hasWriteAccess = this.currentUser.role === 'administrator' || this.currentUser.role === 'operator'; this.updateUIForPermissions(hasWriteAccess); } } /** * Show unauthenticated UI */ showUnauthenticatedUI() { document.getElementById('loginPage').style.display = 'flex'; document.getElementById('mainHeader').style.display = 'none'; document.getElementById('mainContent').style.display = 'none'; } /** * Update UI elements based on user permissions */ updateUIForPermissions(hasWriteAccess) { // Disable/enable write action buttons const writeButtons = document.querySelectorAll('[data-requires-write]'); writeButtons.forEach(button => { button.style.display = hasWriteAccess ? '' : 'none'; }); // Update button attributes for later reference document.getElementById('addListBtn').setAttribute('data-requires-write', ''); document.getElementById('addMemberBtn').setAttribute('data-requires-write', ''); document.getElementById('showBulkImportBtn').setAttribute('data-requires-write', ''); } /** * Switch to users tab (triggered from user dropdown) */ switchToUsersTab() { // Switch to users tab programmatically const tabButtons = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // Remove active class from all tabs and contents tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Show users tab content const usersTab = document.getElementById('users-tab'); if (usersTab) { usersTab.classList.add('active'); } // Load users data if needed this.loadUsers(); } /** * Load all data from API */ async loadData() { if (!this.isAuthenticated) return; try { uiManager.setLoading(true); // Load lists and members in parallel const [lists, members] = await Promise.all([ apiClient.getLists(), apiClient.getMembers() ]); this.lists = lists; this.members = members; // Load users if admin if (this.currentUser && this.currentUser.role === 'administrator') { try { this.users = await apiClient.getUsers(); } catch (error) { console.warn('Failed to load users:', error); this.users = []; } } // Load subscriptions for each list await this.loadSubscriptions(); // Render all views this.renderLists(); this.renderMembers(); if (this.currentUser && this.currentUser.role === 'administrator') { this.renderUsers(); } } catch (error) { uiManager.handleError(error, 'Failed to load data'); } finally { uiManager.setLoading(false); } } /** * Load subscription data for all lists */ async loadSubscriptions() { this.subscriptions.clear(); const subscriptionPromises = this.lists.map(async (list) => { try { const members = await apiClient.getListMembers(list.list_id); this.subscriptions.set(list.list_id, members); } catch (error) { console.warn(`Failed to load members for list ${list.list_id}:`, error); this.subscriptions.set(list.list_id, []); } }); await Promise.all(subscriptionPromises); } /** * Render mailing lists table */ renderLists() { const tbody = document.getElementById('listsTableBody'); tbody.innerHTML = ''; if (this.lists.length === 0) { tbody.innerHTML = `