UNPKG

besper-frontend-site-dev-main

Version:

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

688 lines (613 loc) 22.1 kB
/** * Manage-workspace Page Script - Optimized for fast rendering * Handles workspace management with immediate UI loading like contact-us */ class ManageWorkspacePage { constructor(options = {}) { this.options = { containerId: 'besper-site-content', environment: 'prod', ...options, }; this.initialized = false; this.workspaces = []; this.authService = this.getAuthService(); this.isAuthenticated = false; } /** * 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 using global window.auth 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 workspace management page - IMMEDIATE RENDERING with skeleton loading * Shows UI instantly without waiting for authentication or token generation like contact-us */ async initialize(_data = {}) { if (this.initialized) return; try { // Set global instance for onclick handlers if (typeof window !== 'undefined') { window.manageworkspacePage = this; } // IMMEDIATE: Show the form UI without any loading delays this.renderImmediateUI(); // IMMEDIATE: Setup form elements and interactions this.setupFormElements(); // IMMEDIATE: Enable basic interactions this.setupInteractions(); this.initialized = true; // DEFERRED: Initialize authentication and data loading in background this.initializeBackgroundTasks(); } catch (error) { console.error('Error initializing manage workspace page:', error); this.showError(error); } } /** * Render immediate UI structure - shows content instantly like contact-us */ renderImmediateUI() { const container = document.getElementById(this.options.containerId); if (!container) return; // Clear any existing loading indicators container.innerHTML = ''; // Add immediate workspace management content container.innerHTML = this.getWorkspaceManagementHTML(); // Update page header immediately this.setupPageHeader(); } /** * Setup form elements and interactions immediately */ setupFormElements() { // Setup search functionality const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('keyup', () => this.filterWorkspaces()); } // Setup button click handlers this.setupButtonHandlers(); } /** * Setup basic interactions immediately */ setupInteractions() { // Close sidebar on overlay click const overlay = document.getElementById('overlay'); if (overlay) { overlay.addEventListener('click', () => this.closeSidebar()); } } /** * Initialize background tasks (authentication, data loading) */ async initializeBackgroundTasks() { try { // Check authentication in background this.isAuthenticated = this.authService.isUserAuthenticated(); if (!this.isAuthenticated) { this.showUnauthenticatedView(); return; } // Load workspace data in background await this.loadWorkspaces(); // Update the UI with real data this.updateWorkspaceDisplay(); } catch (error) { console.error('Background initialization failed:', error); this.showError('Failed to load workspace data'); } } /** * 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(); } } /** * 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.setupAdvancedInteractions(); } 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 HTML content */ getWorkspaceManagementHTML() { return ` <div class="bsp-page-container bsp-manage-workspace"> <header class="bsp-page-header"> <div class="bsp-container"> <h1 class="bsp-heading-1" id="page-title">Workspace Management</h1> <p class="bsp-text-lg bsp-text-secondary" id="page-subtitle"> Manage organizational workspaces and their hierarchies </p> </div> </header> <main class="bsp-page-content"> <div class="bsp-container"> <div id="page-content-area"> <!-- Header Actions --> <div class="workspace-header"> <div class="header-actions"> <button class="btn btn-secondary" data-action="export"> Export </button> <button class="btn btn-primary" data-action="create"> 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">12</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">1,247</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">486</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">8</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"> <!-- Initial demo data --> <tr> <td><div class="workspace-name">Global Organization</div></td> <td><span class="parent-workspace">—</span></td> <td class="center">1,247</td> <td class="center">486</td> <td><div class="cost-pools"><span class="cost-pool-badge">Enterprise</span><span class="cost-pool-badge">Operations</span></div></td> <td class="center"><div class="actions-cell"> <button class="action-btn view" title="View">👁️</button> <button class="action-btn edit" title="Edit">✏️</button> <button class="action-btn admin" title="Manage Administrators">👥</button> </div></td> </tr> </tbody> </table> </div> </div> </div> </main> </div> `; } /** * Setup advanced page interactions after data loads */ setupAdvancedInteractions() { // 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-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 - matches contact-us pattern exactly if (typeof module !== 'undefined' && module.exports) { module.exports = ManageWorkspacePage; } else if (typeof window !== 'undefined') { window.ManageWorkspacePage = ManageWorkspacePage; }