// Global State let currentTab = 'feed'; let issuesData = []; let statsData = null; let leafletMap = null; let mapMarkers = []; let pickerMap = null; let pickerMarker = null; let categoryChartInstance = null; let statusChartInstance = null; // Category Config const CATEGORY_META = { 'آسفالت و معابر': { icon: 'fa-road', color: 'text-amber-500', bg: 'bg-amber-50', badge: 'bg-amber-100 text-amber-800' }, 'روشنایی و برق': { icon: 'fa-lightbulb', color: 'text-yellow-500', bg: 'bg-yellow-50', badge: 'bg-yellow-100 text-yellow-800' }, 'نظافت و پسماند': { icon: 'fa-trash-can', color: 'text-red-500', bg: 'bg-red-50', badge: 'bg-red-100 text-red-800' }, 'فضای سبز و بوستان‌ها': { icon: 'fa-tree', color: 'text-emerald-500', bg: 'bg-emerald-50', badge: 'bg-emerald-100 text-emerald-800' }, 'ترافیک و حمل‌ونقل': { icon: 'fa-traffic-light', color: 'text-blue-500', bg: 'bg-blue-50', badge: 'bg-blue-100 text-blue-800' }, 'ساختمان‌سازی و سد معبر': { icon: 'fa-person-digging', color: 'text-orange-500', bg: 'bg-orange-50', badge: 'bg-orange-100 text-orange-800' }, }; const STATUS_META = { 'pending': { label: 'در انتظار بررسی', color: 'bg-slate-100 text-slate-700 border-slate-300', dot: 'bg-slate-400', step: 1 }, 'reviewing': { label: 'بررسی کارشناسی', color: 'bg-blue-100 text-blue-800 border-blue-200', dot: 'bg-blue-500', step: 2 }, 'in_progress': { label: 'در حال انجام عملیات', color: 'bg-amber-100 text-amber-800 border-amber-200', dot: 'bg-amber-500', step: 3 }, 'resolved': { label: 'رفع و حل شده', color: 'bg-emerald-100 text-emerald-800 border-emerald-200', dot: 'bg-emerald-500', step: 4 }, 'rejected': { label: 'رد شده / خارج از اختیارات', color: 'bg-rose-100 text-rose-800 border-rose-200', dot: 'bg-rose-500', step: 0 } }; const PRIORITY_META = { 'emergency': { label: '🚨 فوری و خطرآفرین', badge: 'bg-red-600 text-white animate-pulse' }, 'high': { label: 'اولویت بالا', badge: 'bg-orange-100 text-orange-800' }, 'medium': { label: 'متوسط', badge: 'bg-yellow-100 text-yellow-800' }, 'low': { label: 'عادی', badge: 'bg-slate-100 text-slate-700' } }; // Initialize Application document.addEventListener('DOMContentLoaded', () => { setupCategoryRadios(); loadStats(); fetchIssues(); updateClock(); setInterval(updateClock, 60000); }); function updateClock() { const el = document.getElementById('currentDateTime'); if (!el) return; const now = new Date(); const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }; try { const dateStr = new Intl.DateTimeFormat('fa-IR', options).format(now); el.innerHTML = ` ${dateStr}`; } catch (e) { el.innerHTML = ` سامانه فعال است`; } } // Switch Active Tab function switchTab(tabId) { currentTab = tabId; // Hide all views ['feed', 'map', 'track', 'admin'].forEach(tab => { const view = document.getElementById(`view-${tab}`); const navBtn = document.getElementById(`nav-${tab}`); if (view) view.classList.add('hidden'); if (navBtn) { navBtn.classList.remove('bg-white', 'text-emerald-700', 'shadow-sm'); navBtn.classList.add('text-slate-600'); } }); // Show selected view const activeView = document.getElementById(`view-${tabId}`); const activeNav = document.getElementById(`nav-${tabId}`); if (activeView) activeView.classList.remove('hidden'); if (activeNav) { activeNav.classList.add('bg-white', 'text-emerald-700', 'shadow-sm'); activeNav.classList.remove('text-slate-600'); } // Tab specific actions if (tabId === 'map') { setTimeout(initLeafletMap, 100); } else if (tabId === 'admin') { loadStats(); loadAdminIssues(); } } // Fetch Stats from API async function loadStats() { try { const res = await fetch('/api/stats'); if (!res.ok) return; const data = await res.json(); statsData = data; document.getElementById('stat-total').textContent = data.total.toLocaleString('fa-IR'); document.getElementById('stat-resolved').textContent = data.resolved.toLocaleString('fa-IR'); document.getElementById('stat-in-progress').textContent = (data.in_progress + data.reviewing).toLocaleString('fa-IR'); document.getElementById('stat-upvotes').textContent = data.total_upvotes.toLocaleString('fa-IR'); document.getElementById('stat-rate').textContent = `نرخ تکمیل: ${data.resolution_rate}٪`; if (currentTab === 'admin') { renderAdminCharts(data); } } catch (err) { console.error('Error loading stats:', err); } } // Fetch Issues with Filters async function fetchIssues() { const loadingEl = document.getElementById('feedLoading'); const emptyEl = document.getElementById('feedEmpty'); const container = document.getElementById('issuesContainer'); loadingEl.classList.remove('hidden'); emptyEl.classList.add('hidden'); container.innerHTML = ''; const activePill = document.querySelector('.cat-pill.active'); const selectedCat = activePill ? activePill.getAttribute('data-cat') || 'all' : 'all'; const district = document.getElementById('filterDistrict').value; const status = document.getElementById('filterStatus').value; const priority = document.getElementById('filterPriority').value; const sortBy = document.getElementById('sortBy').value; let url = `/api/issues?category=${encodeURIComponent(selectedCat)}&status=${status}&district=${district}&priority=${priority}&sort_by=${sortBy}`; try { const res = await fetch(url); const data = await res.json(); issuesData = data; loadingEl.classList.add('hidden'); if (data.length === 0) { emptyEl.classList.remove('hidden'); return; } renderIssueCards(data); if (leafletMap) { updateMapMarkers(data); } } catch (err) { loadingEl.classList.add('hidden'); console.error('Error fetching issues:', err); } } // Render Issue Cards function renderIssueCards(issues) { const container = document.getElementById('issuesContainer'); container.innerHTML = ''; issues.forEach(issue => { const cat = CATEGORY_META[issue.category] || { icon: 'fa-circle-info', color: 'text-emerald-500', bg: 'bg-emerald-50', badge: 'bg-emerald-100 text-emerald-800' }; const st = STATUS_META[issue.status] || STATUS_META['pending']; const prio = PRIORITY_META[issue.priority] || PRIORITY_META['medium']; const imgUrl = issue.image_url || 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80'; const card = document.createElement('div'); card.className = "bg-white rounded-3xl border border-slate-200 overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 flex flex-col group"; card.innerHTML = `
${issue.title}
${st.label} ${prio.label}
${issue.tracking_code} منطقه ${issue.district} شهرداری
${issue.category} ${issue.created_at}

