Permitting Dashboard

Permitting Dashboard

Track and manage the status and lifecycle of permits.

Permit Overview

Permits by Type

Permit Status Distribution

Permit Log

Permit ID Type Status Application Date Cycle Time (Days)

Manage Permits

${totalPermits}

Pending / In Review

${pending}

Approved

${approved}

Avg. Cycle Time

${avgCycleTime.toFixed(0)} Days

`; }; const renderDataTable = () => { dataTableBody.innerHTML = ''; const sortedPermits = [...state.permits].sort((a, b) => new Date(b.appDate) - new Date(a.appDate)); sortedPermits.forEach(p => { const cycleTime = dayDiff(p.appDate, p.approvalDate || new Date().toISOString().split('T')[0]); const statusColors = { 'Approved': 'bg-green-100 text-green-800', 'Pending': 'bg-yellow-100 text-yellow-800', 'In Review': 'bg-blue-100 text-blue-800', 'Denied': 'bg-red-100 text-red-800' }; const row = ` ${p.id} ${p.type} ${p.status} ${p.appDate} ${cycleTime !== null ? `${cycleTime} days` : 'N/A'} `; dataTableBody.innerHTML += row; }); }; const renderCharts = () => { // Permits by Type const countByType = state.permits.reduce((acc, p) => { acc[p.type] = (acc[p.type] || 0) + 1; return acc; }, {}); const typeLabels = Object.keys(countByType); const typeData = typeLabels.map(label => countByType[label]); if (typeChart) typeChart.destroy(); typeChart = new Chart(typeCanvas.getContext('2d'), { type: 'bar', data: { labels: typeLabels, datasets: [{ label: 'Number of Permits', data: typeData, backgroundColor: 'rgba(245, 158, 11, 0.7)' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } }); // Status Distribution const countByStatus = state.permits.reduce((acc, p) => { acc[p.status] = (acc[p.status] || 0) + 1; return acc; }, {}); const statusLabels = Object.keys(countByStatus); const statusData = statusLabels.map(label => countByStatus[label]); if (statusChart) statusChart.destroy(); statusChart = new Chart(statusCanvas.getContext('2d'), { type: 'pie', data: { labels: statusLabels, datasets: [{ data: statusData, backgroundColor: ['#22c55e', '#facc15', '#3b82f6', '#ef4444', '#6b7280'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } } } }); }; const renderConfigRows = () => { configRowsContainer.innerHTML = ''; const sortedPermits = [...state.permits].sort((a,b) => b.id.localeCompare(a.id)); sortedPermits.forEach(p => { configRowsContainer.innerHTML += `
`.replaceAll('class="config-input"', 'class="config-input w-full border-gray-300 rounded-md shadow-sm"'); }); addConfigEventListeners(); }; // --- EVENT HANDLERS --- const handleConfigChange = (e) => { const id = e.target.dataset.id; const field = e.target.dataset.field; const value = e.target.value; const permit = state.permits.find(p => p.id === id); if (permit) { if (field === 'approvalDate' && value === '') permit[field] = null; else permit[field] = value; } renderAll(); }; const handleAddPermit = () => { const newId = "NEW-" + (state.permits.length + 1); const today = new Date().toISOString().split('T')[0]; state.permits.push({ id: newId, type: "Building", status: "Pending", appDate: today, approvalDate: null }); renderAll(); }; const handleRemovePermit = (e) => { const id = e.target.dataset.id; state.permits = state.permits.filter(p => p.id !== id); renderAll(); }; const addConfigEventListeners = () => { document.querySelectorAll('.config-input').forEach(input => input.addEventListener('change', handleConfigChange)); document.querySelectorAll('.remove-btn').forEach(button => button.addEventListener('click', handleRemovePermit)); }; const handleDownloadPdf = () => { const { jsPDF } = window.jspdf; const pdfContent = document.getElementById('pdf-content'); document.querySelectorAll('.no-print').forEach(el => el.style.visibility = 'hidden'); Chart.defaults.animation = false; html2canvas(pdfContent, { scale: 2, useCORS: true, backgroundColor: '#ffffff' }).then(canvas => { document.querySelectorAll('.no-print').forEach(el => el.style.visibility = 'visible'); Chart.defaults.animation = true; const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const imgWidth = pdfWidth - 20; const imgHeight = canvas.height * imgWidth / canvas.width; pdf.addImage(imgData, 'PNG', 10, 10, imgWidth, imgHeight); pdf.save('Permitting-Dashboard.pdf'); }); }; // --- TABBING LOGIC --- let currentTabIndex = 0; const updateTabButtons = () => { prevTabBtn.disabled = currentTabIndex === 0; nextTabBtn.disabled = currentTabIndex === tabContents.length - 1; }; const switchTab = (index) => { tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); tabButtons[index].classList.add('active'); tabContents[index].classList.add('active'); currentTabIndex = index; updateTabButtons(); }; tabButtons.forEach((button, index) => button.addEventListener('click', () => switchTab(index))); prevTabBtn.addEventListener('click', () => { if (currentTabIndex > 0) switchTab(currentTabIndex - 1); }); nextTabBtn.addEventListener('click', () => { if (currentTabIndex < tabContents.length - 1) switchTab(currentTabIndex + 1); }); // --- INITIALIZATION --- if (kpiContainer && addPermitBtn && downloadPdfBtn) { addPermitBtn.addEventListener('click', handleAddPermit); downloadPdfBtn.addEventListener('click', handleDownloadPdf); renderAll(); updateTabButtons(); } else { console.error("Essential dashboard elements could not be found."); } });
Scroll to Top