Public Records Request Tracker

Public Records Request Tracker

All Active Requests

Agency Subject Submitted Due Date Status Ref # Days Left

Add New Request

Current Requests

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); });
Scroll to Top