script.js

13.08 KB
09/07/2025 07:56
JS
script.js
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
});