// static/js/customers_runset_usage_tracking_dashboard.js.js document.addEventListener('DOMContentLoaded', () => { // --- IDs for controls and indicators --- const percentageThresholdSelect = document.getElementById('percentageThreshold'); const noDaysSelect = document.getElementById('noDays'); const groupedDataContainer = document.getElementById('grouped-data-container'); const loadingIndicator = document.getElementById('loadingIndicator'); const errorIndicator = document.getElementById('errorIndicator'); const refreshBtn = document.getElementById('refreshBtn'); const lastUpdatedDiv = document.getElementById('lastUpdated'); let autoRefreshIntervalId = null; const REFRESH_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes // --- Helper Functions --- function showLoading() { loadingIndicator.classList.remove('d-none'); errorIndicator.classList.add('d-none'); // Clear previous data and show loading message groupedDataContainer.innerHTML = '
Loading data...
'; } function showError(message = 'An error occurred while fetching data.') { loadingIndicator.classList.add('d-none'); errorIndicator.textContent = message; errorIndicator.classList.remove('d-none'); // Clear previous data and show error message groupedDataContainer.innerHTML = `
${message}
`; } function hideIndicators() { loadingIndicator.classList.add('d-none'); errorIndicator.classList.add('d-none'); } function updateLastUpdated() { const now = new Date(); lastUpdatedDiv.textContent = `Last updated: ${now.toLocaleTimeString()}`; } function formatBoolean(value) { if (value === true || value === 'true' || value === 1 || value === '1') { return 'Yes'; // Assumes .boolean-true is defined in CSS if used } else if (value === false || value === 'false' || value === 0 || value === '0' || value === null || value === undefined) { return 'No'; // Assumes .boolean-false is defined in CSS if used } return value; // Return original value if not a recognized boolean } // --- Core Data Fetching --- async function fetchData() { showLoading(); stopAutoRefresh(); // Stop existing timer before starting a new fetch const percentageThreshold = percentageThresholdSelect.value; const noDays = noDaysSelect.value; // Save current selections to session storage sessionStorage.setItem('selectedThreshold', percentageThreshold); sessionStorage.setItem('selectedDays', noDays); // Construct API URL const apiUrl = `/api/compare_logs?percentage_threshold=${percentageThreshold}&no_days=${noDays}`; try { // Fetch data from the API const response = await fetch(apiUrl); // Handle HTTP errors if (!response.ok) { let errorMsg = `Error: ${response.status} ${response.statusText}`; try { // Try to get more specific error from JSON response body const errorData = await response.json(); errorMsg = errorData.error || errorMsg; } catch (e) { // Ignore if the error response wasn't valid JSON } throw new Error(errorMsg); } // Parse the JSON response const data = await response.json(); // Populate the tables with the fetched data populateGroupedTables(data); // Update the "Last updated" timestamp updateLastUpdated(); } catch (error) { // Log the error and display an error message to the user console.error("Fetch error:", error); showError(error.message); } finally { // Hide loading/error indicators regardless of success or failure hideIndicators(); // Restart the auto-refresh timer startAutoRefresh(); } } // --- Populate Grouped and Collapsible Tables function --- function populateGroupedTables(data) { groupedDataContainer.innerHTML = ''; // Clear previous content // Handle cases where no data is returned if (!data || !Array.isArray(data) || data.length === 0) { groupedDataContainer.innerHTML = '
No data found for the selected criteria.
'; return; } // 1. Group data by Customer const groupedData = data.reduce((acc, row) => { const customer = row.Customer || 'Unknown Customer'; // Handle potential missing customer names if (!acc[customer]) { acc[customer] = []; } acc[customer].push(row); return acc; }, {}); // 2. Define column order/headers (WITHOUT the boolean 'exceeds' columns) const innerTableColumnOrder = [ 'runset_id', 'number_of_days', 'adjusted_average_audit_count', 'audit_count', 'adjusted_average_translation_tracking_count', 'translation_tracking_count', 'adjusted_average_out_transport_tracking_count', 'out_transport_tracking_count', 'adjusted_average_in_transport_tracking_count', 'in_transport_tracking_count' ]; const innerTableHeaders = { 'runset_id': 'Runset ID', 'number_of_days': 'Days Reported', 'adjusted_average_audit_count': 'Adj Avg Audit', 'audit_count': 'Audit Count', 'adjusted_average_translation_tracking_count': 'Adj Avg Trans Track', 'translation_tracking_count': 'Trans Track Count', 'adjusted_average_out_transport_tracking_count': 'Adj Avg Out Trans', 'out_transport_tracking_count': 'Out Trans Count', 'adjusted_average_in_transport_tracking_count': 'Adj Avg In Trans', 'in_transport_tracking_count': 'In Trans Count' }; // --- Map data columns to their corresponding 'exceeds' flag key in the original data --- const columnToExceedsFlagMap = { 'adjusted_average_audit_count': 'audit_count_exceeds_threshold', 'audit_count': 'audit_count_exceeds_threshold', 'adjusted_average_translation_tracking_count': 'translation_tracking_count_exceeds_threshold', 'translation_tracking_count': 'translation_tracking_count_exceeds_threshold', 'adjusted_average_out_transport_tracking_count': 'out_transport_tracking_count_exceeds_threshold', 'out_transport_tracking_count': 'out_transport_tracking_count_exceeds_threshold', 'adjusted_average_in_transport_tracking_count': 'in_transport_tracking_count_exceeds_threshold', 'in_transport_tracking_count': 'in_transport_tracking_count_exceeds_threshold' }; // 3. Create HTML for each customer group const sortedCustomers = Object.keys(groupedData).sort(); // Sort customer names alphabetically let customerIndex = 0; sortedCustomers.forEach(customerName => { const customerRows = groupedData[customerName]; const collapseId = `collapse-customer-${customerIndex}`; // Unique ID for the collapsible element // --- Create Heading (H3) with internal Collapse Button --- const heading = document.createElement('h3'); heading.className = 'mt-4 mb-0'; // Bootstrap margin classes const collapseButton = document.createElement('button'); collapseButton.className = 'btn btn-link text-start text-decoration-none fs-4 p-0 collapsible-header'; // Bootstrap button styling collapseButton.setAttribute('type', 'button'); collapseButton.setAttribute('data-bs-toggle', 'collapse'); collapseButton.setAttribute('data-bs-target', `#${collapseId}`); collapseButton.setAttribute('aria-expanded', 'true'); collapseButton.setAttribute('aria-controls', collapseId); const rowCount = customerRows.length; // Get number of rows for this customer collapseButton.textContent = `${customerName} (${rowCount})`; // Display customer name and row count heading.appendChild(collapseButton); groupedDataContainer.appendChild(heading); // --- Create the Collapsible Div Wrapper --- const collapseWrapper = document.createElement('div'); // Add 'show' class to start expanded, remove it to start collapsed collapseWrapper.className = 'collapse show'; collapseWrapper.id = collapseId; collapseWrapper.style.marginBottom = '1.5rem'; // Add space below each customer section // --- Create Table structure (inside the collapse wrapper) --- const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-responsive pt-2'; // Make table scroll horizontally on small screens const table = document.createElement('table'); table.className = 'table table-striped table-bordered table-hover table-sm'; // Bootstrap table styling // --- Create Table Header (thead) --- const thead = table.createTHead(); thead.className = 'table-dark'; // Dark header background const headerRow = thead.insertRow(); innerTableColumnOrder.forEach(key => { const th = document.createElement('th'); th.textContent = innerTableHeaders[key] || key; // Use defined header text or the key itself headerRow.appendChild(th); }); // --- Create Table Body (tbody) --- const tbody = table.createTBody(); customerRows.forEach(row => { // Iterate through each data row for the current customer const tr = tbody.insertRow(); innerTableColumnOrder.forEach(key => { // Iterate through the columns defined for display const td = tr.insertCell(); // Get value, display '-' for null/undefined let value = (row[key] !== null && row[key] !== undefined) ? row[key] : '-'; td.textContent = value; // Set the cell's display text // --- Apply Coloring based on the 'exceeds' flag --- const exceedsFlagKey = columnToExceedsFlagMap[key]; // Find the corresponding flag key if (exceedsFlagKey) { // Get the flag's value from the *original data row object* const exceedsValue = row[exceedsFlagKey]; // Determine class based on flag value (handles true, 'true', 1, '1' as danger) if (exceedsValue === true || String(exceedsValue).toLowerCase() === 'true' || Number(exceedsValue) === 1) { td.classList.add('text-danger'); // Apply Bootstrap red text class td.classList.add('fw-bold'); // Apply Bootstrap bold text class } else { // Treats false, 'false', 0, '0', null, undefined as success td.classList.add('text-success'); // Apply Bootstrap green text class } } }); }); // --- End Tbody Creation --- // Append thead and tbody to the table table.appendChild(thead); table.appendChild(tbody); // Append table to its wrapper, wrapper to collapse div, collapse div to main container tableWrapper.appendChild(table); collapseWrapper.appendChild(tableWrapper); groupedDataContainer.appendChild(collapseWrapper); customerIndex++; // Increment index for unique IDs }); } // --- END of populateGroupedTables function --- // --- Auto Refresh Logic --- function startAutoRefresh() { stopAutoRefresh(); // Ensure no multiple intervals run autoRefreshIntervalId = setInterval(fetchData, REFRESH_INTERVAL_MS); // Log when refresh starts for debugging console.log(`Grouped View: Auto-refresh started. Interval: ${REFRESH_INTERVAL_MS / 1000} seconds.`); } function stopAutoRefresh() { if (autoRefreshIntervalId) { clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; // Log when refresh stops for debugging console.log("Grouped View: Auto-refresh stopped."); } } // --- Event Listeners --- percentageThresholdSelect.addEventListener('change', fetchData); // Fetch on threshold change noDaysSelect.addEventListener('change', fetchData); // Fetch on days change refreshBtn.addEventListener('click', fetchData); // Fetch on button click // --- Initial Load --- // Define default values (adjust as needed) const defaultThreshold = '0.5'; const defaultDays = '30'; // Retrieve saved values from session storage, or use defaults const savedThreshold = sessionStorage.getItem('selectedThreshold'); const savedDays = sessionStorage.getItem('selectedDays'); // Set dropdown values based on saved/default percentageThresholdSelect.value = savedThreshold || defaultThreshold; noDaysSelect.value = savedDays || defaultDays; fetchData(); // Perform the initial data fetch on page load // --- Add current year to footer --- const yearSpan = document.querySelector('footer .text-muted'); // Target the span in the footer const currentYear = new Date().getFullYear(); if (yearSpan && !yearSpan.textContent.includes(currentYear)) { // Replace placeholder like {{ year }} or just append if no placeholder found const placeholderRegex = /\{\{\s*year\s*\}\}/; if (placeholderRegex.test(yearSpan.textContent)) { yearSpan.textContent = yearSpan.textContent.replace(placeholderRegex, currentYear); } else { // Fallback if no placeholder - adjust if your footer structure is different yearSpan.textContent += ` ${currentYear}`; } } }); // --- END of DOMContentLoaded listener ---