UNPKG

besper-frontend-site-dev-main

Version:

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

782 lines (705 loc) 25.8 kB
/** * Manage-workspace Page Script * Handles page initialization and functionality */ class ManageWorkspacePage { constructor(data = {}) { this.data = data; this.pageId = 'manage-workspace'; this.initialized = false; this.authService = this.getAuthService(); this.operatorsService = null; // Will be set later when available this.workspaces = []; this.currentWorkspace = null; this.currentMode = null; } /** * 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; } // Fallback implementation return { isUserAuthenticated: () => { return ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' && !!window.auth.getToken() ); }, getToken: () => { if ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' ) { return window.auth.getToken(); } return null; }, getUserPermission: key => { 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; }, }; } /** * Initialize the page - IMMEDIATE RENDERING with skeleton loading */ async initialize(_data = {}) { if (this.initialized) return; try { // Set global instance for onclick handlers if (typeof window !== 'undefined') { window.manageworkspacePage = this; } // IMMEDIATE: Show the UI structure without waiting for authentication or services this.renderImmediateUI(); // IMMEDIATE: Setup basic interactions (buttons disabled until loaded) this.setupBasicInteractions(); this.initialized = true; // DEFERRED: Initialize data loading in background - completely non-blocking this.initializeDataLoadingInBackground(); } catch (error) { console.error('Error initializing manage workspace page:', error); this.showError(error); } } /** * Render immediate UI structure - DO NOT replace existing skeleton loading * Only make minimal changes to prepare for interactions */ renderImmediateUI() { // Update page title and subtitle immediately (non-blocking) this.setupPageHeader(); // Ensure initial skeleton content is visible and accessible const loadingContent = document.querySelector('.initial-loading'); if (loadingContent) { loadingContent.style.opacity = '1'; loadingContent.style.pointerEvents = 'auto'; } // Remove any existing loading overlays that might block interaction const loadingOverlays = document.querySelectorAll( '.loading-overlay, .bsp-loading-indicator' ); loadingOverlays.forEach(overlay => overlay.remove()); } /** * Setup basic interactions immediately (buttons remain disabled until data loads) */ setupBasicInteractions() { // Add placeholder event listeners to existing skeleton buttons // They will be replaced when real content loads // Setup future interactions that will be enabled after loading this.setupDeferredInteractions(); } /** * Initialize data loading completely in background - NO UI BLOCKING * Uses requestIdleCallback for true non-blocking execution like contact-us page */ async initializeDataLoadingInBackground() { const loadData = async () => { try { this.loading = true; // Set operators service if available (deferred to background) this.operatorsService = window.pageOperatorsService || null; // Check authentication in background if (!this.authService || !this.authService.isUserAuthenticated()) { // Replace skeleton with auth view this.showUnauthenticatedView(); return; } // Load workspace data in background await this.loadWorkspaces(); // Smoothly transition from skeleton to real content this.transitionToRealContent(); // Enable all interactions after content is ready this.enableAllInteractions(); this.loading = false; } catch (error) { console.error('Background data loading failed:', error); this.showError('Failed to load workspace data'); this.loading = false; } }; // Use requestIdleCallback for true non-blocking background execution if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(loadData, { timeout: 3000 }); } else { // Fallback for browsers without requestIdleCallback setTimeout(loadData, 100); } } /** * Setup page header with title and subtitle */ setupPageHeader() { const titleElement = document.getElementById('page-title'); const subtitleElement = document.getElementById('page-subtitle'); if (titleElement) { titleElement.textContent = this.getPageTitle(); } if (subtitleElement) { subtitleElement.textContent = this.getPageSubtitle(); } } /** * Get skeleton content for immediate loading */ getSkeletonContent() { return ` <!-- Header Actions --> <div class="workspace-header"> <div class="header-actions"> <button class="btn btn-secondary skeleton-btn" data-action="export" disabled> <div class="skeleton-text skeleton-text-sm"></div> </button> <button class="btn btn-primary skeleton-btn" data-action="create" disabled> <div class="skeleton-text skeleton-text-sm"></div> </button> </div> </div> <!-- Stats Cards Skeleton --> <div class="stats-grid"> <div class="stat-card skeleton-card"> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-2xl"></div> <div class="skeleton-text skeleton-text-xs"></div> </div> <div class="stat-card skeleton-card"> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-2xl"></div> <div class="skeleton-text skeleton-text-xs"></div> </div> <div class="stat-card skeleton-card"> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-2xl"></div> <div class="skeleton-text skeleton-text-xs"></div> </div> <div class="stat-card skeleton-card"> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-2xl"></div> <div class="skeleton-text skeleton-text-xs"></div> </div> </div> <!-- Table Skeleton --> <div class="table-container"> <div class="table-header"> <div class="skeleton-text skeleton-text-lg"></div> <div class="skeleton-text skeleton-text-base" style="width: 250px;"></div> </div> <div class="skeleton-table"> <!-- Table header skeleton --> <div class="skeleton-table-header"> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-xs"></div> <div class="skeleton-text skeleton-text-xs"></div> </div> <!-- Table rows skeleton --> ${Array(5) .fill(0) .map( () => ` <div class="skeleton-table-row"> <div class="skeleton-text skeleton-text-sm"></div> <div class="skeleton-text skeleton-text-sm"></div> <div class="skeleton-text skeleton-text-sm"></div> <div class="skeleton-text skeleton-text-sm"></div> <div class="skeleton-text skeleton-text-sm"></div> <div class="skeleton-actions"> <div class="skeleton-btn-sm"></div> <div class="skeleton-btn-sm"></div> </div> </div> ` ) .join('')} </div> </div> `; } /** * Setup deferred interactions that will be enabled after loading */ setupDeferredInteractions() { // Store interactions to be setup later this.deferredInteractions = { createWorkspace: () => this.openSidebar('create'), exportData: () => this.exportData(), searchWorkspaces: () => this.filterWorkspaces(), }; } /** * Smoothly transition from skeleton to real content */ transitionToRealContent() { const contentArea = document.getElementById('page-content-area'); if (!contentArea) return; // Add fade transition contentArea.style.transition = 'opacity 0.3s ease-in-out'; contentArea.style.opacity = '0.7'; // Replace content after brief fade setTimeout(() => { contentArea.innerHTML = this.getWorkspaceManagementContent(); this.renderWorkspaces(); this.updateStats(); // Fade back in contentArea.style.opacity = '1'; // Remove transition after animation setTimeout(() => { contentArea.style.transition = ''; }, 300); }, 150); } /** * Enable all interactions after content is ready */ enableAllInteractions() { // Setup Create button const createBtn = document.querySelector('[data-action="create"]'); if (createBtn) { createBtn.addEventListener( 'click', this.deferredInteractions.createWorkspace ); } // Setup Export button const exportBtn = document.querySelector('[data-action="export"]'); if (exportBtn) { exportBtn.addEventListener('click', this.deferredInteractions.exportData); } // Setup all other interactions this.setupInteractions(); } async loadWorkspaces() { if (!this.operatorsService) { throw new Error('Operators service not available'); } try { const response = await this.operatorsService.getWorkspaces(); this.workspaces = response.workspaces || []; } catch (error) { console.error('Failed to load workspaces:', error); throw error; // Re-throw error instead of falling back to sample data } } /** * Get workspace management content */ getWorkspaceManagementContent() { return ` <!-- Header Actions --> <div class="workspace-header"> <div class="header-actions"> <button class="btn btn-secondary" data-action="export"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/> </svg> Export </button> <button class="btn btn-primary" data-action="create"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"/> </svg> Create Workspace </button> </div> </div> <!-- Stats Cards --> <div class="stats-grid"> <div class="stat-card"> <div class="stat-label">Total Workspaces</div> <div class="stat-value" id="totalWorkspaces">0</div> <div class="stat-subtext">Active workspaces</div> </div> <div class="stat-card"> <div class="stat-label">Total Users</div> <div class="stat-value" id="totalUsers">0</div> <div class="stat-subtext">Across all workspaces</div> </div> <div class="stat-card"> <div class="stat-label">Total Licenses</div> <div class="stat-value" id="totalLicenses">0</div> <div class="stat-subtext">License allocation</div> </div> <div class="stat-card"> <div class="stat-label">Cost Pools</div> <div class="stat-value" id="totalCostPools">0</div> <div class="stat-subtext">Unique cost centers</div> </div> </div> <!-- Workspace Table --> <div class="table-container"> <div class="table-header"> <div class="table-title">All Workspaces</div> <input type="text" class="search-box" placeholder="Search workspaces..." id="searchInput"> </div> <table> <thead> <tr> <th>Workspace Name</th> <th>Parent Workspace</th> <th class="center">Users</th> <th class="center">Licenses</th> <th>Cost Pools</th> <th class="center">Actions</th> </tr> </thead> <tbody id="workspaceTableBody"> <!-- Table rows will be populated here --> </tbody> </table> <div class="empty-state" id="emptyState" style="display: none;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> <line x1="9" y1="9" x2="15" y2="15"/> <line x1="9" y1="15" x2="15" y2="9"/> </svg> <h3>No workspaces found</h3> <p>Create your first workspace to get started</p> <button class="btn btn-primary" data-action="create">Create Workspace</button> </div> </div> <!-- Sidebar --> <div class="overlay" id="overlay"></div> <div class="sidebar" id="sidebar"> <div class="sidebar-header"> <div class="sidebar-title" id="sidebarTitle">Create Workspace</div> <button class="close-btn" onclick="closeSidebar()"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path d="M18 6L6 18M6 6l12 12"/> </svg> </button> </div> <div class="sidebar-body" id="sidebarContent"> <!-- Content will be dynamically inserted here --> </div> <div class="sidebar-footer" id="sidebarFooter"> <!-- Footer buttons will be dynamically inserted here --> </div> </div> `; } /** * Setup page interactions */ setupInteractions() { // Search functionality const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('keyup', () => this.filterWorkspaces()); } // Close sidebar on overlay click const overlay = document.getElementById('overlay'); if (overlay) { overlay.addEventListener('click', () => this.closeSidebar()); } } /** * Render workspaces table */ renderWorkspaces() { const tbody = document.getElementById('workspaceTableBody'); const emptyState = document.getElementById('emptyState'); if (!tbody) return; if (this.workspaces.length === 0) { tbody.style.display = 'none'; if (emptyState) emptyState.style.display = 'block'; return; } tbody.style.display = ''; if (emptyState) emptyState.style.display = 'none'; tbody.innerHTML = this.workspaces .map((workspace, _index) => { const indent = workspace.level > 0 ? `<span class="workspace-hierarchy" style="margin-left: ${workspace.level * 20}px;"></span>` : ''; return ` <tr id="workspace-${workspace.id}"> <td> <div class="workspace-name"> ${indent} ${workspace.name} </div> </td> <td> <span class="parent-workspace">${workspace.parent || '—'}</span> </td> <td class="center">${workspace.users ? workspace.users.toLocaleString() : '0'}</td> <td class="center">${workspace.licenses ? workspace.licenses.toLocaleString() : '0'}</td> <td> <div class="cost-pools"> ${workspace.costPools ? workspace.costPools.map(pool => `<span class="cost-pool-badge">${pool}</span>`).join('') : ''} </div> </td> <td class="center"> <div class="actions-cell"> <button class="action-btn view" onclick="window.manageworkspacePage?.openSidebar?.('view', ${_index})" title="View"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> </button> <button class="action-btn edit" onclick="window.manageworkspacePage?.openSidebar?.('edit', ${_index})" title="Edit"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/> <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/> </svg> </button> <button class="action-btn admin" onclick="window.manageworkspacePage?.openAdminManagement?.(${_index})" title="Manage Administrators"> 👥 </button> </div> </td> </tr> `; }) .join(''); } /** * Update statistics */ updateStats() { const totalWorkspacesEl = document.getElementById('totalWorkspaces'); const totalUsersEl = document.getElementById('totalUsers'); const totalLicensesEl = document.getElementById('totalLicenses'); const totalCostPoolsEl = document.getElementById('totalCostPools'); if (totalWorkspacesEl) { totalWorkspacesEl.textContent = this.workspaces.length; } if (totalUsersEl) { const totalUsers = this.workspaces.reduce( (sum, w) => sum + (w.users || 0), 0 ); totalUsersEl.textContent = totalUsers.toLocaleString(); } if (totalLicensesEl) { const totalLicenses = this.workspaces.reduce( (sum, w) => sum + (w.licenses || 0), 0 ); totalLicensesEl.textContent = totalLicenses.toLocaleString(); } if (totalCostPoolsEl) { const uniqueCostPools = new Set(); this.workspaces.forEach(w => { if (w.costPools) { w.costPools.forEach(pool => uniqueCostPools.add(pool)); } }); totalCostPoolsEl.textContent = uniqueCostPools.size; } } /** * Filter workspaces based on search */ filterWorkspaces() { const searchInput = document.getElementById('searchInput'); if (!searchInput) return; const searchTerm = searchInput.value.toLowerCase(); const filtered = this.workspaces.filter( workspace => workspace.name.toLowerCase().includes(searchTerm) || (workspace.parent && workspace.parent.toLowerCase().includes(searchTerm)) || (workspace.costPools && workspace.costPools.some(pool => pool.toLowerCase().includes(searchTerm) )) ); // Re-render with filtered data this.renderFilteredWorkspaces(filtered); } /** * Render filtered workspaces */ renderFilteredWorkspaces(filteredWorkspaces) { const tbody = document.getElementById('workspaceTableBody'); const emptyState = document.getElementById('emptyState'); if (!tbody) return; if (filteredWorkspaces.length === 0) { tbody.style.display = 'none'; if (emptyState) emptyState.style.display = 'block'; return; } tbody.style.display = ''; if (emptyState) emptyState.style.display = 'none'; tbody.innerHTML = filteredWorkspaces .map((workspace, _index) => { const originalIndex = this.workspaces.indexOf(workspace); const indent = workspace.level > 0 ? `<span class="workspace-hierarchy" style="margin-left: ${workspace.level * 20}px;"></span>` : ''; return ` <tr id="workspace-${workspace.id}"> <td> <div class="workspace-name"> ${indent} ${workspace.name} </div> </td> <td> <span class="parent-workspace">${workspace.parent || '—'}</span> </td> <td class="center">${workspace.users ? workspace.users.toLocaleString() : '0'}</td> <td class="center">${workspace.licenses ? workspace.licenses.toLocaleString() : '0'}</td> <td> <div class="cost-pools"> ${workspace.costPools ? workspace.costPools.map(pool => `<span class="cost-pool-badge">${pool}</span>`).join('') : ''} </div> </td> <td class="center"> <div class="actions-cell"> <button class="action-btn view" onclick="window.manageworkspacePage?.openSidebar?.('view', ${originalIndex})" title="View"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> <path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> </svg> </button> <button class="action-btn edit" onclick="window.manageworkspacePage?.openSidebar?.('edit', ${originalIndex})" title="Edit"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002-2v-7"/> <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/> </svg> </button> <button class="action-btn admin" onclick="window.manageworkspacePage?.openAdminManagement?.(${originalIndex})" title="Manage Administrators"> 👥 </button> </div> </td> </tr> `; }) .join(''); } /** * Open sidebar for create/edit/view */ openSidebar(mode, workspaceIndex = null) { console.log('Opening sidebar:', mode, workspaceIndex); // Placeholder - implement sidebar functionality } /** * Close sidebar */ closeSidebar() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('overlay'); if (sidebar) sidebar.classList.remove('open'); if (overlay) overlay.classList.remove('active'); } /** * Open admin management */ openAdminManagement(workspaceIndex) { console.log('Opening admin management for workspace:', workspaceIndex); // Placeholder - implement admin management } /** * Export data */ exportData() { const csvContent = 'data:text/csv;charset=utf-8,' + 'Workspace Name,Parent Workspace,Users,Licenses,Cost Pools\n' + this.workspaces .map( w => `"${w.name}","${w.parent || ''}",${w.users || 0},${w.licenses || 0},"${w.costPools ? w.costPools.join('; ') : ''}"` ) .join('\n'); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', 'workspaces_export.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); } /** * DEPRECATED: Get fallback data for testing * This method should never be called in production */ getFallbackData() { console.warn( '[ManageWorkspace] getFallbackData is deprecated and should not be used' ); throw new Error( 'Fallback workspace data is not allowed. Use APIM operators instead.' ); } /** * Show unauthenticated view */ showUnauthenticatedView() { const contentArea = document.getElementById('page-content-area'); if (contentArea) { contentArea.innerHTML = ` <div class="unauthenticated-view"> <h3>Authentication Required</h3> <p>Please log in to access workspace management.</p> <button class="btn btn-primary" onclick="window.location.href='/login'"> Sign In </button> </div> `; } } /** * Show error message */ showError(message) { const contentArea = document.getElementById('page-content-area'); if (contentArea) { contentArea.innerHTML = ` <div class="error-view"> <h3>Error</h3> <p>${message}</p> <button class="btn btn-primary" onclick="location.reload()"> Retry </button> </div> `; } } /** * Get page title */ getPageTitle() { return 'Workspace Management'; } /** * Get page subtitle */ getPageSubtitle() { return 'Manage organizational workspaces and their hierarchies'; } } // Export for use in other modules - FIXED PATTERN if (typeof module !== 'undefined' && module.exports) { module.exports = ManageWorkspacePage; } else if (typeof window !== 'undefined') { window.ManageWorkspacePage = ManageWorkspacePage; // Store instance globally for onclick handlers (deferred setup) window.manageworkspacePage = null; }