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
JavaScript
/**
* 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;
}