// 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 ---