// static/js/emails.js document.addEventListener('DOMContentLoaded', () => { // --- IDs for controls and indicators --- const daysAgoSelect = document.getElementById('daysAgoSelect'); const emailGroupsContainer = document.getElementById('email-groups-container'); const loadingIndicator = document.getElementById('loadingIndicator'); const errorIndicator = document.getElementById('errorIndicator'); const sendErrorIndicator = document.getElementById('sendErrorIndicator'); const sendSuccessIndicator = document.getElementById('sendSuccessIndicator'); const refreshBtn = document.getElementById('refreshBtn'); const lastUpdatedDiv = document.getElementById('lastUpdated'); // --- Modal Elements --- const emailModalElement = document.getElementById('emailModal'); const emailModal = new bootstrap.Modal(emailModalElement); const modalSubjectSpan = document.getElementById('modalSubject'); const modalBodyDiv = document.getElementById('modalBody'); const modalSendBtn = document.getElementById('modalSendBtn'); const modalCloseBtn = document.getElementById('modalCloseBtn'); const modalSendSpinner = document.getElementById('modalSendSpinner'); const modalDateSpan = document.getElementById('modalDate'); // --- Load More Configuration --- const CHUNK_SIZE = 50; // Number of rows to load per chunk let autoRefreshIntervalId = null; const REFRESH_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes let currentModalEmailKey = null; // --- Store fetched data globally within this scope --- let fullGroupedData = {}; // To store all rows grouped by source // --- Helper Functions --- // ... (showLoading, showError, showSendStatus, hideIndicators, updateLastUpdated, stripHtml, truncateText, formatTableRowDate remain the same) ... function showLoading() { loadingIndicator.classList.remove('d-none'); errorIndicator.classList.add('d-none'); sendErrorIndicator.classList.add('d-none'); sendSuccessIndicator.classList.add('d-none'); emailGroupsContainer.innerHTML = '
Loading emails...
'; refreshBtn.disabled = true; } function showError(message = 'An error occurred.') { loadingIndicator.classList.add('d-none'); errorIndicator.textContent = message; errorIndicator.classList.remove('d-none'); emailGroupsContainer.innerHTML = `
${message}
`; refreshBtn.disabled = false; } function showSendStatus(message, isSuccess = true) { sendErrorIndicator.classList.add('d-none'); sendSuccessIndicator.classList.add('d-none'); const indicator = isSuccess ? sendSuccessIndicator : sendErrorIndicator; indicator.textContent = message; indicator.classList.remove('d-none'); setTimeout(() => indicator.classList.add('d-none'), 5000); } function hideIndicators() { loadingIndicator.classList.add('d-none'); refreshBtn.disabled = false; } function updateLastUpdated() { const now = new Date(); lastUpdatedDiv.textContent = `Last updated: ${now.toLocaleTimeString()}`; } function stripHtml(htmlString) { if (!htmlString) return ""; try { const d = document.createElement('div'); d.innerHTML = htmlString; return d.textContent || d.innerText || ""; } catch (e) { console.error("Strip HTML error:", e); return htmlString; } } function truncateText(text, maxLength = 50) { if (!text || text.length <= maxLength) { return text || ''; } return text.substring(0, maxLength) + '...'; } function formatTableRowDate(dateString) { if (!dateString) return '-'; try { const d = new Date(dateString); if (!isNaN(d.getTime())) { return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); } else { return '(Invalid)'; } } catch (e) { return '(Error)'; } } // --- Core Data Fetching --- async function fetchData() { showLoading(); stopAutoRefresh(); const daysAgo = daysAgoSelect.value; sessionStorage.setItem('selectedDaysAgo', daysAgo); const apiUrl = `/api/emails?days_ago=${daysAgo}`; try { const response = await fetch(apiUrl); if (!response.ok) { let errorMsg = `Error: ${response.status} ${response.statusText}`; try { const errorData = await response.json(); errorMsg = errorData.error || errorMsg; } catch (e) { /* Ignore */ } throw new Error(errorMsg); } const data = await response.json(); // Get all data // Group the data and store it globally fullGroupedData = data.reduce((acc, row) => { const source = row.source || 'Unknown Source'; if (!acc[source]) { acc[source] = { rows: [], status: 'Ok' }; } acc[source].rows.push(row); if (row.flag === 'Error') { acc[source].status = 'Error'; } else if (row.flag === 'Warn' && acc[source].status !== 'Error') { acc[source].status = 'Warn'; } return acc; }, {}); // Render the initial view populateEmailGroups(fullGroupedData); updateLastUpdated(); errorIndicator.classList.add('d-none'); } catch (error) { console.error("Fetch error:", error); fullGroupedData = {}; // Clear data on error showError(error.message); } finally { hideIndicators(); startAutoRefresh(); } } // --- Refactored Row Creation Logic --- function createTableRow(row) { const tr = document.createElement('tr'); // Store data on TR tr.dataset.key = row.key; tr.dataset.subject = row.subject; tr.dataset.body = row.body; tr.dataset.date = row.date; tr.style.cursor = 'pointer'; // Define columns order for rendering cells const columnsToRender = ['date', 'subject', 'body']; columnsToRender.forEach(key => { const td = tr.insertCell(); let displayValue = row[key]; // Apply formatting if (key === 'body') { displayValue = truncateText(stripHtml(displayValue), 50); } else if (key === 'date') { displayValue = formatTableRowDate(displayValue); } td.textContent = (displayValue !== null && displayValue !== undefined) ? displayValue : '-'; // Apply traffic light styling to TD switch (row.flag) { case 'Error': td.classList.add('text-danger', 'fw-bold'); break; case 'Warn': td.classList.add('text-warning'); break; case 'Ok': td.classList.add('text-success'); break; } }); return tr; } // --- Populate Grouped Email Sections (Initial Render) --- function populateEmailGroups(groupedData) { // Takes the globally stored grouped data emailGroupsContainer.innerHTML = ''; // Clear previous content if (Object.keys(groupedData).length === 0) { // Handle case where fetch succeeded but returned no data or grouping failed if (!errorIndicator.classList.contains('d-none')) { // If an error is already shown, don't overwrite it } else if (loadingIndicator.classList.contains('d-none')) { // Only show "No emails found" if not currently loading and no error shown emailGroupsContainer.innerHTML = '
No emails found for the selected criteria.
'; } return; } const sortedSources = Object.keys(groupedData).sort(); const innerTableHeaders = { 'date': 'Date', 'subject': 'Subject', 'body': 'Body Preview' }; const innerTableColumns = ['date', 'subject', 'body']; // For header creation sortedSources.forEach((sourceName, index) => { const group = groupedData[sourceName]; const allSourceRows = group.rows; const groupStatus = group.status; const totalRows = allSourceRows.length; const collapseId = `collapse-source-${index}`; const tbodyId = `tbody-source-${index}`; // Unique ID for tbody const loadMoreContainerId = `loadmore-source-${index}`; // Unique ID for load more container const isInitiallyCollapsed = (groupStatus === 'Ok'); // --- Create Heading --- const heading = document.createElement('h3'); heading.className = 'mt-4 mb-0'; const collapseButton = document.createElement('button'); collapseButton.className = `btn btn-link text-start text-decoration-none fs-4 p-0 collapsible-header ${ groupStatus === 'Error' ? 'text-danger' : groupStatus === 'Warn' ? 'text-warning' : 'text-success' }`; // ... (set button attributes) ... collapseButton.setAttribute('type', 'button'); collapseButton.setAttribute('data-bs-toggle', 'collapse'); collapseButton.setAttribute('data-bs-target', `#${collapseId}`); collapseButton.setAttribute('aria-expanded', isInitiallyCollapsed ? 'false' : 'true'); collapseButton.setAttribute('aria-controls', collapseId); collapseButton.textContent = `${sourceName} (${totalRows})`; // Show total count heading.appendChild(collapseButton); emailGroupsContainer.appendChild(heading); // --- Create Collapsible Div --- const collapseWrapper = document.createElement('div'); collapseWrapper.className = `collapse ${isInitiallyCollapsed ? '' : 'show'}`; collapseWrapper.id = collapseId; collapseWrapper.style.marginBottom = '1rem'; // --- Create Table structure --- const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-responsive pt-2'; const table = document.createElement('table'); table.className = 'table table-striped table-bordered table-hover table-sm email-table'; const thead = table.createTHead(); thead.className = 'table-light'; const headerRow = thead.insertRow(); innerTableColumns.forEach(key => { const th = document.createElement('th'); th.textContent = innerTableHeaders[key] || key; headerRow.appendChild(th); }); const tbody = table.createTBody(); tbody.id = tbodyId; // Assign the unique ID // *** Render Initial Chunk *** const initialRowsToRender = allSourceRows.slice(0, CHUNK_SIZE); initialRowsToRender.forEach(row => { tbody.appendChild(createTableRow(row)); // Use helper function }); table.appendChild(thead); table.appendChild(tbody); tableWrapper.appendChild(table); collapseWrapper.appendChild(tableWrapper); // *** Add "Load More" Button if needed *** const loadMoreContainer = document.createElement('div'); loadMoreContainer.id = loadMoreContainerId; loadMoreContainer.className = 'text-center mt-2'; // Center the button if (totalRows > CHUNK_SIZE) { const loadMoreBtn = document.createElement('button'); loadMoreBtn.className = 'btn btn-outline-secondary btn-sm load-more-btn'; loadMoreBtn.textContent = `Load More (${CHUNK_SIZE} / ${totalRows})`; loadMoreBtn.dataset.tbodyTarget = tbodyId; // Link button to tbody loadMoreBtn.dataset.sourceName = sourceName; // Store source name loadMoreBtn.dataset.currentlyShown = CHUNK_SIZE.toString(); // Store current count as string loadMoreContainer.appendChild(loadMoreBtn); } // Append container (even if empty, helps keep structure consistent) collapseWrapper.appendChild(loadMoreContainer); emailGroupsContainer.appendChild(collapseWrapper); }); // End sortedSources.forEach // Add/Update Event Listener for Row Clicks AND Load More buttons emailGroupsContainer.removeEventListener('click', handleContainerClick); // Use one handler emailGroupsContainer.addEventListener('click', handleContainerClick); } // --- END of populateEmailGroups function --- // --- Combined Click Handler for Table Rows and Load More Buttons --- function handleContainerClick(event) { const target = event.target; // Check if a Load More button was clicked if (target.classList.contains('load-more-btn')) { handleLoadMoreClick(target); // Pass the button element } // Check if a table row (or its child) was clicked else { const tableRow = target.closest('tr'); if (tableRow && tableRow.closest('.email-table')) { // Check if it's within an email table handleTableRowClick(tableRow); // Pass the row element } } } // --- Handle Table Row Clicks (Takes TR element) --- function handleTableRowClick(tableRow) { // This function now just gets data from the passed row element const key = tableRow.dataset.key; const subject = tableRow.dataset.subject; const body = tableRow.dataset.body; const date = tableRow.dataset.date; if (key) { openEmailModal(key, subject, body, date); } else { console.warn("Clicked row is missing data attributes."); } } // --- Handle Load More Button Click (Takes Button element) --- function handleLoadMoreClick(button) { const tbodyId = button.dataset.tbodyTarget; const sourceName = button.dataset.sourceName; let currentlyShown = parseInt(button.dataset.currentlyShown || '0', 10); const tbody = document.getElementById(tbodyId); if (!tbody || !fullGroupedData[sourceName]) { console.error("Load More: Cannot find tbody or source data.", tbodyId, sourceName); button.textContent = "Error loading data"; button.disabled = true; return; } const allSourceRows = fullGroupedData[sourceName].rows; const totalRows = allSourceRows.length; const nextChunkStart = currentlyShown; const nextChunkEnd = Math.min(nextChunkStart + CHUNK_SIZE, totalRows); if (nextChunkStart >= totalRows) { console.warn("Load More: No more rows to load."); button.remove(); // Or disable and change text return; } // Disable button while loading button.disabled = true; button.textContent = "Loading..."; // Append the next chunk of rows const rowsToAppend = allSourceRows.slice(nextChunkStart, nextChunkEnd); rowsToAppend.forEach(row => { tbody.appendChild(createTableRow(row)); // Use helper function }); // Update button state currentlyShown = nextChunkEnd; button.dataset.currentlyShown = currentlyShown.toString(); if (currentlyShown >= totalRows) { button.remove(); // Remove button if all loaded } else { button.textContent = `Load More (${currentlyShown} / ${totalRows})`; button.disabled = false; // Re-enable button } } // --- Open and Populate Modal --- function openEmailModal(key, subject, body, dateString) { currentModalEmailKey = key; modalSubjectSpan.textContent = subject || '(No Subject)'; let formattedDate = '(No Date Provided)'; if (dateString) { try { const dateObj = new Date(dateString); if (!isNaN(dateObj.getTime())) { formattedDate = dateObj.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); } else { formattedDate = `(Invalid Date: ${dateString})`; } } catch (e) { formattedDate = `(Error Formatting Date: ${dateString})`; } } modalDateSpan.textContent = formattedDate; modalBodyDiv.innerHTML = body || '(Empty Body)'; modalSendBtn.disabled = false; modalSendSpinner.classList.add('d-none'); emailModal.show(); } // --- Handle Send Email Button Click --- async function handleSendEmail() { if (!currentModalEmailKey) { /* ... */ return; } modalSendBtn.disabled = true; modalSendSpinner.classList.remove('d-none'); // ... rest of send logic ... sendErrorIndicator.classList.add('d-none'); sendSuccessIndicator.classList.add('d-none'); const apiUrl = '/api/send_email'; const payload = { key: currentModalEmailKey }; try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || `HTTP error ${response.status}`); } showSendStatus(result.message || "Email marked 'Pending'.", true); emailModal.hide(); fetchData(); // Refresh to reflect potential status change visibility if filtering by status } catch (error) { console.error("Send email error:", error); showSendStatus(`Failed to send email: ${error.message}`, false); } finally { modalSendBtn.disabled = false; modalSendSpinner.classList.add('d-none'); } } // --- Clear modal key when hidden --- emailModalElement.addEventListener('hidden.bs.modal', () => { currentModalEmailKey = null; }); // --- Auto Refresh Logic --- function startAutoRefresh() { /* ... */ } function stopAutoRefresh() { /* ... */ } // --- Event Listeners Setup (Only top-level listeners needed) --- daysAgoSelect.addEventListener('change', fetchData); refreshBtn.addEventListener('click', fetchData); modalSendBtn.addEventListener('click', handleSendEmail); // Event delegation is handled within populateEmailGroups by adding listener to emailGroupsContainer // --- Initial Load --- const savedDaysAgo = sessionStorage.getItem('selectedDaysAgo'); if (savedDaysAgo && ['7', '14', '28'].includes(savedDaysAgo)) { daysAgoSelect.value = savedDaysAgo; } else { daysAgoSelect.value = typeof defaultDaysAgo !== 'undefined' ? defaultDaysAgo : '7'; sessionStorage.setItem('selectedDaysAgo', daysAgoSelect.value); } fetchData(); // Initial fetch // --- Add current year to footer --- // ... }); // --- END of DOMContentLoaded listener ---