// 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 = `
${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 `
`;
}).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} نظر ثبت شده
`;
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' }
}
}
});
}
}