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