${issue.title}

${issue.description}

${issue.address}
`; container.appendChild(card); }); } // Category filter button click function filterByCategory(catName) { document.querySelectorAll('.cat-pill').forEach(pill => { pill.classList.remove('active', 'bg-slate-900', 'text-white'); pill.classList.add('bg-slate-100', 'text-slate-600'); }); const target = event ? event.currentTarget : document.querySelector('.cat-pill'); if (target) { target.classList.add('active', 'bg-slate-900', 'text-white'); target.classList.remove('bg-slate-100', 'text-slate-600'); target.setAttribute('data-cat', catName); } fetchIssues(); } function applyFilters() { fetchIssues(); } // Upvote Handler async function handleUpvote(e, issueId, btn) { e.stopPropagation(); try { const res = await fetch(`/api/issues/${issueId}/upvote`, { method: 'POST' }); if (res.ok) { const data = await res.json(); const countEl = btn.querySelector('.count'); if (countEl) countEl.textContent = data.upvotes; btn.classList.add('bg-emerald-100', 'text-emerald-800', 'border-emerald-400'); btn.querySelector('i').classList.replace('fa-regular', 'fa-solid'); Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'تایید شما با موفقیت ثبت شد', showConfirmButton: false, timer: 1800 }); } } catch (err) { console.error('Error upvoting:', err); } } // Quick Search / Track from Hero function handleQuickSearch() { const val = document.getElementById('quickTrackInput').value.trim(); if (!val) return; if (val.toUpperCase().startsWith('SHR-')) { // Switch to track tab and search switchTab('track'); document.getElementById('trackCodeInput').value = val.toUpperCase(); fetchTrackingDetail(); } else { // Search keyword in feed switchTab('feed'); searchIssues(val); } } async function searchIssues(keyword) { const loadingEl = document.getElementById('feedLoading'); const container = document.getElementById('issuesContainer'); loadingEl.classList.remove('hidden'); container.innerHTML = ''; try { const res = await fetch(`/api/issues?search=${encodeURIComponent(keyword)}`); const data = await res.json(); loadingEl.classList.add('hidden'); renderIssueCards(data); } catch (err) { loadingEl.classList.add('hidden'); } } // Leaflet Map Initialization function initLeafletMap() { if (leafletMap) { leafletMap.invalidateSize(); return; } // Tehran Center Coordinates leafletMap = L.map('leafletMap').setView([35.7538, 51.4172], 12); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap شهرنگار' }).addTo(leafletMap); if (issuesData.length > 0) { updateMapMarkers(issuesData); } } function updateMapMarkers(issues) { if (!leafletMap) return; // Clear existing mapMarkers.forEach(m => leafletMap.removeLayer(m)); mapMarkers = []; issues.forEach(issue => { const isResolved = issue.status === 'resolved'; const isEmergency = issue.priority === 'emergency'; const pinColor = isResolved ? '#10b981' : (isEmergency ? '#ef4444' : '#f59e0b'); const customIcon = L.divIcon({ className: 'custom-map-pin', html: `
`, iconSize: [32, 32], iconAnchor: [16, 32], popupAnchor: [0, -32] }); const marker = L.marker([issue.lat, issue.lng], { icon: customIcon }).addTo(leafletMap); const popupContent = `
${STATUS_META[issue.status]?.label || ''}

