UNPKG

besper-frontend-site-dev-main

Version:

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

1,532 lines (1,378 loc) 68.5 kB
/** * Comprehensive Account Management with Workspace Hierarchy and APIM Dataverse Integration * Implements user impersonation, workspace hierarchy management, and related entity processing * Follows developer guide requirements for structured format with parent account relationships */ class AccountManagementPage { constructor(options = {}) { this.options = { containerId: 'besper-site-content', environment: 'prod', ...options, }; this.initialized = false; // Comprehensive workspace management system this.currentWorkspace = null; this.workspaces = []; this.isEditMode = false; this.originalValues = {}; this.apiBaseUrl = this.getApiBaseUrl(); this.userToken = null; this.userId = null; // BULLETPROOF: Defer all complex operations to avoid blocking class export this.authService = null; this.operatorsService = null; this.isAuthenticated = false; this.authInitialized = false; this.authInitializing = false; // Initialize auth safely in next tick to ensure class export completes first if (typeof window !== 'undefined') { setTimeout(() => { this.initializeAuth(); }, 1); } } /** * Get API base URL for Dataverse operations via APIM */ getApiBaseUrl() { // Determine the correct API base URL based on environment const hostname = window.location.hostname; if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) { return 'http://localhost:7071/api'; } else { // Use APIM internal endpoint for administrative operations const envMatch = hostname.match(/(\w+)\.powerapp/); const environment = envMatch ? envMatch[1] : 'dev'; return `https://apim${environment}0926internal.azure-api.net/api/admin`; } } /** * Initialize authentication safely without blocking class export */ initializeAuth() { // Prevent multiple simultaneous auth initializations if (this.authInitialized || this.authInitializing) { console.log( '[WorkspaceManagement] Auth already initialized or initializing' ); return; } this.authInitializing = true; try { this.authService = this.getAuthService(); this.operatorsService = this.getOperatorsService(); // Initialize workspace authentication for APIM Dataverse operations this.initializeWorkspaceAuth(); this.authInitialized = true; } catch (error) { console.warn('Auth initialization failed, using fallback:', error); this.authService = this.createFallbackAuthService(); this.operatorsService = this.createFallbackOperatorsService(); this.authInitialized = true; } finally { this.authInitializing = false; } } /** * Initialize authentication for workspace management with APIM Dataverse */ async initializeWorkspaceAuth() { try { console.log( '[WorkspaceManagement] Initializing workspace authentication with PROFESSIONAL cookie-based tokens...' ); // Use PROFESSIONAL cookie-based token manager (hyper-secure and infinite-loop-proof) if (window.professionalTokenManager) { try { const token = await window.professionalTokenManager.getToken( 'WorkspaceManagement' ); if (token) { this.userToken = token; // Get user info from professional token manager const userInfo = window.professionalTokenManager.getUserInfo(); this.userId = userInfo.contactId || userInfo.userId || 'unknown'; console.log( '[WorkspaceManagement] [SECURITY] Authentication initialized with PROFESSIONAL tokens, user:', this.userId ); this.isAuthenticated = true; return; } } catch (error) { console.warn( '[WorkspaceManagement] Professional token manager failed:', error ); } } // Fallback to cookieTokenManager if professional not available if (window.cookieTokenManager) { try { const token = await window.cookieTokenManager.getToken( 'WorkspaceManagement' ); if (token) { this.userToken = token; // Get user info from cookie manager const userInfo = window.cookieTokenManager.getUserInfo(); this.userId = userInfo.contactId || userInfo.userId || 'unknown'; console.log( '[WorkspaceManagement] 🍪 Authentication from cookie token manager' ); this.isAuthenticated = true; return; } } catch (error) { console.warn( '[WorkspaceManagement] Cookie token manager failed:', error ); } } // Final fallback to global token functions if (window.generateToken) { try { const token = await window.generateToken('WorkspaceManagement'); if (token) { this.userToken = token; this.userId = window.getUserInfo?.()?.contactId || 'unknown'; console.log( '[WorkspaceManagement] 🔑 Authentication from global generateToken' ); this.isAuthenticated = true; return; } } catch (error) { console.warn( '[WorkspaceManagement] Global token generation failed:', error ); } } // Instead of throwing error, set to demo mode console.log( '[WorkspaceManagement] No authentication available, using demo mode' ); this.userToken = null; this.userId = null; } catch (error) { console.error('[WorkspaceManagement] Authentication failed:', error); this.userToken = null; this.userId = 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; } 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 with enhanced authentication timing */ createFallbackAuthService() { return { isUserAuthenticated: () => { try { if (typeof window !== 'undefined') { // Check multiple authentication sources for robust detection return !!( // Power Pages Simple Token Manager ( (window.auth && typeof window.auth.getToken === 'function' && window.auth.getToken()) || // Direct PowerPages auth data (window.PowerPagesAuth && window.PowerPagesAuth.token) || // Global variables set by liquid template window.bsp_token || // Cached tokens localStorage.getItem('bsp_token') || sessionStorage.getItem('bsp_token') || // Guest token also counts as "authenticated" for functionality sessionStorage.getItem('bsp_guest_token') ) ); } return false; } catch (error) { console.warn('[Auth] Authentication check failed:', error); return false; } }, // Enhanced method with retry logic for token generation timing isUserAuthenticatedWithRetry: async ( maxWaitTime = 5000, checkInterval = 250 ) => { const startTime = Date.now(); // Only log in console, not to user interface console.log('[Auth] Checking for authentication token...'); while (Date.now() - startTime < maxWaitTime) { try { // Check multiple token sources for robust authentication if ( typeof window !== 'undefined' && // Power Pages Simple Token Manager ((window.auth && typeof window.auth.getToken === 'function' && !!window.auth.getToken()) || // Direct PowerPages auth data (window.PowerPagesAuth && window.PowerPagesAuth.token) || // Global variables set by liquid template window.bsp_token || // localStorage cached token localStorage.getItem('bsp_token') || // Session storage token sessionStorage.getItem('bsp_token')) ) { console.log('[Auth] Authentication token found'); return true; } } catch (error) { console.warn('[Auth] Token check failed:', error); } // Shorter wait interval for faster response await new Promise(resolve => setTimeout(resolve, checkInterval)); } // Log timeout only to console, generate fallback token for functionality console.log( '[Auth] Token check timeout - generating fallback token for public access' ); // Generate a fallback guest token to ensure functionality this.generateFallbackGuestToken(); return true; // Return true to allow page to function with guest access }, // Generate fallback guest token for public functionality generateFallbackGuestToken: () => { try { const guestToken = 'guest_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); if (typeof window !== 'undefined') { // Store guest token for session sessionStorage.setItem('bsp_guest_token', guestToken); console.log('[Auth] Generated guest token for public access'); } } catch (error) { console.warn('[Auth] Failed to generate guest token:', error); } }, getToken: () => { try { if (typeof window !== 'undefined') { // Try multiple token sources for robust authentication // 1. Power Pages Simple Token Manager (primary) if (window.auth && typeof window.auth.getToken === 'function') { const token = window.auth.getToken(); if (token) return token; } // 2. Direct PowerPages auth data if (window.PowerPagesAuth && window.PowerPagesAuth.token) { return window.PowerPagesAuth.token; } // 3. Global variables set by liquid template if (window.bsp_token) { return window.bsp_token; } // 4. localStorage cached token const cachedToken = localStorage.getItem('bsp_token'); if (cachedToken) return cachedToken; // 5. Session storage token const sessionToken = sessionStorage.getItem('bsp_token'); if (sessionToken) return sessionToken; // 6. Guest token for public access const guestToken = sessionStorage.getItem('bsp_guest_token'); if (guestToken) return guestToken; } } catch (error) { console.warn('[Auth] 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 { callOperator: async () => { console.warn( 'Operators service not available, returning empty response' ); 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 account 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 account 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;">Account Management</h1> <p style="color: #6c757d; font-size: 14px; margin: 0;">Manage your account settings and preferences</p> </div> <!-- Main Content Area --> <div style="background: white; border-radius: 8px; border: 1px solid #e0e0e0; overflow: hidden;"> <div id="page-content-area" style="padding: 2rem;"> <!-- Clean loading state --> <div style="text-align: center; padding: 2rem;"> <div style="display: inline-block; width: 20px; height: 20px; border: 2px solid #e0e0e0; border-top: 2px solid #022d54; border-radius: 50%; animation: spin 1s linear infinite;"></div> <p style="color: #6c757d; margin-top: 1rem;">Loading content...</p> </div> </div> </div> </div> </div> <!-- Loading Animation --> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> `; } /** * Setup interactions - immediate setup without waiting */ setupInteractions() { // TODO: Add page-specific interactions here console.log('Account Management interactions ready'); } /** * Initialize authentication features completely in background - NO UI BLOCKING * Now includes retry logic to wait for token generation as requested */ async initializeAuthenticationFeaturesInBackground() { // Use requestIdleCallback for true background processing const initializeAuth = async () => { try { // Ensure services are ready if (!this.authService) { this.initializeAuth(); } console.log( '[Site] Checking authentication status (allowing time for token generation)...' ); // Check authentication status with retry logic for token generation if ( this.authService && typeof this.authService.isUserAuthenticatedWithRetry === 'function' ) { // Use the enhanced method that waits for token generation this.isAuthenticated = await this.authService.isUserAuthenticatedWithRetry(15000, 1000); } else { // Fallback to immediate check this.isAuthenticated = this.authService ? this.authService.isUserAuthenticated() : false; } if (this.isAuthenticated) { console.log('[Site] User authenticated for page: account-management'); // Load data in background this.loadDataInBackground(); } else { console.log( '[Site] User not authenticated for page: account-management' ); this.showUnauthenticatedView(); } } catch (error) { console.warn( 'Background authentication features initialization failed:', error ); // Continue silently - authentication is not critical for UI display this.showUnauthenticatedView(); } }; // 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 with comprehensive Dataverse queries */ async loadDataInBackground() { try { console.log( 'Loading comprehensive account management data in background...' ); // Load comprehensive data from APIM Dataverse with multiple entity queries await this.loadAccountDataFromAPIM(); // Update UI after data loading setTimeout(() => { this.updateUIWithActualContent(); }, 500); } catch (error) { console.error('Background comprehensive data loading failed:', error); // Show basic UI even if data loading fails this.updateUIWithActualContent(); } } /** * Update UI with actual content (replace skeleton loading) * Now using the static template structure from template.html */ updateUIWithActualContent() { // Get user's preferred language const language = this.getCurrentLanguage(); const translations = this.getTranslations(language); // Hide loading state and show main content const loadingState = document.getElementById('loading-state'); const mainContainer = document.getElementById('mainContainer'); if (loadingState) { loadingState.style.display = 'none'; } if (mainContainer) { mainContainer.style.display = 'block'; } // Initialize account management functionality this.initializeAccountManagementFeatures(translations); } /** * Get current language (German or English) */ getCurrentLanguage() { // Check URL parameter first const urlParams = new URLSearchParams(window.location.search); const langParam = urlParams.get('lang'); if (langParam && ['en', 'de'].includes(langParam)) { return langParam; } // Check localStorage const storedLang = localStorage.getItem('bsp_language'); if (storedLang && ['en', 'de'].includes(storedLang)) { return storedLang; } // Check browser language const browserLang = navigator.language.toLowerCase(); if (browserLang.startsWith('de')) { return 'de'; } // Default to English return 'en'; } /** * Get translations for the specified language */ getTranslations(language) { const translations = { en: { title: 'Account Management', subtitle: 'Manage your account settings and workspace access', totalWorkspaces: 'TOTAL WORKSPACES', totalLicenses: 'TOTAL LICENSES', monthlyUsage: 'MONTHLY USAGE', userCount: 'TOTAL USERS', activeWorkspaces: 'Active workspaces', licenseAllocation: 'License allocation', thisMonth: 'This month', totalUsers: 'Total users', allWorkspaces: 'My Workspaces', searchWorkspaces: 'Search workspaces...', workspaceName: 'WORKSPACE NAME', role: 'ROLE', lastAccess: 'LAST ACCESS', status: 'STATUS', actions: 'ACTIONS', noWorkspacesFound: 'No workspaces found', noWorkspacesMessage: "You don't have access to any workspaces yet", createWorkspace: 'Request Access', accountSettings: 'Account Settings', profileInfo: 'Profile Information', securitySettings: 'Security Settings', export: 'Export', view: 'View', edit: 'Edit', active: 'Active', inactive: 'Inactive', admin: 'Admin', member: 'Member', viewer: 'Viewer', loadingAccount: 'Loading account information...', }, de: { title: 'Kontoverwaltung', subtitle: 'Verwalten Sie Ihre Kontoeinstellungen und Arbeitsbereich-Zugriffe', totalWorkspaces: 'GESAMTE ARBEITSBEREICHE', totalLicenses: 'GESAMTE LIZENZEN', monthlyUsage: 'MONATLICHE NUTZUNG', userCount: 'GESAMTE BENUTZER', activeWorkspaces: 'Aktive Arbeitsbereiche', licenseAllocation: 'Lizenzzuteilung', thisMonth: 'Diesen Monat', totalUsers: 'Gesamte Benutzer', allWorkspaces: 'Meine Arbeitsbereiche', searchWorkspaces: 'Arbeitsbereiche suchen...', workspaceName: 'ARBEITSBEREICH-NAME', role: 'ROLLE', lastAccess: 'LETZTER ZUGRIFF', status: 'STATUS', actions: 'AKTIONEN', noWorkspacesFound: 'Keine Arbeitsbereiche gefunden', noWorkspacesMessage: 'Sie haben noch keinen Zugriff auf Arbeitsbereiche', createWorkspace: 'Zugriff anfordern', accountSettings: 'Kontoeinstellungen', profileInfo: 'Profilinformationen', securitySettings: 'Sicherheitseinstellungen', export: 'Exportieren', view: 'Anzeigen', edit: 'Bearbeiten', active: 'Aktiv', inactive: 'Inaktiv', admin: 'Administrator', member: 'Mitglied', viewer: 'Betrachter', loadingAccount: 'Kontoinformationen werden geladen...', }, }; return translations[language] || translations.en; } /** * Initialize account management features */ initializeAccountManagementFeatures(translations) { this.translations = translations; this.accountData = { workspaces: [], stats: { totalWorkspaces: 0, totalLicenses: 0, totalUsers: 0, monthlyUsage: '0', }, }; // Initialize workspace management system this.initializeWorkspaceManagement(); // Update UI with current translations this.updateUITranslations(); // Load account data from APIM APIs this.loadAccountDataFromAPIM(); } /** * Initialize workspace management system with comprehensive business logic */ initializeWorkspaceManagement() { this.currentWorkspace = null; this.workspaces = []; this.isEditMode = false; this.originalValues = {}; this.userToken = null; this.userId = null; // Initialize authentication for workspace management this.initializeWorkspaceAuth(); } /** * Load workspaces with user impersonation and hierarchy support via APIM Dataverse */ async loadWorkspacesWithHierarchy() { try { console.log( '[WorkspaceManagement] 📡 Loading workspaces with APIM user impersonation...' ); // Ensure proper authentication for Dataverse operations if (!this.userToken || !this.userId) { await this.initializeWorkspaceAuth(); } if (!this.userToken) { throw new Error( 'User authentication required for Dataverse workspace operations' ); } const response = await fetch( `${this.apiBaseUrl}/administrative/workspace/list`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.userToken}`, 'X-User-Impersonation': this.userId, 'X-API-Version': '2024-01', }, body: JSON.stringify({ user_id: this.userId, user_token: this.userToken, include_hierarchy: true, include_aggregates: true, include_related_entities: true, impersonation_context: { source: 'account_management', permissions: ['workspace.read', 'workspace.manage'], }, }), } ); if (!response.ok) { throw new Error( `APIM Dataverse API request failed: ${response.status} ${response.statusText}` ); } const data = await response.json(); if (!data.success) { throw new Error( data.message || 'Failed to load workspaces from Dataverse' ); } this.workspaces = data.workspaces || []; console.log( '[WorkspaceManagement] [SUCCESS] Loaded', this.workspaces.length, 'workspaces from APIM Dataverse' ); // Process workspace hierarchy and related entities this.processWorkspaceHierarchy(data); // Update UI with real data from Dataverse this.updateWorkspaceTableWithHierarchy(); this.updateStatsFromWorkspaces(); } catch (error) { console.error( '[WorkspaceManagement] Failed to load workspaces from APIM:', error ); this.showError( 'Failed to load workspace data from Dataverse. Please try again.' ); // Show demo data for development this.loadDemoWorkspaceData(); } } /** * Process workspace hierarchy and related entities from Dataverse */ processWorkspaceHierarchy(data) { // Process parent-child relationships this.workspaces.forEach(workspace => { if (workspace.parentWorkspaceId) { const parent = this.workspaces.find( ws => ws.id === workspace.parentWorkspaceId ); workspace.parentWorkspaceName = parent ? parent.name : 'Unknown Parent'; workspace.isRootLevel = false; } else { workspace.parentWorkspaceName = null; workspace.isRootLevel = true; } // Process related entities from Dataverse workspace.usersData = data.users?.filter(u => u.workspaceId === workspace.id) || []; workspace.licensesData = data.licenses?.filter(l => l.workspaceId === workspace.id) || []; workspace.costPoolsData = data.costPools?.filter(cp => cp.workspaceId === workspace.id) || []; // Update counts from actual data workspace.users = workspace.usersData.length; workspace.licenses = workspace.licensesData.length; workspace.costPools = workspace.costPoolsData.map(cp => cp.name); }); } /** * Load demo workspace data with proper hierarchy for development * Follows the structured format requirements from the developer guide */ loadDemoWorkspaceDataWithHierarchy() { console.log( '[WorkspaceManagement] 📋 Loading demo data with structured hierarchy for development' ); this.workspaces = [ { id: 'ws-1', name: 'Global Organization', parentWorkspaceId: null, parentWorkspaceName: null, users: 1247, licenses: 486, costPools: ['Enterprise', 'Operations'], status: 'active', isRootLevel: true, indentationLevel: 0, hierarchyIndicator: '', monthlyUsage: 62350, // 1247 users * 50 operations usersData: [ { id: 'u1', name: 'John Doe', email: 'john@company.com', workspaceId: 'ws-1', }, { id: 'u2', name: 'Jane Smith', email: 'jane@company.com', workspaceId: 'ws-1', }, ], licensesData: [ { id: 'l1', type: 'Professional', status: 'active', workspaceId: 'ws-1', }, { id: 'l2', type: 'Enterprise', status: 'active', workspaceId: 'ws-1', }, ], costPoolsData: [ { id: 'cp1', name: 'Enterprise', budget: '$50,000', workspaceId: 'ws-1', }, { id: 'cp2', name: 'Operations', budget: '$25,000', workspaceId: 'ws-1', }, ], }, { id: 'ws-2', name: 'Enterprise Division', parentWorkspaceId: 'ws-1', parentWorkspaceName: 'Global Organization', users: 342, licenses: 125, costPools: ['Enterprise'], status: 'active', isRootLevel: false, indentationLevel: 1, hierarchyIndicator: '└─ ', monthlyUsage: 17100, // 342 users * 50 operations usersData: [ { id: 'u3', name: 'Mike Wilson', email: 'mike@enterprise.com', workspaceId: 'ws-2', }, ], licensesData: [ { id: 'l3', type: 'Professional', status: 'active', workspaceId: 'ws-2', }, ], costPoolsData: [ { id: 'cp3', name: 'Enterprise Dev', budget: '$15,000', workspaceId: 'ws-2', }, ], }, { id: 'ws-3', name: 'Product Development', parentWorkspaceId: 'ws-1', parentWorkspaceName: 'Global Organization', users: 185, licenses: 87, costPools: ['R&D', 'Engineering'], status: 'active', isRootLevel: false, indentationLevel: 1, hierarchyIndicator: '└─ ', monthlyUsage: 9250, // 185 users * 50 operations usersData: [ { id: 'u4', name: 'Sarah Connor', email: 'sarah@product.com', workspaceId: 'ws-3', }, ], licensesData: [ { id: 'l4', type: 'Developer', status: 'active', workspaceId: 'ws-3', }, ], costPoolsData: [ { id: 'cp4', name: 'R&D', budget: '$30,000', workspaceId: 'ws-3' }, ], }, { id: 'ws-4', name: 'Engineering Team', parentWorkspaceId: 'ws-3', parentWorkspaceName: 'Product Development', users: 45, licenses: 23, costPools: ['Engineering'], status: 'active', isRootLevel: false, indentationLevel: 2, hierarchyIndicator: ' └─ ', monthlyUsage: 2250, // 45 users * 50 operations usersData: [ { id: 'u5', name: 'Alex Tech', email: 'alex@engineering.com', workspaceId: 'ws-4', }, ], licensesData: [ { id: 'l5', type: 'Developer', status: 'active', workspaceId: 'ws-4', }, ], costPoolsData: [ { id: 'cp5', name: 'Engineering Core', budget: '$8,000', workspaceId: 'ws-4', }, ], }, ]; // Sort for hierarchical display this.workspaces = this.sortWorkspacesForHierarchicalDisplay( this.workspaces ); this.updateWorkspaceTableWithStructuredHierarchy(); this.updateStatsFromComprehensiveData(); } /** * Update workspace table with structured hierarchy and proper indentation * Implements the styling requirements from the root example */ updateWorkspaceTableWithStructuredHierarchy() { const tbody = document.getElementById('workspaceTableBody'); const emptyState = document.getElementById('emptyState'); if (!tbody) return; if (this.workspaces.length === 0) { tbody.innerHTML = ''; if (emptyState) emptyState.style.display = 'block'; return; } if (emptyState) emptyState.style.display = 'none'; tbody.innerHTML = this.workspaces .map(workspace => { const isHierarchical = workspace.parentWorkspaceId !== null; const hierarchyClass = isHierarchical ? 'workspace-hierarchy' : ''; const indentationStyle = workspace.indentationLevel > 0 ? `padding-left: ${workspace.indentationLevel * 20}px;` : ''; return ` <tr onclick="window.accountManagementPage.openWorkspaceDetails('${workspace.id}')" style="cursor: pointer;"> <td> <div class="workspace-name ${hierarchyClass}" style="${indentationStyle}"> ${workspace.hierarchyIndicator}${workspace.name} </div> ${ workspace.parentWorkspaceName ? `<div class="parent-workspace" style="${indentationStyle}">↳ ${workspace.parentWorkspaceName}</div>` : '' } </td> <td>${workspace.parentWorkspaceName || '—'}</td> <td class="center"><span class="stat-number">${workspace.users}</span></td> <td class="center"><span class="stat-number">${workspace.licenses}</span></td> <td> ${ workspace.costPools ?.map(cp => `<span class="role-badge member">${cp}</span>`) .join(' ') || '—' } </td> <td class="center"> <span class="status-badge status-${workspace.status}"> ${workspace.status} </span> </td> <td class="center"> <div class="actions-cell"> <button class="action-btn" title="View Details" onclick="event.stopPropagation(); window.accountManagementPage.openWorkspaceDetails('${workspace.id}')"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> <circle cx="12" cy="12" r="3"/> </svg> </button> <button class="action-btn" title="Edit Workspace" onclick="event.stopPropagation(); window.accountManagementPage.openWorkspaceDetails('${workspace.id}', true)"> <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> </div> </td> </tr> `; }) .join(''); } /** * Legacy method name for backward compatibility */ updateWorkspaceTableWithHierarchy() { return this.updateWorkspaceTableWithStructuredHierarchy(); } /** * Update stats from comprehensive Dataverse data */ updateStatsFromComprehensiveData() { const totalWorkspaces = this.workspaces.length; const totalUsers = this.workspaces.reduce((sum, ws) => sum + ws.users, 0); const totalLicenses = this.workspaces.reduce( (sum, ws) => sum + ws.licenses, 0 ); const allCostPools = new Set(); this.workspaces.forEach(ws => { ws.costPools?.forEach(cp => allCostPools.add(cp)); }); // Calculate monthly usage from actual data - sum from workspace usage metrics let monthlyUsage = 0; this.workspaces.forEach(ws => { // If workspace has usage data, add it if (ws.monthlyUsage && typeof ws.monthlyUsage === 'number') { monthlyUsage += ws.monthlyUsage; } else if (ws.usersData && ws.usersData.length > 0) { // Estimate based on user activity if no explicit usage data monthlyUsage += ws.usersData.length * 50; // Estimate 50 operations per user per month } }); // Format monthly usage for display let monthlyUsageDisplay = '0'; if (monthlyUsage >= 1000) { monthlyUsageDisplay = (monthlyUsage / 1000).toFixed(1) + 'k'; } else { monthlyUsageDisplay = monthlyUsage.toString(); } // Update account stats with comprehensive data from actual operations this.accountData = this.accountData || {}; this.accountData.stats = { totalWorkspaces, totalUsers, totalLicenses, monthlyUsage: monthlyUsageDisplay, costPools: allCostPools.size, }; // Update UI elements this.updateStatsDisplay(); } /** * Legacy method name for backward compatibility */ updateStatsFromWorkspaces() { return this.updateStatsFromComprehensiveData(); } /** * Open workspace details with comprehensive APIM Dataverse management */ async openWorkspaceDetails(workspaceId, editMode = false) { console.log( '[WorkspaceManagement] Opening comprehensive workspace details via APIM:', workspaceId ); try { // Find workspace in loaded data const workspace = this.workspaces.find(ws => ws.id === workspaceId); if (!workspace) { throw new Error('Workspace not found'); } this.currentWorkspace = workspace; this.isEditMode = editMode; // Load detailed workspace data with all related entities from Dataverse await this.loadComprehensiveWorkspaceDetailsFromDataverse(workspaceId); // Show comprehensive workspace details UI this.showComprehensiveWorkspaceDetailsModal(workspace); } catch (error) { console.error( '[WorkspaceManagement] Failed to open comprehensive workspace details:', error ); this.showError( 'Failed to load comprehensive workspace details from Dataverse' ); } } /** * Load comprehensive workspace details from APIM Dataverse with all entity queries */ async loadComprehensiveWorkspaceDetailsFromDataverse(workspaceId) { try { console.log( '[WorkspaceManagement] Loading comprehensive workspace details from Dataverse:', workspaceId ); if (!this.userToken) { throw new Error( 'Authentication required for comprehensive Dataverse operations' ); } // Comprehensive API call for workspace details with all related entities const response = await fetch( `${this.apiBaseUrl}/administrative/workspace/details`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.userToken}`, 'X-User-Impersonation': this.userId, 'X-Include-Entities': 'accounts,contacts,subscriptions,costpools,permissions', }, body: JSON.stringify({ workspace_id: workspaceId, user_id: this.userId, user_token: this.userToken, // Comprehensive entity queries include_users: true, include_licenses: true, include_cost_pools: true, include_permissions: true, include_parent_relationships: true, include_child_relationships: true, include_aggregated_stats: true, }), } ); if (!response.ok) { throw new Error( `Comprehensive Dataverse API request failed: ${response.status} ${response.statusText}` ); } const data = await response.json(); if (!data.success) { throw new Error( data.message || 'Failed to load comprehensive workspace details from Dataverse' ); } // Update current workspace with comprehensive data from all Dataverse entities Object.assign(this.currentWorkspace, data.workspace); this.currentWorkspace.usersData = data.users || []; this.currentWorkspace.licensesData = data.licenses || []; this.currentWorkspace.costPoolsData = data.costPools || []; this.currentWorkspace.permissions = data.permissions || []; this.currentWorkspace.parentRelationships = data.parentRelationships || []; this.currentWorkspace.childRelationships = data.childRelationships || []; console.log( '[WorkspaceManagement] [SUCCESS] Loaded comprehensive workspace details from Dataverse with all entities' ); } catch (error) { console.error( '[WorkspaceManagement] Failed to load comprehensive workspace details from Dataverse:', error ); throw error; } } /** * Show comprehensive workspace details modal with all entity information */ showComprehensiveWorkspaceDetailsModal(workspace) { const hierarchyInfo = workspace.parentWorkspaceName ? `Parent: ${workspace.parentWorkspaceName} (Level ${workspace.indentationLevel})` : 'Root Level Workspace'; const details = ` COMPREHENSIVE WORKSPACE DETAILS FROM DATAVERSE ============================================= Workspace: ${workspace.name} ${hierarchyInfo} Hierarchy Indicator: ${workspace.hierarchyIndicator} Status: ${workspace.status} ENTITY STATISTICS: - Users (Contacts): ${workspace.users} - Licenses (Subscriptions): ${workspace.licenses} - Cost Pools: ${workspace.costPools?.join(', ') || 'None'} - Permissions: ${workspace.permissions?.length || 0} RELATED ENTITY DETAILS: - Users Data: ${workspace.usersData?.length || 0} contact records - License Data: ${workspace.licensesData?.length || 0} subscription records - Cost Pool Data: ${workspace.costPoolsData?.length || 0} cost pool records - Parent Relationships: ${workspace.parentRelationships?.length || 0} - Child Relationships: ${workspace.childRelationships?.length || 0} DATA SOURCE: APIM Dataverse API with User Impersonation Entity Queries: accounts, contacts, subscriptions, costpools, permissions Structured Format: Hierarchical with parent relationship indentation `; alert(details); // TODO: Replace with proper comprehensive modal UI implementation console.log( '[WorkspaceManagement] Comprehensive workspace details loaded from all Dataverse entities:', workspace ); } /** * Export comprehensive workspace data with structured hierarchy format */ exportComprehensiveWorkspaceData() { console.log( '[WorkspaceManagement] Exporting comprehensive workspace data from APIM Dataverse...' ); const exportData = { workspaces: this.workspaces, exportDate: new Date().toISOString(), userId: this.userId, source: 'APIM_Dataverse_Comprehensive', apiBaseUrl: this.apiBaseUrl, version: '2024.01', dataStructure: 'hierarchical_with_indentation', entityQueries: [ 'accounts', 'contacts', 'subscriptions', 'costpools', 'permissions', ], metadata: { totalWorkspaces: this.workspaces.length, totalUsers: this.workspaces.reduce( (sum, ws) => sum + (ws.users || 0), 0 ), totalLicenses: this.workspaces.reduce( (sum, ws) => sum + (ws.licenses || 0), 0 ), hierarchicalWorkspaces: this.workspaces.filter(ws => !ws.isRootLevel) .length, rootWorkspaces: this.workspaces.filter(ws => ws.isRootLevel).length, maxIndentationLevel: Math.max( ...this.workspaces.map(ws => ws.indentationLevel || 0) ), parentChildRelationships: this.workspaces.filter( ws => ws.parentWorkspaceId ).length, }, structuredHierarchy: this.workspaces.map(ws => ({ id: ws.id, name: ws.name, level: ws.indentationLevel, indicator: ws.hierarchyIndicator, parent: ws.parentWorkspaceName, isRoot: ws.isRootLevel, })), }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `comprehensive-workspace-dataverse-export-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); console.log( '[WorkspaceManagement] Comprehensive workspace data exported successfully from APIM Dataverse' ); } /** * Update UI translations for all elements */ updateUITranslations() { // Update text content based on current translations const updateElement = (id, key) => { const element = document.getElementById(id); if (element && this.translations[key]) { element.textContent = this.translations[key]; } }; // Header elements updateElement('page-title', 'title'); updateElement('page-subtitle', 'subtitle'); updateElement('export-btn-text', 'export'); // Stats labels updateElement('total-workspaces-label', 'totalWorkspaces'); updateElement('total-licenses-label', 'totalLicenses'); updateElement('monthly-usage-label', 'monthlyUsage'); updateElement('user-count-label', 'userCount'); // Stats subtexts updateElement('active-workspaces-text', 'activeWorkspaces'); updateElement('license-allocation-text', 'licenseAllocation'); updateElement('this-month-text', 'thisMonth'); updateElement('user-count-text', 'totalUsers'); // Table elements updateElement('my-workspaces-title', 'allWorkspaces'); updateElement('workspace-name-header', 'workspaceName'); updateElement('role-header', 'role'); updateElement('last-access-header', 'lastAccess'); updateElement('status-header', 'status'); updateElement('actions-header', 'actions'); // Empty state updateElement('no-workspaces-title', 'noWorkspacesFound'); updateElement('no-workspaces-text', 'noWorkspacesMessage'); updateElement('loading-text', 'loadingAccount'); // Update search placeholder const searchInput = document.getElementById('searchInput'); if (searchInput && this.translations.searchWorkspaces) { searchInput.placeholder = this.translations.searchWorkspaces; } } /** * Load account data from APIM with comprehensive Dataverse queries * Implements multiple entity queries as required by developer guide */ async loadAccountDataFromAPIM() { try { console.log( '[AccountManagement] Loading account data from APIM Dataverse...' ); // Ensure authentication is available - fail if not authenticated if (!this.authService?.isUserAuthenticated()) { throw new Error( 'Authentication required for account management operations' ); } // Load comprehensive workspace data using APIM operators await this.loadWorkspacesWithAPIMOperators(); } catch (error) { console.error('[AccountManagement] Failed to load account data:', error); throw error; // Re-throw error instead of falling back to sample data } } /** * Load workspaces with comprehensive Dataverse queries for multiple entities * Implements structured format with parent account relationship indentation */ /** * DEPRECATED: Load workspaces with comprehensive Dataverse queries for multiple entities * Use loadWorkspacesWithAPIMOperators() instead */ async loadWorkspacesWithComprehensiveDataverseQueries() { console.warn( '[AccountManagement] loadWorkspacesWithComprehensiveDataverseQueries is deprecated. Use loadWorkspacesWithAPIMOperators() instead.' ); throw new Error( 'Direct Dataverse queries are deprecated. Use APIM operators instead.' ); } /** * Process comprehensive Dataverse response with multiple entities * Implements structured format with parent account relationship indentation */ processComprehensiveDataverseResponse(data) { console.log( '[AccountManagement] Processing comprehensive Dataverse response with multiple entities...' ); // Extract workspaces from account entities this.workspaces = data.workspaces || []; // Process parent-child relationships with proper indentation structure this.workspaces.forEach(workspace => { if (workspace.parentWorkspaceId) { const parent = this.workspaces.find( ws => ws.id === workspace.parentWorkspaceId ); workspace.parentWorkspaceName = parent ? parent.name : 'Unknown Parent'; workspace.isRootLevel = false; // Add indentation level for hierarchical display workspace.indentationLevel = this.calculateIndentationLevel(workspace); workspace.hierarchyIndicator = this.getHierarchyIndicator( workspace.indentationLevel ); } else { workspace.parentWorkspaceName = null; workspace.isRootLevel = true; workspace.indentationLevel = 0; workspace.hierarchyIndicator = ''; } // Process related entities from comprehensive Dataverse queries workspace.usersData = data.users?.filter(u => u.workspaceId === workspace.id) || []; workspace.licensesData = data.licenses?.filter(l => l.workspaceId === workspace.id) || []; workspace.costPoolsData = data.costPools?.filter(cp => cp.workspaceId === workspace.id) || []; workspace.permissions = data.permissions?.filter(p => p.workspaceId === workspace.id) || []; // Update counts from actual Dataverse entity data workspace.users = workspace.usersData.length; workspace.licenses = workspace.licensesData.length; workspace.costPools = workspace.costPoolsData.map(cp => cp.name); }); // Sort workspaces to show hierarchy properly with parent relationships first this.works