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