const widgetChartInstances = {}; // Store chart instances to prevent duplicates
document.addEventListener('DOMContentLoaded', () => {
loadDashboardLayout();
});
async function loadDashboardLayout() {
try {
const response = await fetch('configs/dashboard.json');
const layoutConfig = await response.json();
document.getElementById('dashboard-title').textContent = layoutConfig.title;
const container = document.getElementById('dashboard-container');
container.innerHTML = ''; // Clear previous content
for (const item of layoutConfig.layout) {
const widgetWrapper = document.createElement('div');
widgetWrapper.id = `widget-wrapper-${item.widgetId}`;
widgetWrapper.className = 'widget';
widgetWrapper.style.gridColumnEnd = `span ${item.width}`;
widgetWrapper.innerHTML = `
<h3 class="widget-title" id="title-${item.widgetId}"></h3>
<div class="widget-content" id="content-${item.widgetId}">
<div class="loading">Loading...</div>
</div>
`;
container.appendChild(widgetWrapper);
loadWidget(item.widgetId);
}
} catch (error) {
document.getElementById('dashboard-container').innerHTML = `<div class="error">Failed to load dashboard layout: ${error.message}</div>`;
console.error('Layout loading error:', error);
}
}
async function loadWidget(widgetId) {
try {
const configResponse = await fetch(`configs/widgets/${widgetId}.json`);
const config = await configResponse.json();
document.getElementById(`title-${config.id}`).textContent = config.title;
await fetchAndRender(config);
if (config.refreshInterval > 0) {
setInterval(() => {
console.log(`Refreshing ${widgetId}...`);
fetchAndRender(config);
}, config.refreshInterval);
}
} catch (error) {
const contentDiv = document.getElementById(`content-${widgetId}`);
contentDiv.innerHTML = `<div class="error">Failed to load widget config: ${error.message}</div>`;
console.error(`Error loading widget config ${widgetId}:`, error);
}
}
async function fetchAndRender(config, page = 1) {
const contentDiv = document.getElementById(`content-${config.id}`);
const paginationEnabled = config.displayOptions.pagination?.enabled;
let url = `api/data_source.php?widget_id=${config.id}`;
if (paginationEnabled) {
url += `&page=${page}&limit=${config.displayOptions.pagination.limit}`;
}
try {
const dataResponse = await fetch(url);
if (!dataResponse.ok) {
const errorData = await dataResponse.json();
throw new Error(errorData.error || `HTTP error! status: ${dataResponse.status}`);
}
const result = await dataResponse.json();
if (result.error) {
throw new Error(result.error);
}
switch (config.type) {
case 'table':
renderTable(contentDiv, config, result);
break;
case 'chart':
renderChart(contentDiv, config.displayOptions, result.data);
break;
case 'card':
renderCard(contentDiv, config.displayOptions, result.data);
break;
}
} catch (error) {
contentDiv.innerHTML = `<div class="error">Failed to load data: ${error.message}</div>`;
console.error(`Error fetching/rendering widget ${config.id}:`, error);
}
}
function renderTable(element, config, result) {
const {displayOptions} = config;
const {data, pagination} = result;
if (!data || data.length === 0) {
element.innerHTML = '<div class="loading">No data available.</div>';
return;
}
let tableHtml = '<div class="table-wrapper"><table><thead><tr>';
displayOptions.columns.forEach(col => {
if (col.visible) tableHtml += `<th>${col.displayName}</th>`;
});
tableHtml += '</tr></thead><tbody>';
data.forEach(rowData => {
let rowClass = '';
if (displayOptions.conditionalFormatting) {
displayOptions.conditionalFormatting.forEach(rule => {
if (rowData[rule.column] == rule.value) {
rowClass = rule.class;
}
});
}
tableHtml += `<tr class="${rowClass}">`;
displayOptions.columns.forEach(col => {
if (col.visible) tableHtml += `<td>${rowData[col.dataKey] ?? ''}</td>`;
});
tableHtml += '</tr>';
});
tableHtml += '</tbody></table></div>';
element.innerHTML = tableHtml;
if (pagination) {
renderPaginationControls(element, config, pagination);
}
}
function renderPaginationControls(element, config, pagination) {
const {currentPage, totalPages} = pagination;
const controlsContainer = document.createElement('div');
controlsContainer.className = 'pagination-controls';
const prevButton = document.createElement('button');
prevButton.innerHTML = '« Previous';
prevButton.disabled = currentPage <= 1;
prevButton.addEventListener('click', () => fetchAndRender(config, currentPage - 1));
const pageInfo = document.createElement('span');
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
const nextButton = document.createElement('button');
nextButton.innerHTML = 'Next »';
nextButton.disabled = currentPage >= totalPages;
nextButton.addEventListener('click', () => fetchAndRender(config, currentPage + 1));
controlsContainer.appendChild(prevButton);
controlsContainer.appendChild(pageInfo);
controlsContainer.appendChild(nextButton);
element.appendChild(controlsContainer);
}
function renderChart(element, options, data) {
const canvasId = `chart-${element.id}`;
element.innerHTML = `<canvas id="${canvasId}"></canvas>`;
const canvas = document.getElementById(canvasId);
if (widgetChartInstances[canvasId]) {
widgetChartInstances[canvasId].destroy();
}
const labels = data.map(row => row[options.labelsColumn]);
const datasets = options.datasets.map(col => ({
label: col.label,
data: data.map(row => row[col.dataKey]),
backgroundColor: col.backgroundColor,
borderColor: col.borderColor,
borderWidth: col.borderWidth || 1,
}));
widgetChartInstances[canvasId] = new Chart(canvas, {
type: options.chartType,
data: {labels, datasets},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {y: {beginAtZero: true}}
}
});
}
function renderCard(element, options, data) {
if (!data || data.length === 0) {
element.innerHTML = '<div class="loading">No data.</div>';
return;
}
const value = data[0][options.valueKey];
const formattedValue = new Intl.NumberFormat('en-US').format(value);
element.innerHTML = `
<div class="card-content">
<div class="card-value">${formattedValue}</div>
<div class="card-label">${options.label}</div>
</div>
`;
}