/** * QuadrantX Dashboard - Main Application * * Initializes and orchestrates all dashboard functionality. */ class DashboardApp { constructor() { this.config = window.QUADRANTX_CONFIG; this.auth = new AuthManager(this.config); this.api = new ApiClient(this.config, this.auth); this.charts = new ChartManager(); // State this.currentCategory = null; this.currentChartType = 'quadrant'; this.appliedSettings = {}; // DOM elements this.elements = {}; } /** * Initialize application */ async init() { try { // Cache DOM elements this.cacheElements(); // Initialize authentication const isAuthenticated = await this.auth.init(); if (!isAuthenticated) { this.showLoginRequired(); return; } if (!this.auth.isApproved()) { this.showPendingApproval(); return; } // User is authenticated and approved this.showDashboard(); await this.initializeDashboard(); } catch (error) { console.error('App initialization error:', error); this.showError('Failed to initialize application'); } } /** * Cache DOM element references */ cacheElements() { this.elements = { loadingState: document.getElementById('loadingState'), loginRequired: document.getElementById('loginRequired'), pendingApproval: document.getElementById('pendingApproval'), dashboard: document.getElementById('dashboard'), // Auth buttons loginBtn: document.getElementById('loginBtn'), signupBtn: document.getElementById('signupBtn'), logoutBtn: document.getElementById('logoutBtn'), logoutPendingBtn: document.getElementById('logoutPendingBtn'), // User info userEmail: document.getElementById('userEmail'), // Controls categorySelect: document.getElementById('categorySelect'), startDate: document.getElementById('startDate'), endDate: document.getElementById('endDate'), allTimeCheck: document.getElementById('allTimeCheck'), showHiddenCheck: document.getElementById('showHiddenCheck'), vendorSelect: document.getElementById('vendorSelect'), sortVendors: document.getElementById('sortVendors'), modelSelect: document.getElementById('modelSelect'), updateChartsBtn: document.getElementById('updateChartsBtn'), // Summary reportsCount: document.getElementById('reportsCount'), vendorsCount: document.getElementById('vendorsCount'), modelsCount: document.getElementById('modelsCount'), timespan: document.getElementById('timespan'), // Chart chartTitle: document.getElementById('chartTitle'), // Modal reportModal: document.getElementById('reportModal'), modalVendorLogo: document.getElementById('modalVendorLogo'), modalVendorName: document.getElementById('modalVendorName'), modalReportInfo: document.getElementById('modalReportInfo'), modalScores: document.getElementById('modalScores'), modalModelScores: document.getElementById('modalModelScores') }; // Bind event handlers this.bindEvents(); } /** * Bind event handlers */ bindEvents() { // Auth buttons this.elements.loginBtn?.addEventListener('click', () => this.auth.login()); this.elements.signupBtn?.addEventListener('click', () => this.auth.signup()); this.elements.logoutBtn?.addEventListener('click', () => this.auth.logout()); this.elements.logoutPendingBtn?.addEventListener('click', () => this.auth.logout()); // Category select this.elements.categorySelect?.addEventListener('change', () => this.onCategoryChange()); // Date inputs this.elements.startDate?.addEventListener('change', () => this.onSettingsChange()); this.elements.endDate?.addEventListener('change', () => this.onSettingsChange()); // Checkboxes this.elements.allTimeCheck?.addEventListener('change', () => this.onAllTimeChange()); this.elements.showHiddenCheck?.addEventListener('change', () => this.onSettingsChange()); // Vendor select this.elements.vendorSelect?.addEventListener('change', () => this.onSettingsChange()); // Sort vendors this.elements.sortVendors?.addEventListener('change', () => this.onSortVendorsChange()); // Model select this.elements.modelSelect?.addEventListener('change', () => this.onSettingsChange()); // Update charts button this.elements.updateChartsBtn?.addEventListener('click', () => this.loadCharts()); // Tab buttons document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => this.onTabChange(tab)); }); // Modal close document.querySelector('.modal-close')?.addEventListener('click', () => this.closeModal()); this.elements.reportModal?.addEventListener('click', (e) => { if (e.target === this.elements.reportModal) this.closeModal(); }); } /** * Show login required state */ showLoginRequired() { this.elements.loadingState.style.display = 'none'; this.elements.loginRequired.style.display = 'flex'; } /** * Show pending approval state */ showPendingApproval() { this.elements.loadingState.style.display = 'none'; this.elements.pendingApproval.style.display = 'flex'; } /** * Show dashboard */ showDashboard() { this.elements.loadingState.style.display = 'none'; this.elements.dashboard.style.display = 'block'; this.elements.userEmail.textContent = this.auth.getEmail(); } /** * Initialize dashboard */ async initializeDashboard() { // Set default dates const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); this.elements.endDate.value = this.formatDate(today); this.elements.startDate.value = this.formatDate(thirtyDaysAgo); // Initialize chart this.charts.initMainChart('mainChart'); this.charts.setPointClickCallback((reportId, vendorId, vendorName, logoFile) => { this.showReportDetail(reportId, vendorId, vendorName, logoFile); }); // Load categories await this.loadCategories(); } /** * Load categories */ async loadCategories() { try { const showHidden = this.elements.showHiddenCheck.checked; const categories = await this.api.getCategories(showHidden); this.elements.categorySelect.innerHTML = categories.map(cat => `` ).join(''); if (categories.length > 0) { this.enableControls(); } } catch (error) { console.error('Load categories error:', error); this.showError('Failed to load categories'); } } /** * Enable dashboard controls */ enableControls() { this.elements.startDate.disabled = false; this.elements.endDate.disabled = false; this.elements.allTimeCheck.disabled = false; this.elements.showHiddenCheck.disabled = false; this.elements.vendorSelect.disabled = false; this.elements.sortVendors.disabled = false; this.elements.modelSelect.disabled = false; } /** * On category change */ async onCategoryChange() { const categoryId = this.elements.categorySelect.value; if (!categoryId) return; this.currentCategory = categoryId; // Load vendors, models, and summary in parallel await Promise.all([ this.loadVendors(true), this.loadModels(), this.loadSummary() ]); // Auto-load charts with top 5 vendors this.saveAppliedSettings(); await this.loadCharts(); } /** * Load vendors */ async loadVendors(selectTopFive = false) { try { const options = this.getFilterOptions(); const vendors = await this.api.getVendors(this.currentCategory, options); // Remember selected vendors const selectedIds = Array.from(this.elements.vendorSelect.selectedOptions) .map(opt => opt.value); // Update vendor list const sortBy = this.elements.sortVendors.value; this.elements.vendorSelect.innerHTML = vendors.map(v => { const displayScore = sortBy === 'score' ? ` (${v.adjusted_score.toFixed(1)} @ ${v.report_count}/${v.total_reports})` : ''; return ``; }).join(''); // Restore selection or select top 5 if (selectTopFive && selectedIds.length === 0) { Array.from(this.elements.vendorSelect.options) .slice(0, 5) .forEach(opt => opt.selected = true); } else { selectedIds.forEach(id => { const option = this.elements.vendorSelect.querySelector(`option[value="${id}"]`); if (option) option.selected = true; }); } } catch (error) { console.error('Load vendors error:', error); } } /** * Load models */ async loadModels() { try { const options = this.getFilterOptions(); const models = await this.api.getModels(this.currentCategory, options); const currentModel = this.elements.modelSelect.value; this.elements.modelSelect.innerHTML = '' + models.map(m => ``).join(''); // Restore selection if still available if (currentModel && models.includes(currentModel)) { this.elements.modelSelect.value = currentModel; } } catch (error) { console.error('Load models error:', error); } } /** * Load summary */ async loadSummary() { try { const options = this.getFilterOptions(); const summary = await this.api.getCategorySummary(this.currentCategory, options); this.elements.reportsCount.textContent = summary.total_reports || '-'; this.elements.vendorsCount.textContent = summary.unique_vendors || '-'; this.elements.modelsCount.textContent = summary.model_count || '-'; if (summary.first_report && summary.latest_report) { const first = new Date(summary.first_report); const latest = new Date(summary.latest_report); const days = Math.ceil((latest - first) / (1000 * 60 * 60 * 24)); this.elements.timespan.textContent = `${days} days`; } else { this.elements.timespan.textContent = '-'; } } catch (error) { console.error('Load summary error:', error); } } /** * Load charts */ async loadCharts() { try { const vendorIds = Array.from(this.elements.vendorSelect.selectedOptions) .map(opt => parseInt(opt.value)); if (vendorIds.length === 0) { this.charts.updateChart([], this.currentChartType); return; } const options = this.getFilterOptions(); const scores = await this.api.getVendorScores( this.currentCategory, vendorIds, options ); this.charts.updateChart(scores, this.currentChartType); this.saveAppliedSettings(); this.updateChartsButtonState(); } catch (error) { console.error('Load charts error:', error); } } /** * Get filter options from current state */ getFilterOptions() { const options = { showHidden: this.elements.showHiddenCheck.checked, sortBy: this.elements.sortVendors.value }; if (!this.elements.allTimeCheck.checked) { options.startDate = this.elements.startDate.value; options.endDate = this.elements.endDate.value; } const model = this.elements.modelSelect.value; if (model) { options.model = model; } return options; } /** * On settings change */ onSettingsChange() { this.updateChartsButtonState(); } /** * On all time checkbox change */ onAllTimeChange() { const allTime = this.elements.allTimeCheck.checked; this.elements.startDate.disabled = allTime; this.elements.endDate.disabled = allTime; if (allTime) { this.elements.startDate.value = ''; this.elements.endDate.value = ''; } else { // Reset to defaults const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); this.elements.endDate.value = this.formatDate(today); this.elements.startDate.value = this.formatDate(thirtyDaysAgo); } this.onSettingsChange(); } /** * On sort vendors change */ async onSortVendorsChange() { await this.loadVendors(false); this.onSettingsChange(); } /** * On tab change */ onTabChange(tab) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const chartType = tab.dataset.chart; this.currentChartType = chartType; // Update title const titles = { quadrant: 'Quadrant Score Over Time', nd: 'Narrative Dominance Over Time', sentiment: 'Sentiment Score Over Time' }; this.elements.chartTitle.textContent = titles[chartType] || 'Score Over Time'; // Reload with new chart type this.loadCharts(); } /** * Show report detail modal */ async showReportDetail(reportId, vendorId, vendorName, logoFile) { try { const detail = await this.api.getReportVendorDetail(reportId, vendorId); // Set header this.elements.modalVendorName.textContent = vendorName; if (logoFile) { this.elements.modalVendorLogo.src = `/static/logos/${logoFile}`; this.elements.modalVendorLogo.style.display = 'block'; } else { this.elements.modalVendorLogo.style.display = 'none'; } // Report info this.elements.modalReportInfo.innerHTML = `

Report ID: ${detail.report.id}

Category: ${detail.report.category_name}

Edition: ${detail.report.edition}

Generated: ${new Date(detail.report.generated_at).toLocaleString()}

`; // Scores this.elements.modalScores.innerHTML = `

Narrative Dominance: ${detail.vendor.narrative_dominance.toFixed(1)}

Sentiment Score: ${detail.vendor.sentiment_score.toFixed(1)}

Quadrant: ${detail.vendor.quadrant || '-'}

Overall Rank: ${detail.vendor.overall_rank || '-'}

`; // Model scores if (detail.model_scores && detail.model_scores.length > 0) { this.elements.modalModelScores.innerHTML = detail.model_scores.map(ms => `

${ms.model_name}: ND ${ms.narrative_dominance.toFixed(1)}, Sentiment ${ms.sentiment_score.toFixed(1)}

`).join(''); } else { this.elements.modalModelScores.innerHTML = '

No per-model scores available

'; } this.elements.reportModal.style.display = 'flex'; } catch (error) { console.error('Show report detail error:', error); } } /** * Close modal */ closeModal() { this.elements.reportModal.style.display = 'none'; } /** * Save current settings as applied */ saveAppliedSettings() { this.appliedSettings = this.getCurrentSettings(); this.updateChartsButtonState(); } /** * Get current settings */ getCurrentSettings() { return { category: this.elements.categorySelect.value, startDate: this.elements.startDate.value, endDate: this.elements.endDate.value, allTime: this.elements.allTimeCheck.checked, showHidden: this.elements.showHiddenCheck.checked, vendors: Array.from(this.elements.vendorSelect.selectedOptions).map(o => o.value).join(','), sortBy: this.elements.sortVendors.value, model: this.elements.modelSelect.value }; } /** * Check for unapplied changes */ hasUnappliedChanges() { const current = this.getCurrentSettings(); return JSON.stringify(current) !== JSON.stringify(this.appliedSettings); } /** * Update charts button state */ updateChartsButtonState() { const hasChanges = this.hasUnappliedChanges(); this.elements.updateChartsBtn.disabled = !hasChanges; if (hasChanges) { this.elements.updateChartsBtn.classList.add('active'); } else { this.elements.updateChartsBtn.classList.remove('active'); } } /** * Format date as YYYY-MM-DD */ formatDate(date) { return date.toISOString().split('T')[0]; } /** * Show error message */ showError(message) { console.error(message); // Could implement toast notifications here } } // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { const app = new DashboardApp(); app.init(); });