UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

565 lines (501 loc) 17.3 kB
/** * User Management New Page Script - Bulletproof loading pattern * Implements modern user management UI with APIM operators */ class UserManagementNewPage { constructor(options = {}) { this.options = { containerId: 'besper-site-content', environment: 'prod', ...options, }; this.initialized = false; this.users = []; this.loading = false; this.workspaceId = null; // BULLETPROOF: Defer all complex operations to avoid blocking class export this.authService = null; this.operatorsService = null; this.isAuthenticated = false; // Initialize auth safely in next tick to ensure class export completes first if (typeof window !== 'undefined') { setTimeout(() => { this.initializeAuth(); }, 1); } } /** * Initialize authentication safely without blocking class export */ initializeAuth() { try { this.authService = this.getAuthService(); this.operatorsService = this.getOperatorsService(); } catch (error) { console.warn('Auth initialization failed, using fallback:', error); this.authService = this.createFallbackAuthService(); this.operatorsService = this.createFallbackOperatorsService(); } } /** * Get authentication service from global scope or create simple fallback */ getAuthService() { // Try to access global token auth service if available if (typeof window !== 'undefined' && window.tokenAuthService) { return window.tokenAuthService; } return this.createFallbackAuthService(); } /** * Get operators service from global scope or create simple fallback */ getOperatorsService() { // Try to access global page operators service if available if (typeof window !== 'undefined' && window.pageOperatorsService) { return window.pageOperatorsService; } return this.createFallbackOperatorsService(); } /** * Create safe fallback auth service */ createFallbackAuthService() { return { isUserAuthenticated: () => { try { return ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' && !!window.auth.getToken() ); } catch (error) { return false; } }, getToken: () => { try { if ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' ) { return window.auth.getToken(); } } catch (error) { console.warn('Token access failed:', error); } return null; }, getUserPermission: key => { try { if (typeof window === 'undefined') return null; const mappings = { contactId: window.contact_id || window.user_contactid, userName: window.user_name, name: window.user_name, userEmail: window.user_email, email: window.user_email, workspaceId: window.workspace_id, accountId: window.account_id, subscriptionId: window.subscription_id, }; return mappings[key] || null; } catch (error) { console.warn('User permission access failed:', error); return null; } }, }; } /** * Create safe fallback operators service */ createFallbackOperatorsService() { return { getUsers: async () => { console.warn( 'Operators service not available, returning empty user list' ); return []; }, inviteUser: async () => { console.warn('Operators service not available, invite user failed'); return { success: false, message: 'Service not available' }; }, removeUser: async () => { console.warn('Operators service not available, remove user failed'); return { success: false, message: 'Service not available' }; }, }; } /** * Initialize the page - IMMEDIATE RENDERING * Shows UI instantly without waiting for authentication */ async initialize(_data = {}) { if (this.initialized) return; try { // IMMEDIATE: Show the UI without any loading delays this.renderImmediateUI(); // IMMEDIATE: Setup basic interactions this.setupInteractions(); this.initialized = true; // DEFERRED: Initialize authentication features in background this.initializeAuthenticationFeaturesInBackground(); } catch (error) { console.error('Error initializing user management page:', error); this.showError(error); } } /** * Render immediate UI structure - shows content instantly */ renderImmediateUI() { const container = document.getElementById(this.options.containerId); if (!container) return; // Clear any existing loading indicators container.innerHTML = ''; // Show immediate user management interface container.innerHTML = ` <div style="background: #f8f9fa; color: #022d54; font-family: Arial, sans-serif;"> <div style="max-width: 1200px; margin: 0 auto; padding: 2rem 1rem;"> <!-- Header --> <div style="margin-bottom: 2rem;"> <h1 style="color: #022d54; font-weight: 300; font-size: 28px; margin-bottom: 0.5rem;">User Management</h1> <p style="color: #6c757d; font-size: 14px; margin: 0;">Manage user access and permissions</p> </div> <!-- Action Buttons --> <div style="display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 2rem;"> <button id="inviteUserBtn" style="padding: 10px 20px; border: none; border-radius: 6px; background: #022d54; color: white; cursor: pointer; font-size: 14px;"> ➕ Invite User </button> </div> <!-- Users List with Skeleton Loading --> <div style="background: white; border-radius: 8px; border: 1px solid #e0e0e0; overflow: hidden;"> <div style="padding: 1rem; border-bottom: 1px solid #e0e0e0;"> <h3 style="margin: 0; color: #022d54; font-size: 16px; font-weight: 500;">Users</h3> </div> <div id="page-content-area" style="padding: 2rem;"> <!-- Skeleton Loading Animation --> <div style="height: 20px; background: #e0e0e0; border-radius: 4px; margin-bottom: 1rem; animation: pulse 1.5s ease-in-out infinite;"></div> <div style="height: 20px; background: #e0e0e0; border-radius: 4px; margin-bottom: 1rem; width: 80%; animation: pulse 1.5s ease-in-out infinite;"></div> <div style="height: 20px; background: #e0e0e0; border-radius: 4px; margin-bottom: 1rem; width: 60%; animation: pulse 1.5s ease-in-out infinite;"></div> <div style="height: 100px; background: #e0e0e0; border-radius: 4px; animation: pulse 1.5s ease-in-out infinite;"></div> </div> </div> </div> </div> <!-- Skeleton Loading Animation --> <style> @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } </style> `; } /** * Setup interactions - immediate setup without waiting */ setupInteractions() { // Setup invite user button const inviteBtn = document.getElementById('inviteUserBtn'); if (inviteBtn) { inviteBtn.addEventListener('click', () => { this.showInviteUserModal(); }); } // Setup row click handlers (will be rebound when real content loads) document.addEventListener('click', e => { if (e.target.closest('.user-row')) { this.selectUser(e.target.closest('.user-row').dataset.userId); } }); } /** * Initialize authentication features completely in background - NO UI BLOCKING */ async initializeAuthenticationFeaturesInBackground() { // Use requestIdleCallback for true background processing const initializeAuth = () => { try { // Ensure services are ready if (!this.authService) { this.initializeAuth(); } // Check authentication status without blocking if (this.authService) { this.isAuthenticated = this.authService.isUserAuthenticated(); if (this.isAuthenticated) { this.workspaceId = this.authService.getUserPermission('workspaceId'); // Load data in background this.loadDataInBackground(); } else { this.showUnauthenticatedView(); } } } catch (error) { console.warn( 'Background authentication features initialization failed:', error ); // Continue silently - authentication is not critical for UI display } }; // Use requestIdleCallback for non-blocking background execution if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(initializeAuth, { timeout: 5000 }); } else { // Fallback for browsers without requestIdleCallback setTimeout(initializeAuth, 50); } } /** * Load data in background */ async loadDataInBackground() { try { console.log('Loading users in background...'); await this.loadUsers(); } catch (error) { console.error('Background data loading failed:', error); } } async loadUsers() { try { this.setLoadingState(true); if (!this.operatorsService) { console.warn('Operators service not available'); return; } const response = await this.operatorsService.callOperator( 'user_management', 'list_users', { authData: this.authService.getAuthData ? this.authService.getAuthData() : { token: this.authService.getToken() }, workspaceId: this.workspaceId, } ); if (response.success) { this.users = response.data.users || []; this.renderUsersTable(); } } catch (error) { console.error('Error loading users:', error); this.showError('Failed to load users'); } finally { this.setLoadingState(false); } } async inviteUser(userData) { try { if (!this.operatorsService) { console.warn('Operators service not available'); return; } const response = await this.operatorsService.callOperator( 'user_management', 'invite_user', { authData: this.authService.getAuthData ? this.authService.getAuthData() : { token: this.authService.getToken() }, workspaceId: this.workspaceId, ...userData, } ); if (response.success) { await this.loadUsers(); this.hideInviteUserModal(); this.showSuccess('User invited successfully'); } } catch (error) { console.error('Error inviting user:', error); this.showError('Failed to invite user'); } } renderUsersTable() { const tableBody = document.getElementById('usersTableBody'); if (!tableBody) return; tableBody.innerHTML = this.users .map( user => ` <tr class="user-row" data-user-id="${user.id}"> <td> <div class="user-info"> <div class="user-name">${user.name}</div> <div class="user-email">${user.email}</div> </div> </td> <td> <span class="role-badge ${user.role}">${user.role}</span> </td> <td> <span class="status-badge ${user.status}">${user.status}</span> </td> <td>${user.lastActive || 'Never'}</td> </tr> ` ) .join(''); } showInviteUserModal() { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = ` <div class="modal"> <div class="modal-header"> <h2>Invite User</h2> <button class="close-modal">&times;</button> </div> <div class="modal-body"> <form id="inviteUserForm"> <div class="form-group"> <label>Email *</label> <input type="email" name="email" required> </div> <div class="form-group"> <label>Role</label> <select name="role"> <option value="member">Member</option> <option value="admin">Admin</option> </select> </div> </form> </div> <div class="modal-footer"> <button class="btn btn-outline close-modal">Cancel</button> <button class="btn btn-primary" onclick="this.submitInviteUser()">Send Invite</button> </div> </div> `; document.body.appendChild(modal); modal.querySelectorAll('.close-modal').forEach(btn => { btn.addEventListener('click', () => this.hideInviteUserModal()); }); } hideInviteUserModal() { document.querySelector('.modal-overlay')?.remove(); } async submitInviteUser() { const form = document.getElementById('inviteUserForm'); if (!form) return; const formData = new FormData(form); await this.inviteUser({ email: formData.get('email'), role: formData.get('role'), }); } setLoadingState(loading) { this.loading = loading; const loadingIndicator = document.getElementById('loadingIndicator'); if (loadingIndicator) { loadingIndicator.style.display = loading ? 'block' : 'none'; } } /** * Show error message */ showError(message) { console.error(message); const contentArea = document.getElementById('page-content-area'); if (contentArea) { contentArea.innerHTML = ` <div style="text-align: center; padding: 2rem; color: #d32f2f;"> <h3>Error</h3> <p>${message}</p> <button onclick="location.reload()" style="padding: 10px 20px; background: #022d54; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;"> Try Again </button> </div> `; } } showSuccess(message) { console.log(message); // TODO: Implement success notification } showUnauthenticatedView() { const contentArea = document.getElementById('page-content-area'); if (contentArea) { contentArea.innerHTML = ` <div style="text-align: center; padding: 2rem;"> <h2 style="color: #022d54;">Authentication Required</h2> <p style="color: #6c757d;">Please log in to access user management.</p> </div> `; } } selectUser(userId) { console.log('User selected:', userId); // TODO: Implement user selection logic } getPageContent() { return ` <div style="padding: 1rem;"> <div style="margin-bottom: 2rem;"> <h2 style="color: #022d54; margin-bottom: 0.5rem;">Team Members</h2> <div id="loadingIndicator" style="display: none; color: #6c757d;">Loading users...</div> </div> <div style="background: white; border-radius: 8px; border: 1px solid #e0e0e0; overflow: hidden;"> <table style="width: 100%; border-collapse: collapse;"> <thead> <tr style="background: #f8f9fa; border-bottom: 1px solid #e0e0e0;"> <th style="padding: 1rem; text-align: left; color: #022d54; font-weight: 500;">User</th> <th style="padding: 1rem; text-align: left; color: #022d54; font-weight: 500;">Role</th> <th style="padding: 1rem; text-align: left; color: #022d54; font-weight: 500;">Status</th> <th style="padding: 1rem; text-align: left; color: #022d54; font-weight: 500;">Last Active</th> </tr> </thead> <tbody id="usersTableBody"></tbody> </table> </div> </div> `; } getPageTitle() { return 'User Management'; } getPageSubtitle() { return 'Manage team members and permissions'; } destroy() { this.hideInviteUserModal(); } } // BULLETPROOF EXPORT MECHANISM - Ensures class is always available (function () { 'use strict'; // Multiple export strategies to ensure maximum compatibility // Strategy 1: Direct window assignment (most reliable) if (typeof window !== 'undefined') { window.UserManagementNewPage = UserManagementNewPage; } // Strategy 2: Module exports for Node.js environments if (typeof module !== 'undefined' && module.exports) { module.exports = UserManagementNewPage; } // Strategy 3: Global fallback if (typeof global !== 'undefined') { global.UserManagementNewPage = UserManagementNewPage; } // Strategy 4: AMD/RequireJS support if (typeof define === 'function' && define.amd) { define([], function () { return UserManagementNewPage; }); } // Strategy 5: Self-executing verification setTimeout(function () { if (typeof window !== 'undefined' && !window.UserManagementNewPage) { console.warn('UserManagementNewPage export failed, retrying...'); window.UserManagementNewPage = UserManagementNewPage; } }, 10); })();