${issue.title}

${issue.address}

`; marker.bindPopup(popupContent); mapMarkers.push(marker); }); } // Issue Detail Modal async function openIssueDetail(issueId) { try { const res = await fetch(`/api/issues/${issueId}`); if (!res.ok) return; const issue = await res.json(); const st = STATUS_META[issue.status] || STATUS_META['pending']; const prio = PRIORITY_META[issue.priority] || PRIORITY_META['medium']; const cat = CATEGORY_META[issue.category] || { icon: 'fa-city', color: 'text-emerald-600', badge: 'bg-emerald-100 text-emerald-800' }; const modal = document.getElementById('detailModal'); const content = document.getElementById('detailModalContent'); // Build Stepper Stages const stages = [ { key: 'pending', label: 'ثبت در سامانه', icon: 'fa-file-lines' }, { key: 'reviewing', label: 'بررسی کارشناسی', icon: 'fa-clipboard-check' }, { key: 'in_progress', label: 'اعزام اکیپ و اجرا', icon: 'fa-screwdriver-wrench' }, { key: 'resolved', label: 'رفع کامل مشکل', icon: 'fa-circle-check' } ]; const currentStep = st.step || 1; let stepperHtml = `
${stages.map((stage, idx) => { const stepNum = idx + 1; const isDone = currentStep >= stepNum; const isCurrent = currentStep === stepNum; return `
${stage.label}
`; }).join('')}
`; // Build Timeline items let timelineHtml = (issue.timeline || []).map(tl => `
${tl.title}
${tl.created_at}

${tl.description}

`).join(''); // Build Comments let commentsHtml = (issue.comments || []).map(c => `
${c.author_name} ${c.created_at}

${c.content}

`).join(''); content.innerHTML = `
${issue.tracking_code}

${issue.title}

منطقه ${issue.district} شهرداری • تاریخ ثبت: ${issue.created_at}

${stepperHtml}
تصویر گزارش‌شده توسط شهروند:
${issue.resolved_image_url ? `
تصویر پس از انجام عملیات و رفع نقص:
` : ''}
${issue.category} ${prio.label} ${st.label}

شرح دقیق مشکل:

${issue.description}

نشانی محل: ${issue.address}
${issue.official_response ? `
پاسخ رسمی شهرداری منطقه ${issue.district}:

${issue.official_response}

` : ''}

تاریخچه و روند رسیدگی پرونده

${timelineHtml}

نظرات و مشاهدات شهروندان

