document.addEventListener('DOMContentLoaded', () => { console.log('Dashboard script loaded. Initializing...'); // --- DOM Elements --- const tableBody = document.querySelector('#data-table tbody'); const tableHead = document.querySelector('#data-table thead'); const chartCanvas = document.getElementById('data-chart'); const cardsDisplayArea = document.getElementById('cards-display-area'); const errorMessageArea = document.getElementById('error-message-area'); const datasourceSelect = document.getElementById('datasource-select'); const chartTypeSelect = document.getElementById('chart-type-select'); const refreshButton = document.getElementById('refresh-button'); // Widget wrapper elements const cardsWrapper = document.getElementById('cards-container-wrapper'); const tableWrapper = document.getElementById('table-container-wrapper'); const chartWrapper = document.getElementById('chart-container-wrapper'); // --- Chart.js State --- let currentChart = null; let currentChartType = 'bar'; // Default chart type // --- Configuration & State --- const REALTIME_UPDATE_INTERVAL = 15000; // 15 seconds for demo, 0 to disable let realtimeIntervalId = null; // --- Utility Functions --- function showLoading(widgetElement) { if (!widgetElement) return; // Remove existing loader first const existingLoader = widgetElement.querySelector('.loading-overlay'); if (existingLoader) existingLoader.remove(); const overlay = document.createElement('div'); overlay.className = 'loading-overlay'; overlay.innerHTML = '
'; widgetElement.style.position = 'relative'; // Ensure overlay positions correctly widgetElement.appendChild(overlay); } function hideLoading(widgetElement) { if (!widgetElement) return; const overlay = widgetElement.querySelector('.loading-overlay'); if (overlay) { overlay.remove(); } widgetElement.style.position = ''; // Reset position } function displayGlobalError(message) { errorMessageArea.textContent = message; errorMessageArea.style.display = 'block'; console.error('Global Error:', message); } function clearGlobalError() { errorMessageArea.textContent = ''; errorMessageArea.style.display = 'none'; } // --- Data Fetching --- async function fetchData(sourceType, params = {}) { clearGlobalError(); let widgetToLoad = null; switch (sourceType) { case 'csv_metrics': widgetToLoad = cardsWrapper; break; case 'db_sales': widgetToLoad = tableWrapper; break; case 'api_revenue': widgetToLoad = chartWrapper; break; } if (widgetToLoad) showLoading(widgetToLoad); console.log(`Fetching data for source: ${sourceType}`, params); try { const queryParams = new URLSearchParams(params).toString(); const response = await fetch(`backend/data_provider.php?source=${sourceType}&${queryParams}`); if (!response.ok) { const errorData = await response.json().catch(() => ({error: `HTTP error! Status: ${response.status}`})); throw new Error(errorData.error || `Failed to fetch. Status: ${response.status}`); } const data = await response.json(); console.log('Data received for ' + sourceType + ':', data); if (data.error) { // Handle errors returned in JSON body throw new Error(data.error); } return data; } catch (error) { console.error(`Error fetching data for ${sourceType}:`, error); displayGlobalError(`Failed to load data from ${sourceType}: ${error.message}`); return null; } finally { if (widgetToLoad) hideLoading(widgetToLoad); } } // --- Data Display Functions --- function displayDataInTable(data) { if (!data || !data.columns || !Array.isArray(data.rows)) { tableHead.innerHTML = 'Could not load card data or data is malformed.
'; console.warn('Card data is insufficient or malformed:', data); return; } if (data.length === 0) { cardsDisplayArea.innerHTML = 'No card data available.
'; return; } cardsDisplayArea.innerHTML = ''; // Clear previous cards const accentColors = ['blue-accent', 'green-accent', 'red-accent', 'yellow-accent', 'purple-accent']; let colorIndex = 0; data.forEach(cardItem => { const cardDiv = document.createElement('div'); cardDiv.className = `data-card ${accentColors[colorIndex % accentColors.length]}`; colorIndex++; const titleH4 = document.createElement('h4'); if (cardItem.icon) { const iconSpan = document.createElement('span'); iconSpan.className = 'material-icons'; iconSpan.textContent = cardItem.icon; titleH4.appendChild(iconSpan); } titleH4.appendChild(document.createTextNode(cardItem.title || 'N/A')); cardDiv.appendChild(titleH4); const valueP = document.createElement('p'); valueP.className = 'card-value'; valueP.textContent = cardItem.value !== undefined ? cardItem.value : 'N/A'; cardDiv.appendChild(valueP); if (cardItem.description) { const descriptionP = document.createElement('p'); descriptionP.className = 'card-description'; descriptionP.textContent = cardItem.description; cardDiv.appendChild(descriptionP); } cardsDisplayArea.appendChild(cardDiv); }); } // --- Data Loading and Refresh Logic --- async function loadDataForSource(sourceKey) { switch (sourceKey) { case 'all': await loadAndDisplayAllData(); break; case 'csv_metrics': cardsWrapper.classList.remove('hidden'); tableWrapper.classList.add('hidden'); chartWrapper.classList.add('hidden'); const cardData = await fetchData('csv_metrics', {file: 'key_metrics.csv'}); if (cardData) displayDataAsCards(cardData); break; case 'db_sales': cardsWrapper.classList.add('hidden'); tableWrapper.classList.remove('hidden'); chartWrapper.classList.add('hidden'); const tableData = await fetchData('db_sales', {table: 'sales_overview'}); if (tableData) displayDataInTable(tableData); break; case 'api_revenue': cardsWrapper.classList.add('hidden'); tableWrapper.classList.add('hidden'); chartWrapper.classList.remove('hidden'); currentChartType = chartTypeSelect.value || 'bar'; const chartData = await fetchData('api_revenue', {endpoint: 'monthly_revenue'}); if (chartData) displayDataInChart(chartData, currentChartType); break; default: console.warn('Unknown data source key:', sourceKey); await loadAndDisplayAllData(); // Default to all } } async function loadAndDisplayAllData() { cardsWrapper.classList.remove('hidden'); tableWrapper.classList.remove('hidden'); chartWrapper.classList.remove('hidden'); // Load in parallel for better perceived performance const cardDataPromise = fetchData('csv_metrics', {file: 'key_metrics.csv'}); const tableDataPromise = fetchData('db_sales', {table: 'sales_overview'}); const chartDataPromise = fetchData('api_revenue', {endpoint: 'monthly_revenue'}); const [cardData, tableData, chartData] = await Promise.all([ cardDataPromise, tableDataPromise, chartDataPromise ]); if (cardData) displayDataAsCards(cardData); if (tableData) displayDataInTable(tableData); if (chartData) displayDataInChart(chartData, currentChartType); } // --- Event Listeners --- if (datasourceSelect) { datasourceSelect.addEventListener('change', (event) => { loadDataForSource(event.target.value); }); } if (chartTypeSelect) { chartTypeSelect.addEventListener('change', async (event) => { currentChartType = event.target.value; // Only reload chart data if the chart is currently visible or "all" is selected const selectedSource = datasourceSelect.value; if (selectedSource === 'all' || selectedSource === 'api_revenue') { console.log(`Chart type changed to: ${currentChartType}. Reloading chart data.`); const chartData = await fetchData('api_revenue', {endpoint: 'monthly_revenue'}); if (chartData) displayDataInChart(chartData, currentChartType); } }); } if (refreshButton) { refreshButton.addEventListener('click', () => { console.log('Manual refresh triggered.'); loadDataForSource(datasourceSelect.value || 'all'); }); } // --- Realtime Update Setup --- function startRealtimeUpdates() { if (REALTIME_UPDATE_INTERVAL > 0 && !realtimeIntervalId) { realtimeIntervalId = setInterval(() => { console.log('Performing realtime update...'); loadDataForSource(datasourceSelect.value || 'all'); // Refresh current view }, REALTIME_UPDATE_INTERVAL); console.log(`Realtime updates started with interval: ${REALTIME_UPDATE_INTERVAL}ms`); } } function stopRealtimeUpdates() { if (realtimeIntervalId) { clearInterval(realtimeIntervalId); realtimeIntervalId = null; console.log('Realtime updates stopped.'); } } // --- Initialization --- function init() { console.log('Initializing dashboard components...'); currentChartType = chartTypeSelect.value || 'bar'; // Ensure chart type is set from dropdown loadDataForSource(datasourceSelect.value || 'all'); // Load initial data based on selection if (REALTIME_UPDATE_INTERVAL > 0) { startRealtimeUpdates(); } // Optional: Stop updates if window/tab loses focus, restart on focus // document.addEventListener('visibilitychange', () => { // if (document.hidden) { stopRealtimeUpdates(); } else { startRealtimeUpdates(); } // }); } init(); // Start the dashboard });