No requests logged yet.
";
}
appState.requests.forEach(req => {
const item = document.createElement("div");
item.className = "prrt-request-item";
item.innerHTML = `
${req.subject} (${req.agency})
`;
requestsList.appendChild(item);
});
}
function getUniqueAgencies() {
const agencies = new Set();
appState.requests.forEach(req => {
if (req.agency && req.agency.trim()) {
agencies.add(req.agency.trim());
}
});
return Array.from(agencies).sort();
}
function renderDashboardTab() {
// Populate Agency Filter
const uniqueAgencies = getUniqueAgencies();
const currentAgencyFilter = filterAgencySelect.value;
filterAgencySelect.innerHTML = '
';
uniqueAgencies.forEach(agency => {
const option = document.createElement('option');
option.value = agency;
option.textContent = agency;
filterAgencySelect.appendChild(option);
});
filterAgencySelect.value = currentAgencyFilter;
const agencyFilter = filterAgencySelect.value;
const statusFilter = filterStatusSelect.value;
const today = new Date().toISOString().split('T')[0];
// 1. Filtering
let filteredRequests = appState.requests.filter(req => {
const agencyMatch = agencyFilter === 'all' || req.agency === agencyFilter;
const statusMatch = statusFilter === 'all' || req.status === statusFilter;
return agencyMatch && statusMatch;
});
// 2. Sorting
filteredRequests.sort((a, b) => {
const dateA = new Date(calculateDueDate(a.dateSubmitted, a.daysExtended || 0));
const dateB = new Date(calculateDueDate(b.dateSubmitted, b.daysExtended || 0));
if (currentSortColumn === 'submittedDate') {
return new Date(a.dateSubmitted) - new Date(b.dateSubmitted);
} else if (currentSortColumn === 'dueDateDesc') {
return dateB - dateA;
}
// default is dueDateAsc
return dateA - dateB;
});
// 3. Render Table
tableBody.innerHTML = "";
if (filteredRequests.length === 0) {
tableBody.innerHTML = "
| No requests match the current filters. |
";
return;
}
filteredRequests.forEach(req => {
const dueDate = calculateDueDate(req.dateSubmitted, req.daysExtended || 0);
const daysLeft = calculateDaysRemaining(dueDate);
const isOverdue = daysLeft < 0 && req.status !== 'Fulfilled' && req.status !== 'Closed';
const tr = document.createElement("tr");
let rowClass = '';
let daysText = `${daysLeft} days`;
if (req.status === 'Fulfilled' || req.status === 'Closed') {
daysText = 'N/A';
} else if (isOverdue) {
rowClass = 'overdue-row';
daysText = `
${Math.abs(daysLeft)} Days Overdue`;
} else if (daysLeft <= 5) {
rowClass = 'overdue-row'; // Highlight nearing due dates
}
tr.className = rowClass;
tr.innerHTML = `
${req.agency} |
${req.subject} |
${dateToLocalString(req.dateSubmitted)} |
${dateToLocalString(dueDate)} |
${req.status} |
${req.trackingNumber || 'N/A'} |
${daysText} |
`;
tableBody.appendChild(tr);
});
}
// --- Event Handlers ---
// Add Request
addReqBtn.addEventListener("click", () => {
const agency = reqAgencyInput.value.trim();
const subject = reqSubjectInput.value.trim();
const dateSubmitted = reqDateSubmittedInput.value;
const status = reqStatusSelect.value;
const trackingNumber = reqTrackingInput.value.trim();
if (!agency || !subject || !dateSubmitted) {
alert("Please enter the Agency, Subject, and Date Submitted.");
return;
}
const newRequest = {
id: Date.now(),
agency: agency,
subject: subject,
dateSubmitted: dateSubmitted,
status: status,
trackingNumber: trackingNumber,
daysExtended: 0 // Default to 0, user can manually update the object if needed
};
appState.requests.push(newRequest);
saveState();
renderConfigTab();
// Clear form
reqAgencyInput.value = "";
reqSubjectInput.value = "";
reqTrackingInput.value = "";
reqDateSubmittedInput.value = "";
reqStatusSelect.value = "Submitted";
});
// Remove Request (Event Delegation)
requestsList.addEventListener("click", (e) => {
if (e.target.classList.contains("prrt-remove-btn")) {
const id = parseInt(e.target.dataset.id);
appState.requests = appState.requests.filter(req => req.id !== id);
saveState();
renderConfigTab();
}
});
// Update Dashboard Button
updateBtn.addEventListener("click", () => {
// Data already in appState, just re-render dashboard
renderDashboardTab();
showTab(0);
});
// Filters/Sort Change
filterAgencySelect.addEventListener("change", renderDashboardTab);
filterStatusSelect.addEventListener("change", renderDashboardTab);
sortBySelect.addEventListener("change", (e) => {
currentSortColumn = e.target.value;
renderDashboardTab();
});
// PDF Download
pdfBtn.addEventListener("click", () => {
const { jsPDF } = window.jspdf;
const fileName = `Public_Records_Report_${new Date().toISOString().split('T')[0]}.pdf`;
// Clone the table element to remove interactive elements like the sort indicators
const tableClone = toolContainer.querySelector("#prrt-requests-table").cloneNode(true);
// Remove event handlers (not strictly necessary but good practice)
tableClone.querySelectorAll('th').forEach(th => th.removeAttribute('data-sort'));
// Remove background colors and borders for a cleaner look in the PDF
tableClone.style.backgroundColor = '#ffffff';
tableClone.style.border = 'none';
// Set table rows to white for PDF background, and simplify status tags
tableClone.querySelectorAll('tr').forEach(tr => {
tr.classList.remove('overdue-row');
tr.style.backgroundColor = 'white';
});
tableClone.querySelectorAll('.overdue-text').forEach(span => {
span.style.color = '#000'; // Black text for print
span.style.fontWeight = 'normal';
});
// Wrap clone for capture
const tempWrapper = document.createElement('div');
tempWrapper.id = 'temp-pdf-wrapper';
tempWrapper.style.padding = '20px';
tempWrapper.appendChild(tableClone);
document.body.appendChild(tempWrapper); // Must be in DOM for html2canvas
html2canvas(tempWrapper, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff'
}).then(canvas => {
// Remove temp wrapper
document.body.removeChild(tempWrapper);
const imgData = canvas.toDataURL('image/png');
const doc = new jsPDF({
orientation: 'l', // Landscape often better for wide tables
unit: 'pt',
format: 'a4'
});
const pdfWidth = doc.internal.pageSize.getWidth();
const pdfHeight = doc.internal.pageSize.getHeight();
const imgProps = doc.getImageProperties(imgData);
const imgWidth = imgProps.width;
const imgHeight = imgProps.height;
const margin = 30; // Smaller margin for landscape
const usableWidth = pdfWidth - (2 * margin);
const ratio = usableWidth / imgWidth;
let scaledHeight = imgHeight * ratio;
doc.addImage(imgData, 'PNG', margin, margin, usableWidth, scaledHeight);
doc.save(fileName);
}).catch(err => {
// Ensure temp element is removed even on error
const temp = document.getElementById('temp-pdf-wrapper');
if (temp) document.body.removeChild(temp);
console.error("PRRT PDF Error:", err);
// alert("An error occurred while generating the PDF."); // Per spec
});
});
// --- Local Storage ---
function saveState() {
try {
localStorage.setItem("prrtAppState", JSON.stringify(appState));
} catch (e) { console.warn("PRRT: Could not save state."); }
}
function loadState() {
try {
const storedState = localStorage.getItem("prrtAppState");
if (storedState) appState = JSON.parse(storedState);
} catch (e) { console.warn("PRRT: Could not load state."); }
}
// --- Initial Load ---
loadState();
renderConfigTab();
renderDashboardTab();
showTab(0);
});