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 = '<div class="loader"></div>';
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 = '<tr><th>Error</th></tr>';
tableBody.innerHTML = '<tr><td>Could not load table data or data is malformed.</td></tr>';
console.warn('Table data is insufficient or malformed:', data);
return;
}
if (data.rows.length === 0) {
tableHead.innerHTML = `<tr>${data.columns.map(col => `<th>${col}</th>`).join('')}</tr>`;
tableBody.innerHTML = `<tr><td colspan="${data.columns.length}">No data available.</td></tr>`;
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 = '<p>Could not load card data or data is malformed.</p>';
console.warn('Card data is insufficient or malformed:', data);
return;
}
if (data.length === 0) {
cardsDisplayArea.innerHTML = '<p>No card data available.</p>';
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
});