/** * 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(); }); // Member search functionality const memberSearchInput = document.getElementById('memberSearchInput'); const memberSearchClear = document.getElementById('memberSearchClear'); memberSearchInput.addEventListener('input', (e) => { this.filterMembers(e.target.value); // Show/hide clear button if (e.target.value.trim()) { memberSearchClear.style.display = 'flex'; } else { memberSearchClear.style.display = 'none'; } }); memberSearchClear.addEventListener('click', () => { memberSearchInput.value = ''; memberSearchClear.style.display = 'none'; this.filterMembers(''); memberSearchInput.focus(); }); // Clear search when switching tabs document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { if (btn.dataset.tab !== 'members') { memberSearchInput.value = ''; memberSearchClear.style.display = 'none'; } }); }); } /** * 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 = `
Try adjusting your search terms or check the spelling.
`; return; } filteredMembers.forEach(member => { const row = tbody.insertRow(); // Name const nameCell = row.insertCell(); nameCell.textContent = member.name; // Email const emailCell = row.insertCell(); const emailLink = document.createElement('a'); emailLink.href = `mailto:${member.email}`; emailLink.textContent = member.email; emailLink.style.color = 'var(--primary-color)'; emailCell.appendChild(emailLink); // Lists (show member's subscriptions) const listsCell = row.insertCell(); const memberLists = []; this.subscriptions.forEach((members, listId) => { if (members.some(m => m.member_id === member.member_id)) { const list = this.lists.find(l => l.list_id === listId); if (list) { memberLists.push(list.list_name); } } }); listsCell.textContent = memberLists.length > 0 ? memberLists.join(', ') : 'None'; // Status const statusCell = row.insertCell(); const statusBadge = document.createElement('span'); statusBadge.className = `status-badge ${member.active ? 'active' : 'inactive'}`; statusBadge.innerHTML = ` ${member.active ? 'Active' : 'Inactive'}`; // Add bounce status if exists if (member.bounce_status && member.bounce_status !== 'clean') { const bounceIndicator = document.createElement('span'); bounceIndicator.className = `bounce-badge bounce-${member.bounce_status === 'hard_bounce' ? 'hard' : 'soft'}`; bounceIndicator.innerHTML = ` Bounces`; bounceIndicator.title = `${member.bounce_count || 0} bounces - ${member.bounce_status}`; statusCell.appendChild(document.createElement('br')); statusCell.appendChild(bounceIndicator); } statusCell.appendChild(statusBadge); // Actions const actionsCell = row.insertCell(); actionsCell.className = 'action-buttons'; // Bounces button (if member has bounce data) if (member.bounce_count > 0) { const bouncesBtn = uiManager.createActionButton('Bounces', 'exclamation-triangle', 'btn-warning', () => { uiManager.showBounceHistory(member); }); actionsCell.appendChild(bouncesBtn); } const subscriptionsBtn = uiManager.createActionButton('Lists', 'list', 'btn-secondary', () => { uiManager.showMemberSubscriptions(member); }); actionsCell.appendChild(subscriptionsBtn); const editBtn = uiManager.createActionButton('Edit', 'edit', 'btn-primary', () => { uiManager.showMemberModal(member); }); editBtn.setAttribute('data-requires-write', ''); actionsCell.appendChild(editBtn); const deleteBtn = uiManager.createActionButton('Delete', 'trash', 'btn-danger', () => { uiManager.showConfirmation( `Are you sure you want to delete member "${member.name}"? This will also remove them from all mailing lists.`, async () => { await this.deleteMember(member.member_id); } ); }); deleteBtn.setAttribute('data-requires-write', ''); actionsCell.appendChild(deleteBtn); }); // Update UI permissions for the filtered results const hasWriteAccess = this.currentUser && (this.currentUser.role === 'administrator' || this.currentUser.role === 'operator'); this.updateUIForPermissions(hasWriteAccess); } } // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.app = new MailingListApp(); });