${(issue.comments || []).length} نظر ثبت شده
${commentsHtml.length > 0 ? commentsHtml : '

هنوز نظری ثبت نشده است. شما اولین نفر باشید!

'}
`; modal.classList.remove('hidden'); } catch (err) { console.error('Error viewing issue detail:', err); } } function closeDetailModal() { document.getElementById('detailModal').classList.add('hidden'); } // Add Comment Handler async function submitComment(e, issueId) { e.preventDefault(); const author = document.getElementById('commentAuthor').value.trim() || 'شهروند'; const content = document.getElementById('commentText').value.trim(); if (!content) return; try { const res = await fetch(`/api/issues/${issueId}/comment`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author_name: author, content: content }) }); if (res.ok) { document.getElementById('commentText').value = ''; openIssueDetail(issueId); // Refresh modal } } catch (err) { console.error('Error submitting comment:', err); } } // Tracking Portal Search async function fetchTrackingDetail() { const code = document.getElementById('trackCodeInput').value.trim().toUpperCase(); if (!code) { Swal.fire({ icon: 'warning', title: 'لطفاً کد رهگیری را وارد نمایید', confirmButtonText: 'متوجه شدم' }); return; } const container = document.getElementById('trackResultContainer'); container.classList.remove('hidden'); container.innerHTML = `

در حال استعلام از سامانه نظارت شهری...

`; try { const res = await fetch(`/api/track/${encodeURIComponent(code)}`); if (!res.ok) { container.innerHTML = `

هیچ گزارشی با کد رهگیری «${code}» یافت نشد!

لطفاً از صحت کد وارد شده اطمینان حاصل فرمایید.

`; return; } const issue = await res.json(); const st = STATUS_META[issue.status] || STATUS_META['pending']; const cat = CATEGORY_META[issue.category] || { icon: 'fa-city', color: 'text-emerald-600', badge: 'bg-emerald-100 text-emerald-800' }; container.innerHTML = `
نتیجه استعلام پرونده

${issue.title}

${issue.tracking_code} ${st.label}
دسته‌بندی موضوع: ${issue.category}
منطقه شهرداری: منطقه ${issue.district}
تاریخ و ساعت ثبت: ${issue.created_at}
${issue.official_response ? `

پاسخ رسمی بازرس و اکیپ شهرداری:

${issue.official_response}

` : ''}

مراحل و اقدامات انجام‌شده:

${(issue.timeline || []).map(tl => `
${tl.title} ${tl.created_at}

${tl.description}

`).join('')}
`; } catch (err) { container.innerHTML = `

خطا در برقراری ارتباط با سرور.

`; } } // Submit New Report Modal & Map Picker function openNewReportModal() { document.getElementById('newReportModal').classList.remove('hidden'); setTimeout(initPickerMap, 200); } function closeNewReportModal() { document.getElementById('newReportModal').classList.add('hidden'); } function setupCategoryRadios() { const radios = document.querySelectorAll('.category-radio input[type="radio"]'); radios.forEach(radio => { radio.addEventListener('change', () => { document.querySelectorAll('.category-radio').forEach(r => { r.classList.remove('border-emerald-500', 'bg-emerald-50/50'); }); if (radio.checked) { radio.closest('.category-radio').classList.add('border-emerald-500', 'bg-emerald-50/50'); } }); }); } function selectSampleImage(el, url) { document.querySelectorAll('.sample-img-card').forEach(c => c.classList.remove('border-emerald-500', 'ring-2', 'ring-emerald-200')); el.classList.add('border-emerald-500', 'ring-2', 'ring-emerald-200'); document.getElementById('reportImageUrl').value = url; } function initPickerMap() { if (pickerMap) { pickerMap.invalidateSize(); return; } pickerMap = L.map('pickerMap').setView([35.7538, 51.4172], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(pickerMap); pickerMarker = L.marker([35.7538, 51.4172], { draggable: true }).addTo(pickerMap); pickerMarker.on('dragend', function (e) { const pos = e.target.getLatLng(); document.getElementById('reportLat').value = pos.lat.toFixed(6); document.getElementById('reportLng').value = pos.lng.toFixed(6); }); pickerMap.on('click', function (e) { pickerMarker.setLatLng(e.latlng); document.getElementById('reportLat').value = e.latlng.lat.toFixed(6); document.getElementById('reportLng').value = e.latlng.lng.toFixed(6); }); } // Submit Form Handler async function submitNewReport(e) { e.preventDefault(); const btn = document.getElementById('submitReportBtn'); btn.disabled = true; btn.innerHTML = ` در حال ثبت...`; const category = document.querySelector('input[name="category"]:checked')?.value || 'آسفالت و معابر'; const title = document.getElementById('reportTitle').value.trim(); const district = parseInt(document.getElementById('reportDistrict').value); const priority = document.getElementById('reportPriority').value; const address = document.getElementById('reportAddress').value.trim(); const description = document.getElementById('reportDescription').value.trim(); const imageUrl = document.getElementById('reportImageUrl').value.trim() || 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80'; const reporterName = document.getElementById('reporterName').value.trim() || 'شهروند مسئول'; const reporterPhone = document.getElementById('reporterPhone').value.trim(); const lat = parseFloat(document.getElementById('reportLat').value) || 35.7538; const lng = parseFloat(document.getElementById('reportLng').value) || 51.4172; const payload = { title, category, district, priority, address, description, image_url: imageUrl, reporter_name: reporterName, reporter_phone: reporterPhone, lat, lng }; try { const res = await fetch('/api/issues', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { const created = await res.json(); closeNewReportModal(); document.getElementById('newReportForm').reset(); // Success Alert with Tracking Card Swal.fire({ icon: 'success', title: 'گزارش شما با موفقیت ثبت شد!', html: `
کد رهگیری اختصاصی شما:
${created.tracking_code}

این کد را جهت استعلام‌های بعدی و پیگیری مراحل یادداشت نمایید.

`, confirmButtonText: 'مشاهده گزارش در صفحه اصلی', confirmButtonColor: '#059669' }); loadStats(); fetchIssues(); } } catch (err) { Swal.fire({ icon: 'error', title: 'خطا در ثبت گزارش', text: 'لطفاً دوباره تلاش کنید.' }); } finally { btn.disabled = false; btn.innerHTML = ` ارسال و ثبت نهایی گزارش`; } } // ADMIN DASHBOARD SECTION async function loadAdminIssues() { const tbody = document.getElementById('adminTableBody'); const countEl = document.getElementById('adminTableCount'); tbody.innerHTML = `در حال بارگذاری اطلاعات پرونده‌ها...`; try { const res = await fetch('/api/issues?sort_by=newest'); const data = await res.json(); countEl.textContent = `${data.length} پرونده ثبت شده`; tbody.innerHTML = ''; data.forEach(issue => { const st = STATUS_META[issue.status] || STATUS_META['pending']; const prio = PRIORITY_META[issue.priority] || PRIORITY_META['medium']; const tr = document.createElement('tr'); tr.className = "hover:bg-slate-50 transition-colors"; tr.innerHTML = ` ${issue.tracking_code}
${issue.title}
${issue.address}
${issue.category} منطقه ${issue.district} ${prio.label} ${st.label} ${issue.created_at} `; tbody.appendChild(tr); }); } catch (err) { console.error('Error loading admin issues:', err); } } function openAdminActionModal(issueId, currentStatus) { document.getElementById('adminTargetIssueId').value = issueId; document.getElementById('adminNewStatus').value = currentStatus; document.getElementById('adminActionModal').classList.remove('hidden'); } function closeAdminActionModal() { document.getElementById('adminActionModal').classList.add('hidden'); } async function submitAdminAction(e) { e.preventDefault(); const issueId = document.getElementById('adminTargetIssueId').value; const status = document.getElementById('adminNewStatus').value; const timelineTitle = document.getElementById('adminTimelineTitle').value.trim(); const officialResponse = document.getElementById('adminOfficialResponse').value.trim(); const resolvedImageUrl = document.getElementById('adminResolvedImage').value.trim(); try { const res = await fetch(`/api/issues/${issueId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, timeline_title: timelineTitle || undefined, official_response: officialResponse || undefined, resolved_image_url: resolvedImageUrl || undefined }) }); if (res.ok) { closeAdminActionModal(); document.getElementById('adminActionForm').reset(); Swal.fire({ icon: 'success', title: 'وضعیت پرونده با موفقیت به‌روزرسانی شد', timer: 1500 }); loadAdminIssues(); loadStats(); } } catch (err) { console.error('Error submitting admin action:', err); } } // Render Chart.js function renderAdminCharts(stats) { // 1. Category Bar Chart const catCtx = document.getElementById('categoryChart')?.getContext('2d'); if (catCtx) { if (categoryChartInstance) categoryChartInstance.destroy(); categoryChartInstance = new Chart(catCtx, { type: 'bar', data: { labels: stats.categories.map(c => c.category), datasets: [{ label: 'تعداد گزارش‌ها', data: stats.categories.map(c => c.count), backgroundColor: '#059669', borderRadius: 8 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } } }); } // 2. Status Doughnut Chart const statusCtx = document.getElementById('statusChart')?.getContext('2d'); if (statusCtx) { if (statusChartInstance) statusChartInstance.destroy(); statusChartInstance = new Chart(statusCtx, { type: 'doughnut', data: { labels: ['در انتظار بررسی', 'بررسی کارشناسی', 'در حال اجرا', 'حل شده'], datasets: [{ data: [stats.pending, stats.reviewing, stats.in_progress, stats.resolved], backgroundColor: ['#94a3b8', '#3b82f6', '#f59e0b', '#10b981'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } }