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 = 'Error'; tableBody.innerHTML = 'Could not load table data or data is malformed.'; console.warn('Table data is insufficient or malformed:', data); return; } if (data.rows.length === 0) { tableHead.innerHTML = `${data.columns.map(col => `${col}`).join('')}`; tableBody.innerHTML = `No data available.`; return; } tableHead.innerHTML = ''; // Clear previous headers tableBody.innerHTML = ''; // Clear previous body const headerRow = document.createElement('tr'); data.columns.forEach(columnName => { const th = document.createElement('th'); th.textContent = columnName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); // Format column name headerRow.appendChild(th); }); tableHead.appendChild(headerRow); data.rows.forEach(rowData => { const row = document.createElement('tr'); data.columns.forEach(columnName => { const td = document.createElement('td'); td.textContent = rowData[columnName] !== undefined && rowData[columnName] !== null ? rowData[columnName] : 'N/A'; row.appendChild(td); }); tableBody.appendChild(row); }); } function displayDataInChart(data, chartType = 'bar') { if (!chartCanvas) { console.error("Chart canvas element not found!"); return; } const ctx = chartCanvas.getContext('2d'); if (!data || !data.labels || !data.datasets || data.datasets.length === 0) { console.warn('Chart data is insufficient or malformed for ' + chartType + ':', data); if (currentChart) { currentChart.destroy(); currentChart = null; } // Optionally display a message on the canvas ctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height); ctx.textAlign = 'center'; ctx.fillText('No chart data available or data is malformed.', chartCanvas.width / 2, chartCanvas.height / 2); return; } if (currentChart) { currentChart.destroy(); // Destroy previous chart instance } currentChart = new Chart(ctx, { type: chartType, data: { labels: data.labels, datasets: data.datasets.map(dataset => ({ ...dataset, // Provide default styling if not present in data backgroundColor: dataset.backgroundColor || 'rgba(0, 123, 255, 0.5)', borderColor: dataset.borderColor || 'rgba(0, 123, 255, 1)', borderWidth: dataset.borderWidth || 1 })) }, options: { responsive: true, maintainAspectRatio: false, scales: (chartType !== 'pie' && chartType !== 'doughnut' && chartType !== 'polarArea' && chartType !== 'radar') ? { y: {beginAtZero: true}, x: {} } : {}, plugins: { legend: { display: data.datasets.length > 1 || chartType === 'pie' || chartType === 'doughnut', position: 'top', }, tooltip: { enabled: true, mode: 'index', intersect: false, }, title: { // Example: Adding a title to the chart display: true, text: data.chartTitle || 'Data Chart' } } } }); console.log('Chart displayed:', chartType, data); } function displayDataAsCards(data) { if (!data || !Array.isArray(data)) { cardsDisplayArea.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 });