document.addEventListener('DOMContentLoaded', function() { // console.log('DOM fully loaded and parsed.'); const companyFilterDropdown = document.getElementById('companyFilterDropdown'); const companySearchInput = document.getElementById('companySearchInput'); const clearFiltersBtn = document.getElementById('clearFiltersBtn'); const auditLogTableContainer = document.getElementById('auditLogTableContainer'); const rowCountSpan = document.getElementById('rowCount'); // Elements from the initial flat table const initialTableWrapper = document.getElementById('initialTableWrapper'); const originalTheadElement = document.getElementById('originalThead'); const initialTableBody = document.getElementById('auditLogTableBody'); let companyGroupElements = []; // --- MODAL RELATED VARIABLES --- const rowDataModalElement = document.getElementById('rowDataModal'); const rowDataModalBody = document.getElementById('rowDataModalBody'); let rowDataModalInstance = null; let groupTableColumnHeaders = []; // Will store headers for grouped tables (and CSV/Modal) if (rowDataModalElement) { // console.log('Row Data Modal HTML element found:', rowDataModalElement); if (typeof bootstrap === 'undefined' || typeof bootstrap.Modal === 'undefined') { console.error('Bootstrap or Bootstrap.Modal is not defined. Modal functionality will not work.'); } else { rowDataModalInstance = new bootstrap.Modal(rowDataModalElement); // console.log('Bootstrap Modal instance for row data created:', rowDataModalInstance); } } else { console.error('Row Data Modal HTML element (id="rowDataModal") NOT FOUND.'); } // --- END MODAL RELATED VARIABLES --- // --- HELPER FUNCTIONS FOR CSV EXPORT --- /** * Converts an HTML string to its plain text representation. * @param {string} htmlString The HTML string to convert. * @returns {string} The plain text content. */ function htmlToPlainText(htmlString) { if (!htmlString) return ""; const tempDiv = document.createElement('div'); // Browsers automatically decode HTML entities when setting innerHTML tempDiv.innerHTML = htmlString; return tempDiv.textContent || tempDiv.innerText || ""; } /** * Escapes a string for use in a CSV cell. * If the string contains commas, newlines, or double quotes, * it will be enclosed in double quotes, and existing double quotes * within the string will be doubled. * @param {string|number|null|undefined} cellData The data for the cell. * @returns {string} The CSV-escaped string. */ function escapeCsvCell(cellData) { if (cellData === null || typeof cellData === 'undefined') { return ''; } let stringValue = String(cellData); // If the string contains a comma, newline, or double quote, enclose it in double quotes. // Also, escape existing double quotes by doubling them. if (stringValue.search(/("|,|\n)/g) >= 0) { stringValue = stringValue.replace(/"/g, '""'); // Escape double quotes stringValue = `"${stringValue}"`; // Enclose in double quotes } return stringValue; } /** * Generates a CSV string from headers and data rows, then triggers a download. * @param {string[]} headers Array of header strings. * @param {Array>} dataRows Array of data rows, where each row is an array of cell strings. * @param {string} filename The desired filename for the downloaded CSV file. */ function generateAndDownloadCsv(headers, dataRows, filename) { // console.log('generateAndDownloadCsv called with filename:', filename); // console.log('CSV Headers:', headers); // console.log('CSV Data Rows sample:', dataRows.slice(0, 2)); const csvRows = []; // Header row csvRows.push(headers.map(header => escapeCsvCell(header)).join(',')); // Data rows dataRows.forEach(rowArray => { csvRows.push(rowArray.map(cell => escapeCsvCell(cell)).join(',')); }); const csvString = csvRows.join('\n'); const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); if (link.download !== undefined) { // Check if HTML5 download attribute is supported const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); // console.log(`CSV file '${filename}' download initiated.`); } else { // Fallback for older browsers console.warn("CSV download is not fully supported in this browser. Fallback or alert user."); alert("CSV download is not fully supported in this browser. Please try a modern browser or copy the data manually."); } } // --- END HELPER FUNCTIONS FOR CSV EXPORT --- // --- CSV EXPORT HANDLER --- /** * Handles the click event for an "Export CSV" button. * Gathers data for the specific company group and triggers CSV generation and download. * @param {Event} event The click event object. */ function handleExportCsvClick(event) { // console.log('handleExportCsvClick triggered for button:', event.currentTarget); const button = event.currentTarget; const companyName = button.dataset.companyNameForExport; if (!companyName) { console.error("Export button clicked, but no company name found in 'data-company-name-for-export' attribute."); alert("Error: Could not determine company for export."); return; } // console.log(`Exporting CSV for company: ${companyName}`); const groupDiv = button.closest('.company-group'); if (!groupDiv) { console.error(`Could not find parent '.company-group' div for export button related to company: ${companyName}`); alert(`Error: Could not find data group for ${companyName}.`); return; } const tableElement = groupDiv.querySelector('table'); if (!tableElement) { console.error(`Could not find table within groupDiv for company: ${companyName}`); alert(`Error: Could not find table data for ${companyName}.`); return; } // Headers are from groupTableColumnHeaders (which already excludes "Company Name") const headersForCsv = [...groupTableColumnHeaders, "Runset Disabled"]; // Use a copy // console.log('Using these headers for CSV:', headersForCsv); // Find the index of "Runset Notes" to handle it specially for full, plain-text content. const runsetNotesHeaderPattern = /runset\s*notes/i; // Case-insensitive, allows for "Runset Notes", "Runset Notes", etc. const notesColumnIndex = headersForCsv.findIndex(header => runsetNotesHeaderPattern.test(header)); if (notesColumnIndex === -1) { console.warn(`"Runset Notes" column (matching /runset\\s*notes/i) not found in CSV headers for company '${companyName}'. Notes will be exported as displayed in table cell text if no special handling is applied.`); } else { // console.log(`"Runset Notes" column found at index ${notesColumnIndex} for CSV export of ${companyName}.`); } const dataRowsForCsv = []; const tableRows = tableElement.querySelectorAll('tbody tr'); // console.log(`Found ${tableRows.length} data rows in the table for ${companyName}.`); tableRows.forEach((tr, rowIndex) => { const cells = tr.querySelectorAll('td'); const rowData = []; cells.forEach((cell, cellIndex) => { if (cellIndex === notesColumnIndex && cell.classList.contains('js-runset-notes-data-cell')) { // console.log(`Row ${rowIndex}, Cell ${cellIndex} (Runset Notes): Extracting full notes.`); const fullNotesHtml = cell.dataset.fullNotes || ''; // Get from data-full-notes rowData.push(htmlToPlainText(fullNotesHtml)); // Convert to plain text } else { rowData.push(cell.textContent.trim()); // Standard text content for other cells } }); rowData.push((tr.dataset.runsetDisabled && tr.dataset.runsetDisabled.toLowerCase() === 'true') ? 'True' : 'False'); dataRowsForCsv.push(rowData); }); if (dataRowsForCsv.length === 0) { console.warn(`No data rows extracted to export for ${companyName}.`); alert(`No data to export for ${companyName}.`); return; } // Sanitize company name for filename and add timestamp const safeCompanyName = companyName.replace(/[^a-z0-9_.-]/gi, '_').toLowerCase(); const timestamp = new Date().toISOString().split('T')[0]; // Format: YYYY-MM-DD const filename = `${safeCompanyName}_audit_log_${timestamp}.csv`; // console.log(`Generated filename for CSV: ${filename}`); generateAndDownloadCsv(headersForCsv, dataRowsForCsv, filename); } // --- END CSV EXPORT HANDLER --- function setupGroupedView() { // console.log('setupGroupedView called.'); const initialRows = initialTableBody ? Array.from(initialTableBody.querySelectorAll('tr')) : []; if (!initialRows.length || !originalTheadElement || !auditLogTableContainer) { // console.warn('Initial data or containers missing for setupGroupedView.'); if (rowCountSpan) rowCountSpan.textContent = '0'; return; } const groupTableTheadTemplate = originalTheadElement.cloneNode(true); groupTableTheadTemplate.removeAttribute('id'); const firstTh = groupTableTheadTemplate.querySelector('tr > th:first-child'); if (firstTh && firstTh.innerHTML.toLowerCase().includes("company")) { // console.log("Removing 'Company Name' header from group table template."); firstTh.remove(); } else { console.warn("Could not find or verify the 'Company Name' header column to remove from group table header template."); } // --- CAPTURE HEADERS FOR MODAL & CSV --- groupTableColumnHeaders = Array.from(groupTableTheadTemplate.querySelectorAll('th')).map(th => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = th.innerHTML; // Use innerHTML to correctly interpret
// Replace sequences of whitespace (including newlines from
) with a single space, then trim. return (tempDiv.textContent || tempDiv.innerText || "").replace(/\s+/g, ' ').trim(); }); // console.log('Group Table Column Headers (for Modal & CSV):', groupTableColumnHeaders); const groupedByCompany = initialRows.reduce((acc, row) => { const companyName = row.getAttribute('data-company-name'); const disabledStatus = row.getAttribute('data-runset-disabled'); const isDisabled = disabledStatus && String(disabledStatus).toLowerCase() === 'true'; if (isDisabled) { row.style.color = '#adb5bd'; row.style.opacity = '0.5'; row.style.fontStyle = 'italic'; row.title = "This runset is disabled"; } if (!companyName) { // console.warn("Row found without data-company-name attribute:", row); return acc; } if (!acc[companyName]) { acc[companyName] = []; } const companyCell = row.querySelector('td:first-child'); if (companyCell) { // console.log(`Removing company cell for company '${companyName}' before adding to group.`); companyCell.remove(); } if (!row.classList.contains('clickable-row')) { row.classList.add('clickable-row'); } acc[companyName].push(row); return acc; }, {}); if (initialTableWrapper) { // console.log('Removing initial flat table wrapper.'); initialTableWrapper.remove(); } const noLogsMessageDiv = document.getElementById('noLogsMessage'); if (noLogsMessageDiv && auditLogTableContainer.contains(noLogsMessageDiv)) { // console.log('Removing "no logs" message before adding grouped view.'); noLogsMessageDiv.remove(); } const sortedCompanyNames = Object.keys(groupedByCompany).sort(); let groupIndex = 0; // console.log('Sorted company names for grouping:', sortedCompanyNames); if (sortedCompanyNames.length === 0 && initialRows.length > 0) { auditLogTableContainer.innerHTML = '
Could not group logs by company.
'; if (rowCountSpan) rowCountSpan.textContent = '0'; // console.warn('Could not group logs by company, though initial rows existed.'); return; } else if (sortedCompanyNames.length === 0) { auditLogTableContainer.innerHTML = '
No audit log entries to display after attempting grouping.
'; if (rowCountSpan) rowCountSpan.textContent = '0'; // console.log('No audit log entries to display after attempting grouping.'); return; } sortedCompanyNames.forEach(companyName => { const companyRows = groupedByCompany[companyName]; const logCount = companyRows.length; const collapseId = `collapse-company-${groupIndex}`; // console.log(`Creating group for: ${companyName}, Logs: ${logCount}, Collapse ID: ${collapseId}`); const groupDiv = document.createElement('div'); groupDiv.className = 'company-group mb-3'; groupDiv.setAttribute('data-company-name', companyName); // --- Export Button --- const heading = document.createElement('h3'); // Use flexbox to align collapse trigger and export button heading.className = 'mt-3 mb-0 d-flex justify-content-between align-items-center'; // Container for the collapse button (company name and count) const collapseButtonContainer = document.createElement('div'); const collapseButton = document.createElement('button'); collapseButton.className = 'btn btn-link text-start text-decoration-none fs-4 p-0 collapsible-header'; collapseButton.setAttribute('type', 'button'); collapseButton.setAttribute('data-bs-toggle', 'collapse'); collapseButton.setAttribute('data-bs-target', `#${collapseId}`); collapseButton.setAttribute('aria-expanded', 'false'); // Start all collapsed collapseButton.setAttribute('aria-controls', collapseId); collapseButton.innerHTML = `${companyName} (${logCount} Results)`; collapseButtonContainer.appendChild(collapseButton); // Create the Export CSV button const exportButton = document.createElement('button'); exportButton.className = 'btn btn-sm btn-outline-primary export-csv-btn ms-2'; // Added ms-2 for margin exportButton.setAttribute('type', 'button'); exportButton.setAttribute('data-company-name-for-export', companyName); // Store company name for the handler exportButton.innerHTML = ` Export CSV`; exportButton.addEventListener('click', handleExportCsvClick); // Attach event listener // console.log(`Added export CSV button for ${companyName}`); heading.appendChild(collapseButtonContainer); // Add collapse button container to heading heading.appendChild(exportButton); // Add export button to heading groupDiv.appendChild(heading); const collapseWrapper = document.createElement('div'); collapseWrapper.className = 'collapse'; // Start all collapsed collapseWrapper.id = collapseId; const tableResponsiveDiv = document.createElement('div'); tableResponsiveDiv.className = 'table-responsive pt-1'; const table = document.createElement('table'); table.className = 'table table-striped table-hover table-sm'; const newTbody = table.createTBody(); companyRows.forEach(row => { newTbody.appendChild(row); }); table.appendChild(groupTableTheadTemplate.cloneNode(true)); table.appendChild(newTbody); tableResponsiveDiv.appendChild(table); collapseWrapper.appendChild(tableResponsiveDiv); groupDiv.appendChild(collapseWrapper); auditLogTableContainer.appendChild(groupDiv); companyGroupElements.push(groupDiv); groupIndex++; }); addEventListenersToTableRows(); // For modal // console.log('setupGroupedView completed.'); } function filterTable() { // console.log('filterTable called.'); const selectedCompany = companyFilterDropdown.value; const searchTerm = companySearchInput.value.toLowerCase().trim(); // console.log(`Filtering with selectedCompany: '${selectedCompany}', searchTerm: '${searchTerm}'`); let visibleLogCount = 0; if (companyGroupElements.length > 0) { // console.log('Filtering grouped view.'); companyGroupElements.forEach(groupDiv => { const groupCompanyName = groupDiv.getAttribute('data-company-name').toLowerCase(); const countSpan = groupDiv.querySelector('.company-log-count'); const logCountInGroup = countSpan ? parseInt(countSpan.textContent) : 0; const collapseWrapper = groupDiv.querySelector('.collapse'); const collapseButton = groupDiv.querySelector('[data-bs-toggle="collapse"]'); // Determine if the group should be visible based on filters let showGroup = true; if (selectedCompany !== 'all' && groupCompanyName !== selectedCompany.toLowerCase()) { showGroup = false; } if (showGroup && searchTerm !== '' && !groupCompanyName.includes(searchTerm)) { showGroup = false; } groupDiv.style.display = showGroup ? '' : 'none'; // console.log(`Group '${groupCompanyName}': showGroup = ${showGroup}`); if (showGroup) { visibleLogCount += logCountInGroup; // --- EXPAND/COLLAPSE LOGIC --- if (collapseWrapper && collapseButton) { let shouldExpandGroup = false; const bsCollapse = bootstrap.Collapse.getInstance(collapseWrapper) || new bootstrap.Collapse(collapseWrapper, { toggle: false }); // Case 1: Dropdown selects this specific company (and it's visible) if (selectedCompany !== 'all' && groupCompanyName === selectedCompany.toLowerCase()) { shouldExpandGroup = true; // console.log(`Group '${groupCompanyName}': Expanding due to specific company selection.`); } // Case 2: Dropdown is "all" AND search term is active (and this group is visible, meaning it matched the search) else if (selectedCompany === 'all' && searchTerm !== '') { shouldExpandGroup = true; // `showGroup` being true here means it matched the searchTerm // console.log(`Group '${groupCompanyName}': Expanding due to search term match (dropdown is 'all').`); } if (shouldExpandGroup) { if (!collapseWrapper.classList.contains('show')) { // console.log(`Group '${groupCompanyName}': Programmatically showing collapse.`); bsCollapse.show(); } collapseButton.setAttribute('aria-expanded', 'true'); } else { // If not explicitly told to expand, and it is shown, collapse it. // This ensures groups collapse if the filter no longer targets them for expansion. if (collapseWrapper.classList.contains('show')) { // console.log(`Group '${groupCompanyName}': Programmatically hiding collapse as it's not targeted for expansion.`); bsCollapse.hide(); } collapseButton.setAttribute('aria-expanded', 'false'); } } // --- MODIFICATION END --- } else { // If the group is hidden by filters, ensure it's also collapsed if (collapseWrapper && collapseButton && collapseWrapper.classList.contains('show')) { // console.log(`Group '${groupCompanyName}': Hidden by filter, ensuring it's collapsed.`); const bsCollapse = bootstrap.Collapse.getInstance(collapseWrapper) || new bootstrap.Collapse(collapseWrapper, { toggle: false }); bsCollapse.hide(); collapseButton.setAttribute('aria-expanded', 'false'); } } }); } else if (initialTableBody) { // console.warn('Filtering on initial flat table (fallback).'); const fallbackRows = Array.from(initialTableBody.querySelectorAll('tr')); fallbackRows.forEach(row => { const companyNameCell = row.querySelector('td:first-child'); const companyName = companyNameCell ? companyNameCell.textContent.toLowerCase() : ""; let showRow = true; if (selectedCompany !== 'all' && companyName !== selectedCompany.toLowerCase()) { showRow = false; } if (showRow && searchTerm !== '' && !companyName.includes(searchTerm)) { showRow = false; } row.style.display = showRow ? '' : 'none'; if (showRow) visibleLogCount++; }); } if (rowCountSpan) { rowCountSpan.textContent = visibleLogCount; // console.log(`Row count updated to: ${visibleLogCount}`); } // console.log('filterTable finished.'); } function clearFilters() { // console.log('clearFilters called.'); if (companyFilterDropdown) companyFilterDropdown.value = 'all'; if (companySearchInput) companySearchInput.value = ''; filterTable(); // This will re-evaluate and collapse groups not matching "all"/empty search // Additionally, explicitly collapse all groups when filters are cleared, // unless the user wants previously opened groups to remain open. // The current filterTable logic should handle collapsing correctly. // If a more aggressive "collapse all" is needed: companyGroupElements.forEach(groupDiv => { const collapseWrapper = groupDiv.querySelector('.collapse'); const collapseButton = groupDiv.querySelector('[data-bs-toggle="collapse"]'); if (collapseWrapper && collapseButton && collapseWrapper.classList.contains('show')) { // console.log(`Clearing filters: Collapsing group '${groupDiv.dataset.companyName}'.`); const bsCollapse = bootstrap.Collapse.getInstance(collapseWrapper) || new bootstrap.Collapse(collapseWrapper, { toggle: false }); bsCollapse.hide(); collapseButton.setAttribute('aria-expanded', 'false'); } }); // console.log('clearFilters finished.'); } // --- MODAL DISPLAY LOGIC --- function showFullDetailsModal(event) { const clickedRow = event.target.closest('tr.clickable-row'); if (!clickedRow) { // console.log('Click was not on a clickable row or tr.clickable-row not found.'); return; } // console.log('showFullDetailsModal triggered for row:', clickedRow); if (!rowDataModalInstance) { console.error('rowDataModalInstance is null. Cannot show modal.'); alert('Error: Modal component is not properly initialized.'); return; } const cells = clickedRow.querySelectorAll('td'); let modalHtml = '
'; const runsetNotesHeaderPattern = /runset\s*notes/i; // Used to identify the notes column consistently groupTableColumnHeaders.forEach((header, index) => { if (cells[index]) { let valueHtml; const currentCell = cells[index]; // Check if this is the Runset Notes column using the cleaned header if (runsetNotesHeaderPattern.test(header) && currentCell.classList.contains('js-runset-notes-data-cell')) { const fullNotesFromAttribute = currentCell.dataset.fullNotes; if (typeof fullNotesFromAttribute !== 'undefined') { // For modal, we want to display the HTML as rendered HTML. // The attribute stores HTML-escaped content. Setting it to innerHTML of a (temporary or modal) // element will render it. valueHtml = fullNotesFromAttribute; // This should be the HTML escaped string // console.log('Full runset notes (HTML for modal):', valueHtml); } else { valueHtml = currentCell.innerHTML; // Fallback // console.warn('Runset notes cell missing data-full-notes attribute, using innerHTML for modal.'); } } else { valueHtml = currentCell.innerHTML; } modalHtml += `
${header}:
${valueHtml}
`; } else { // console.warn(`No cell found at index ${index} for header "${header}" in modal creation.`); } }); const disabledVal = clickedRow.getAttribute('data-runset-disabled'); const isActuallyDisabled = disabledVal && disabledVal.toLowerCase() === 'true'; const statusDisplay = isActuallyDisabled ? 'True' : 'False'; modalHtml += `
Runset Disabled:
${statusDisplay}
`; modalHtml += '
'; rowDataModalBody.innerHTML = modalHtml; rowDataModalInstance.show(); // console.log('rowDataModalInstance.show() called.'); } // --- ADD EVENT LISTENERS TO TABLE ROWS FOR MODAL --- function addEventListenersToTableRows() { const clickableRows = auditLogTableContainer.querySelectorAll('table tbody tr.clickable-row'); // console.log(`addEventListenersToTableRows: Found ${clickableRows.length} clickable rows to attach/reattach listeners.`); clickableRows.forEach(row => { row.removeEventListener('click', showFullDetailsModal); // Prevent multiple listeners row.addEventListener('click', showFullDetailsModal); }); if (clickableRows.length === 0 && companyGroupElements.length > 0) { // console.warn("addEventListenersToTableRows: No 'tr.clickable-row' elements found within grouped tables. Clicks for modal won't work on these rows."); } // console.log("Event listeners for modal added to table rows."); } // --- END MODAL LOGIC --- // --- Event Listeners for Filters --- if (companyFilterDropdown) { companyFilterDropdown.addEventListener('change', function() { // console.log('Company filter dropdown changed.'); filterTable(); }); } if (companySearchInput) { companySearchInput.addEventListener('input', ()=>{ // console.log('Company search input changed.'); filterTable(); }); } if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', clearFilters); } // console.log("Filter event listeners attached."); // --- Initial Setup --- // console.log('Running initial setup...'); if (auditLogTableContainer && initialTableBody && initialTableBody.querySelector('tr')) { // console.log('Initial logs found. Proceeding with setupGroupedView and initial filterTable.'); setupGroupedView(); filterTable(); } else if (rowCountSpan) { // console.log('No initial logs found or essential containers missing for full setup.'); rowCountSpan.textContent = '0'; if (!initialTableBody || !initialTableBody.querySelector('tr')) { // console.log('Initial table body is empty or missing rows.'); if (!document.getElementById('noLogsMessage') && auditLogTableContainer) { // console.log('Displaying "No audit log entries found." message.'); auditLogTableContainer.innerHTML = '
No audit log entries found.
'; } } } // console.log('Initial setup finished.'); });