Files

960 lines
39 KiB
JavaScript

// 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 = `<i class="fa-regular fa-clock ml-1"></i> ${dateStr}`;
} catch (e) {
el.innerHTML = `<i class="fa-regular fa-clock ml-1"></i> سامانه فعال است`;
}
}
// 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 = `
<!-- Card Image & Badges -->
<div class="relative h-48 overflow-hidden bg-slate-100">
<img src="${imgUrl}" alt="${issue.title}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div class="absolute inset-0 bg-gradient-to-t from-slate-950/70 via-transparent to-black/30"></div>
<!-- Top Badges -->
<div class="absolute top-3 right-3 left-3 flex items-center justify-between">
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold ${st.color} border shadow-sm flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full ${st.dot}"></span>
${st.label}
</span>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold ${prio.badge} shadow-sm">
${prio.label}
</span>
</div>
<!-- Bottom Overlay Info -->
<div class="absolute bottom-3 right-3 left-3 flex items-center justify-between text-white text-xs">
<span class="font-mono bg-black/40 backdrop-blur-md px-2 py-0.5 rounded-md font-bold text-emerald-300">
<i class="fa-solid fa-hashtag text-[10px]"></i> ${issue.tracking_code}
</span>
<span class="bg-black/40 backdrop-blur-md px-2 py-0.5 rounded-md text-[11px]">
منطقه ${issue.district} شهرداری
</span>
</div>
</div>
<!-- Card Content -->
<div class="p-5 flex-1 flex flex-col justify-between space-y-4">
<div class="space-y-2">
<!-- Category & Date -->
<div class="flex items-center justify-between text-xs text-slate-400">
<span class="flex items-center gap-1.5 font-bold ${cat.color}">
<i class="fa-solid ${cat.icon}"></i>
${issue.category}
</span>
<span><i class="fa-regular fa-clock ml-1"></i>${issue.created_at}</span>
</div>
<!-- Title -->
<h3 onclick="openIssueDetail(${issue.id})" class="text-base font-bold text-slate-900 line-clamp-1 hover:text-emerald-600 cursor-pointer transition-colors">
${issue.title}
</h3>
<!-- Description -->
<p class="text-xs text-slate-600 line-clamp-2 leading-relaxed">
${issue.description}
</p>
<!-- Address -->
<div class="flex items-start gap-1.5 text-xs text-slate-500 pt-1">
<i class="fa-solid fa-location-dot text-rose-500 mt-0.5 text-xs"></i>
<span class="line-clamp-1">${issue.address}</span>
</div>
</div>
<!-- Card Footer (Actions) -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-between">
<!-- Upvote Button -->
<button onclick="handleUpvote(event, ${issue.id}, this)" class="upvote-btn flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-slate-200 hover:border-emerald-500 hover:bg-emerald-50 text-slate-700 hover:text-emerald-700 text-xs font-bold transition-all">
<i class="fa-regular fa-thumbs-up"></i>
<span class="count">${issue.upvotes}</span>
<span class="text-[10px] text-slate-400">تایید شهروندی</span>
</button>
<!-- Detail Button -->
<button onclick="openIssueDetail(${issue.id})" class="bg-slate-100 hover:bg-emerald-600 hover:text-white text-slate-700 px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all flex items-center gap-1">
<span>مشاهده و پیگیری</span>
<i class="fa-solid fa-chevron-left text-[10px]"></i>
</button>
</div>
</div>
`;
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: `
<div style="background-color: ${pinColor}; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; border: 3px solid white; box-shadow: 0 4px 10px rgba(0,0,0,0.3); font-size: 13px;">
<i class="fa-solid ${CATEGORY_META[issue.category]?.icon || 'fa-map-pin'}"></i>
</div>
`,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
const marker = L.marker([issue.lat, issue.lng], { icon: customIcon }).addTo(leafletMap);
const popupContent = `
<div class="p-2 space-y-2 min-w-[200px]">
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full ${STATUS_META[issue.status]?.color || 'bg-slate-100'}">
${STATUS_META[issue.status]?.label || ''}
</span>
<h4 class="font-bold text-sm text-slate-900 mt-1">${issue.title}</h4>
<p class="text-xs text-slate-500 line-clamp-2">${issue.address}</p>
<button onclick="openIssueDetail(${issue.id})" class="w-full mt-2 bg-emerald-600 text-white py-1.5 rounded-lg text-xs font-bold hover:bg-emerald-700">
مشاهده پرونده کامل
</button>
</div>
`;
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 = `
<div class="grid grid-cols-4 gap-2 text-center my-6 relative">
${stages.map((stage, idx) => {
const stepNum = idx + 1;
const isDone = currentStep >= stepNum;
const isCurrent = currentStep === stepNum;
return `
<div class="flex flex-col items-center gap-1.5 z-10">
<div class="w-10 h-10 rounded-2xl flex items-center justify-center text-sm font-bold transition-all ${isDone ? 'bg-emerald-600 text-white shadow-lg shadow-emerald-600/30' : 'bg-slate-200 text-slate-400'} ${isCurrent ? 'ring-4 ring-emerald-100 animate-bounce' : ''}">
<i class="fa-solid ${stage.icon}"></i>
</div>
<span class="text-[11px] font-bold ${isDone ? 'text-emerald-700' : 'text-slate-400'}">${stage.label}</span>
</div>
`;
}).join('')}
</div>
`;
// Build Timeline items
let timelineHtml = (issue.timeline || []).map(tl => `
<div class="relative pr-6 pb-5 border-r-2 border-emerald-500 last:border-transparent">
<span class="absolute -right-2 top-0 w-4 h-4 rounded-full bg-emerald-600 border-2 border-white"></span>
<div class="text-xs font-bold text-slate-800">${tl.title}</div>
<div class="text-[11px] text-slate-400 mb-1"><i class="fa-regular fa-clock ml-1"></i>${tl.created_at}</div>
<p class="text-xs text-slate-600 leading-relaxed bg-slate-50 p-2.5 rounded-xl border border-slate-200/60">${tl.description}</p>
</div>
`).join('');
// Build Comments
let commentsHtml = (issue.comments || []).map(c => `
<div class="bg-slate-50 p-3 rounded-2xl border border-slate-200 space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="font-bold text-slate-800 flex items-center gap-1.5">
<i class="fa-solid fa-user text-slate-400 text-[10px]"></i>
${c.author_name}
</span>
<span class="text-[10px] text-slate-400">${c.created_at}</span>
</div>
<p class="text-xs text-slate-600">${c.content}</p>
</div>
`).join('');
content.innerHTML = `
<!-- Modal Header -->
<div class="bg-gradient-to-r from-slate-900 to-slate-800 text-white p-6 sticky top-0 z-20 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-mono bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-3 py-1 rounded-xl text-sm font-bold">
${issue.tracking_code}
</span>
<div>
<h3 class="text-base sm:text-lg font-bold">${issue.title}</h3>
<p class="text-xs text-slate-400">منطقه ${issue.district} شهرداری • تاریخ ثبت: ${issue.created_at}</p>
</div>
</div>
<button onclick="closeDetailModal()" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="p-6 space-y-6">
<!-- Status Stepper Bar -->
${stepperHtml}
<!-- Before / After Images (if resolved) -->
<div class="grid grid-cols-1 ${issue.resolved_image_url ? 'sm:grid-cols-2' : ''} gap-4">
<div>
<span class="text-xs font-bold text-slate-500 block mb-1.5">تصویر گزارش‌شده توسط شهروند:</span>
<div class="h-56 rounded-2xl overflow-hidden bg-slate-100 border border-slate-200">
<img src="${issue.image_url || 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80'}" class="w-full h-full object-cover">
</div>
</div>
${issue.resolved_image_url ? `
<div>
<span class="text-xs font-bold text-emerald-600 block mb-1.5 flex items-center gap-1">
<i class="fa-solid fa-circle-check"></i>
تصویر پس از انجام عملیات و رفع نقص:
</span>
<div class="h-56 rounded-2xl overflow-hidden bg-slate-100 border-2 border-emerald-400">
<img src="${issue.resolved_image_url}" class="w-full h-full object-cover">
</div>
</div>
` : ''}
</div>
<!-- Issue Information Details -->
<div class="bg-slate-50 p-5 rounded-2xl border border-slate-200 space-y-3">
<div class="flex flex-wrap items-center gap-2">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${cat.badge}"><i class="fa-solid ${cat.icon} ml-1"></i>${issue.category}</span>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${prio.badge}">${prio.label}</span>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${st.color} border">${st.label}</span>
</div>
<div>
<h4 class="text-xs font-bold text-slate-700 mb-1">شرح دقیق مشکل:</h4>
<p class="text-xs sm:text-sm text-slate-700 leading-relaxed">${issue.description}</p>
</div>
<div class="pt-2 border-t border-slate-200/80 flex items-start gap-2 text-xs text-slate-600">
<i class="fa-solid fa-location-dot text-rose-500 mt-0.5"></i>
<span><strong>نشانی محل:</strong> ${issue.address}</span>
</div>
</div>
<!-- Official Municipal Response -->
${issue.official_response ? `
<div class="bg-emerald-50 border border-emerald-200 p-5 rounded-2xl space-y-2">
<div class="flex items-center gap-2 text-emerald-800 text-xs font-bold">
<i class="fa-solid fa-building-columns text-base"></i>
<span>پاسخ رسمی شهرداری منطقه ${issue.district}:</span>
</div>
<p class="text-xs sm:text-sm text-emerald-900 leading-relaxed font-medium">
${issue.official_response}
</p>
</div>
` : ''}
<!-- Timeline Log -->
<div class="space-y-3">
<h4 class="text-sm font-bold text-slate-900 flex items-center gap-2">
<i class="fa-solid fa-list-timeline text-emerald-600"></i>
<span>تاریخچه و روند رسیدگی پرونده</span>
</h4>
<div class="pr-2 pt-2">
${timelineHtml}
</div>
</div>
<!-- Citizen Comments & Support -->
<div class="space-y-4 pt-4 border-t border-slate-200">
<div class="flex items-center justify-between">
<h4 class="text-sm font-bold text-slate-900 flex items-center gap-2">
<i class="fa-regular fa-comments text-emerald-600"></i>
<span>نظرات و مشاهدات شهروندان</span>
</h4>
<span class="text-xs text-slate-500">${(issue.comments || []).length} نظر ثبت شده</span>
</div>
<!-- Comments List -->
<div class="space-y-2 max-h-48 overflow-y-auto custom-scrollbar">
${commentsHtml.length > 0 ? commentsHtml : '<p class="text-xs text-slate-400 text-center py-4">هنوز نظری ثبت نشده است. شما اولین نفر باشید!</p>'}
</div>
<!-- Add Comment Form -->
<form onsubmit="submitComment(event, ${issue.id})" class="flex gap-2 pt-2">
<input type="text" id="commentAuthor" placeholder="نام شما" class="w-1/3 px-3 py-2 rounded-xl border border-slate-300 text-xs outline-none focus:border-emerald-500">
<input type="text" id="commentText" required placeholder="نظر یا مشاهده تکمیلی خود را بنویسید..." class="flex-1 px-3 py-2 rounded-xl border border-slate-300 text-xs outline-none focus:border-emerald-500">
<button type="submit" class="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-xl text-xs font-bold transition-all">
ارسال
</button>
</form>
</div>
</div>
`;
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 = `
<div class="py-12 text-center">
<div class="inline-block animate-spin text-3xl text-emerald-600 mb-2"><i class="fa-solid fa-circle-notch"></i></div>
<p class="text-xs text-slate-500">در حال استعلام از سامانه نظارت شهری...</p>
</div>
`;
try {
const res = await fetch(`/api/track/${encodeURIComponent(code)}`);
if (!res.ok) {
container.innerHTML = `
<div class="py-8 text-center text-rose-600 space-y-2">
<i class="fa-solid fa-triangle-exclamation text-3xl"></i>
<h4 class="font-bold text-sm">هیچ گزارشی با کد رهگیری «${code}» یافت نشد!</h4>
<p class="text-xs text-slate-500">لطفاً از صحت کد وارد شده اطمینان حاصل فرمایید.</p>
</div>
`;
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 = `
<div class="flex flex-wrap items-center justify-between gap-4 pb-4 border-b border-slate-100">
<div>
<span class="text-xs font-bold text-slate-400 block mb-1">نتیجه استعلام پرونده</span>
<h3 class="text-xl font-black text-slate-900">${issue.title}</h3>
</div>
<div class="flex items-center gap-2">
<span class="font-mono text-sm font-bold bg-slate-900 text-emerald-400 px-3 py-1.5 rounded-xl">
${issue.tracking_code}
</span>
<span class="text-xs font-bold px-3 py-1.5 rounded-xl ${st.color} border">
${st.label}
</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-200">
<span class="text-slate-400 block mb-1">دسته‌بندی موضوع:</span>
<span class="font-bold text-slate-800 flex items-center gap-1.5"><i class="fa-solid ${cat.icon} ${cat.color}"></i> ${issue.category}</span>
</div>
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-200">
<span class="text-slate-400 block mb-1">منطقه شهرداری:</span>
<span class="font-bold text-slate-800">منطقه ${issue.district}</span>
</div>
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-200">
<span class="text-slate-400 block mb-1">تاریخ و ساعت ثبت:</span>
<span class="font-bold text-slate-800">${issue.created_at}</span>
</div>
</div>
${issue.official_response ? `
<div class="bg-emerald-50 border-2 border-emerald-300 p-5 rounded-2xl space-y-1.5">
<h4 class="text-xs font-bold text-emerald-800 flex items-center gap-1.5">
<i class="fa-solid fa-building-flag"></i> پاسخ رسمی بازرس و اکیپ شهرداری:
</h4>
<p class="text-xs sm:text-sm text-emerald-950 leading-relaxed font-medium">${issue.official_response}</p>
</div>
` : ''}
<div class="space-y-4">
<h4 class="text-sm font-bold text-slate-900">مراحل و اقدامات انجام‌شده:</h4>
<div class="space-y-3">
${(issue.timeline || []).map(tl => `
<div class="flex items-start gap-3 bg-slate-50 p-3.5 rounded-2xl border border-slate-200">
<div class="w-8 h-8 rounded-xl bg-emerald-600 text-white flex items-center justify-center text-xs shrink-0 mt-0.5">
<i class="fa-solid fa-check"></i>
</div>
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="font-bold text-xs text-slate-900">${tl.title}</span>
<span class="text-[10px] text-slate-400">${tl.created_at}</span>
</div>
<p class="text-xs text-slate-600">${tl.description}</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
} catch (err) {
container.innerHTML = `<p class="text-xs text-rose-500">خطا در برقراری ارتباط با سرور.</p>`;
}
}
// 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 = `<i class="fa-solid fa-circle-notch animate-spin"></i> در حال ثبت...`;
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: `
<div class="p-4 bg-slate-50 rounded-2xl border border-slate-200 text-center space-y-2 mt-3">
<span class="text-xs text-slate-500">کد رهگیری اختصاصی شما:</span>
<div class="font-mono text-xl font-black text-emerald-600 bg-white py-2 px-4 rounded-xl border-2 border-dashed border-emerald-400">${created.tracking_code}</div>
<p class="text-xs text-slate-500 leading-relaxed">این کد را جهت استعلام‌های بعدی و پیگیری مراحل یادداشت نمایید.</p>
</div>
`,
confirmButtonText: 'مشاهده گزارش در صفحه اصلی',
confirmButtonColor: '#059669'
});
loadStats();
fetchIssues();
}
} catch (err) {
Swal.fire({ icon: 'error', title: 'خطا در ثبت گزارش', text: 'لطفاً دوباره تلاش کنید.' });
} finally {
btn.disabled = false;
btn.innerHTML = `<i class="fa-solid fa-paper-plane"></i> <span>ارسال و ثبت نهایی گزارش</span>`;
}
}
// ADMIN DASHBOARD SECTION
async function loadAdminIssues() {
const tbody = document.getElementById('adminTableBody');
const countEl = document.getElementById('adminTableCount');
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-slate-400">در حال بارگذاری اطلاعات پرونده‌ها...</td></tr>`;
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 = `
<td class="py-3 px-4 font-mono font-bold text-emerald-700">${issue.tracking_code}</td>
<td class="py-3 px-4">
<div class="font-bold text-slate-900">${issue.title}</div>
<div class="text-[11px] text-slate-400 truncate max-w-xs">${issue.address}</div>
</td>
<td class="py-3 px-4 font-semibold">${issue.category}</td>
<td class="py-3 px-4">منطقه ${issue.district}</td>
<td class="py-3 px-4"><span class="px-2 py-0.5 rounded-full text-[10px] font-bold ${prio.badge}">${prio.label}</span></td>
<td class="py-3 px-4"><span class="px-2 py-0.5 rounded-full text-[10px] font-bold ${st.color} border">${st.label}</span></td>
<td class="py-3 px-4 text-slate-400">${issue.created_at}</td>
<td class="py-3 px-4 text-center">
<button onclick="openAdminActionModal(${issue.id}, '${issue.status}')" class="bg-slate-900 hover:bg-emerald-600 text-white px-3 py-1.5 rounded-lg text-xs font-bold transition-colors">
دستور کار / تغییر وضعیت
</button>
</td>
`;
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' }
}
}
});
}
}