feat: complete modern urban issues management platform (ShahrNegar)

This commit is contained in:
Antigravity Bot
2026-08-30 01:54:03 +03:30
parent 8dee99de1b
commit 06e1266662
5 changed files with 2340 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
import sqlite3
import os
import random
from datetime import datetime, timedelta
DB_PATH = os.path.join(os.path.dirname(__file__), "urban_issues.db")
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
cursor = conn.cursor()
# Issues Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS issues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tracking_code TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT NOT NULL,
address TEXT NOT NULL,
district INTEGER NOT NULL,
priority TEXT NOT NULL, -- 'low', 'medium', 'high', 'emergency'
status TEXT NOT NULL, -- 'pending', 'reviewing', 'in_progress', 'resolved', 'rejected'
lat REAL NOT NULL,
lng REAL NOT NULL,
image_url TEXT,
resolved_image_url TEXT,
reporter_name TEXT,
reporter_phone TEXT,
official_response TEXT,
upvotes INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
resolved_at TEXT
)
""")
# Comments Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)
""")
# Timeline Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS timeline (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
status TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)
""")
# Check if empty, then seed initial realistic data
cursor.execute("SELECT COUNT(*) FROM issues")
count = cursor.fetchone()[0]
if count == 0:
seed_data(cursor)
conn.commit()
conn.close()
def seed_data(cursor):
now = datetime.now()
sample_issues = [
{
"tracking_code": "SHR-78214",
"title": "فرونشست و چاله عمیق در لاین کندرو",
"category": "آسفالت و معابر",
"description": "وجود چاله عمیق با قطر حدود ۱ متر پس از بارندگی اخیر که باعث خسارت به لاستیک خودروها و ترمزهای ناگهانی خطرناک می‌شود.",
"address": "بزرگراه شهید همت، نرسیده به خروجی گاندی، لاین کندرو",
"district": 3,
"priority": "emergency",
"status": "in_progress",
"lat": 35.7538,
"lng": 51.4172,
"image_url": "https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "علی رضایی",
"reporter_phone": "09121112233",
"official_response": "اکیپ آسفالت‌ریزی ناحیه ۲ شهرداری منطقه ۳ اعزام گردیده و عملیات زیرسازی و لکه‌گیری در حال انجام است.",
"upvotes": 42,
"days_ago": 2,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش توسط شهروند", "گزارش در سامانه ۱۳۷ با موفقیت ثبت شد."),
("reviewing", "تایید کارشناس و ارجاع به معاونت فنی و عمران", "موضوع به ناحیه ۲ ارجاع داده شد."),
("in_progress", "اعزام اکیپ عملیاتی لکه‌گیری", "تیم فنی با تجهیزات در محل مستقر شد.")
],
"comments": [
("سارا محمدی", "دیروز لاستیک ماشین من هم اینجا آسیب دید، ممنون که پیگیری می‌کنید."),
("حسین کاظمی", "امیدوارم امشب تا قبل از ترافیک صبحگاهی تموم بشه.")
]
},
{
"tracking_code": "SHR-65109",
"title": "خاموشی کامل پایه چراغ‌های روشنایی بوستان",
"category": "روشنایی و برق",
"description": "کل مسیر پیاده‌روی شرقی بوستان ملت در تاریکی مطلق است که باعث کاهش امنیت خانواده‌ها و ورزشکاران شبانه شده است.",
"address": "خیابان ولیعصر، بوستان ملت، ضلع شرقی جنب دریاچه",
"district": 3,
"priority": "high",
"status": "resolved",
"lat": 35.7801,
"lng": 51.4116,
"image_url": "https://images.unsplash.com/photo-1509114397022-ed747cca3f65?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": "https://images.unsplash.com/photo-1517457373958-b7bdd4587205?auto=format&fit=crop&w=800&q=80",
"reporter_name": "مریم احمدی",
"reporter_phone": "09123334455",
"official_response": "کابل‌کشی زیرزمینی و تعویض پروژکتورهای معیوب توسط اداره زیباسازی و تاسیسات منطقه انجام و روشنایی کامل برقرار گردید.",
"upvotes": 68,
"days_ago": 5,
"resolved_at": (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M"),
"timeline": [
("pending", "ثبت گزارش توسط شهروند", "گزارش دریافت شد."),
("reviewing", "بررسی میدانی اداره تاسیسات", "نقص کابل اصلی تایید شد."),
("in_progress", "تعمیرات و تعویض کابل", "تیم تاسیسات در حال اجرای خط جدید."),
("resolved", "تکمیل و رفع نقص روشنایی", "چراغ‌ها روشن و مدار به طور کامل تست شد.")
],
"comments": [
("امیر رستمی", "خیلی سریع درستش کردن، واقعا تشکر از عوامل شهرداری."),
("نگار توکلی", "دیشب رفتم پارک، همه چراغ‌ها درست شده بود.")
]
},
{
"tracking_code": "SHR-91203",
"title": "انباشت پسماند و سرریز مخزن زباله شهری",
"category": "نظافت و پسماند",
"description": "مخزن مکانیزه زباله شکسته شده و زباله‌ها در پیاده‌رو پخش شده که باعث بوی نامطبوع و تجمع حشرات شده است.",
"address": "خیابان آزادی، تقاطع خیابان استاد معین، پلاک ۴۲",
"district": 9,
"priority": "medium",
"status": "pending",
"lat": 35.6997,
"lng": 51.3486,
"image_url": "https://images.unsplash.com/photo-1532996122724-e3c354a0b15b?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "سعید کریمی",
"reporter_phone": "09355556677",
"official_response": "گزارش در نوبت بررسی ناظر پسماند منطقه قرار گرفته است.",
"upvotes": 19,
"days_ago": 1,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش در سامانه", "منتظر بازرسی و تخصیص سطل جدید مکانیزه.")
],
"comments": [
("مهدی بهرامی", "لطفا سطل‌های پدال‌دار بگذارید که درش بسته بمونه.")
]
},
{
"tracking_code": "SHR-44390",
"title": "آسیب دیدگی و شکستن شاخه‌های درخت کهنسال بر اثر طوفان",
"category": "فضای سبز و بوستان‌ها",
"description": "شاخه‌های سنگین درخت چنار شکسته و روی سیم‌های برق و پیاده‌رو معلق مانده است، احتمال سقوط روی عابرین وجود دارد.",
"address": "خیابان شریعتی، بالاتر از پل رومی، نبش کوچه یاس",
"district": 1,
"priority": "emergency",
"status": "in_progress",
"lat": 35.7985,
"lng": 51.4332,
"image_url": "https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "پویا سرمدی",
"reporter_phone": "09124445566",
"official_response": "اکیپ سازمان بوستان‌ها و آتش‌نشانی با جرثقیل در حال هرس ایمن و رفع خطر هستند.",
"upvotes": 54,
"days_ago": 1,
"resolved_at": None,
"timeline": [
("pending", "ثبت فوری گزارش خطر", "هشدار سقوط شاخه دریافت شد."),
("reviewing", "هماهنگی با سازمان آتش‌نشانی و فضای سبز", "اعلام وضعیت اضطراری."),
("in_progress", "عملیات رفع خطر و هرس", "حصارکشی پیاده‌رو و استقرار بالابر.")
],
"comments": [
("رویا صبوری", "خدا رو شکر سریع اومدن نوار خطر کشیدن.")
]
},
{
"tracking_code": "SHR-33219",
"title": "سد معبر طولانی مصالح ساختمانی و تخریب پیاده‌رو",
"category": "ساختمان‌سازی و سد معبر",
"description": "یک پروژه ساختمانی بیش از سه هفته است که تمام پیاده‌رو و بخشی از خیابان را با نخاله و داربست بدون راه عبور ایمن مسدود کرده است.",
"address": "سعادت‌آباد، میدان کاج، خیابان مروارید، پلاک ۱۸",
"district": 2,
"priority": "high",
"status": "reviewing",
"lat": 35.7794,
"lng": 51.3758,
"image_url": "https://images.unsplash.com/photo-1541888946425-d0fbb18086f6?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "فرشید نادری",
"reporter_phone": "09127778899",
"official_response": "اخطاریه ماده ۱۰۰ و رفع سد معبر برای مالک صادر شد. مهلت رفع ۴۸ ساعت می‌باشد.",
"upvotes": 37,
"days_ago": 3,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش تخلف ساختمانی", "ثبت شد."),
("reviewing", "بازدید مامور اجرای احکام شهرداری", "صدور اخطاریه رسمی رفع انسداد معبر.")
],
"comments": [
("کامران شمس", "مادر من با ویلچر مجبور شد بره توی خیابان که خیلی خطرناک بود!")
]
},
{
"tracking_code": "SHR-55821",
"title": "خرابی و چشمک‌زن ماندن چراغ راهنمایی تقاطع پرتردد",
"category": "ترافیک و حمل‌ونقل",
"description": "چراغ راهنمایی تقاطع به مدت دو روز چشمک‌زن زرد مانده که باعث گره کور ترافیکی و تصادفات مکرر شده است.",
"address": "میدان فاطمی (جهاد)، تقاطع خیابان جویبار و شهید گمنام",
"district": 6,
"priority": "high",
"status": "resolved",
"lat": 35.7208,
"lng": 51.4082,
"image_url": "https://images.unsplash.com/photo-1517649763962-0c623266ddc0?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": "https://images.unsplash.com/photo-1508873696983-2df5703bc275?auto=format&fit=crop&w=800&q=80",
"reporter_name": "ندا زمانی",
"reporter_phone": "09191113355",
"official_response": "برد الکترونیکی کنترلر چراغ توسط شرکت کنترل ترافیک تهران تعویض و زمان‌بندی هوشمند مجدداً فعال گردید.",
"upvotes": 85,
"days_ago": 4,
"resolved_at": (now - timedelta(days=2)).strftime("%Y-%m-%d %H:%M"),
"timeline": [
("pending", "ثبت گزارش خرابی علائم ترافیکی", "گزارش دریافت گردید."),
("reviewing", "ارجاع به شرکت کنترل ترافیک", "تایید خرابی سخت‌افزاری کنترلر."),
("in_progress", "تعویض برد فرمان و سنسورها", "عملیات فنی در محل انجام شد."),
("resolved", "راه‌اندازی و اتصال به مرکز کنترل", "چراغ طبق برنامه زمان‌بندی هوشمند شروع به کار کرد.")
],
"comments": [
("سامان یوسفی", "دست مریزاد، امروز صبح ترافیک خیلی روان‌تر بود.")
]
}
]
for item in sample_issues:
created_time = (now - timedelta(days=item["days_ago"], hours=random.randint(1, 10))).strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, resolved_image_url, reporter_name, reporter_phone,
official_response, upvotes, created_at, resolved_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
item["tracking_code"], item["title"], item["category"], item["description"],
item["address"], item["district"], item["priority"], item["status"],
item["lat"], item["lng"], item["image_url"], item["resolved_image_url"],
item["reporter_name"], item["reporter_phone"], item["official_response"],
item["upvotes"], created_time, item["resolved_at"]
))
issue_id = cursor.lastrowid
for tl_status, tl_title, tl_desc in item["timeline"]:
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, ?, ?, ?, ?)
""", (issue_id, tl_status, tl_title, tl_desc, created_time))
for author, comment_text in item["comments"]:
cursor.execute("""
INSERT INTO comments (issue_id, author_name, content, created_at)
VALUES (?, ?, ?, ?)
""", (issue_id, author, comment_text, created_time))
+338
View File
@@ -0,0 +1,338 @@
import os
import uuid
import random
from datetime import datetime
from typing import Optional, List
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import shutil
from database import get_db_connection, init_db
app = FastAPI(title="سامانه هوشمند مدیریت مسائل شهری - شهرنگار")
# CORS setup
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
UPLOAD_DIR = os.path.join(os.path.dirname(__file__), "static", "uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Initialize database on startup
@app.on_event("startup")
def on_startup():
init_db()
# Pydantic models
class IssueCreate(BaseModel):
title: str
category: str
description: str
address: str
district: int
priority: str = "medium"
lat: float = 35.6892
lng: float = 51.3890
image_url: Optional[str] = None
reporter_name: Optional[str] = "شهروند ناشناس"
reporter_phone: Optional[str] = None
class CommentCreate(BaseModel):
author_name: str
content: str
class StatusUpdate(BaseModel):
status: str
official_response: Optional[str] = None
timeline_title: Optional[str] = None
timeline_desc: Optional[str] = None
resolved_image_url: Optional[str] = None
# Routes
@app.get("/api/stats")
def get_stats():
conn = get_db_connection()
cursor = conn.cursor()
total = cursor.execute("SELECT COUNT(*) FROM issues").fetchone()[0]
pending = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'pending'").fetchone()[0]
reviewing = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'reviewing'").fetchone()[0]
in_progress = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'in_progress'").fetchone()[0]
resolved = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'resolved'").fetchone()[0]
upvotes = cursor.execute("SELECT COALESCE(SUM(upvotes), 0) FROM issues").fetchone()[0]
# Category counts
cat_rows = cursor.execute("SELECT category, COUNT(*) as cnt FROM issues GROUP BY category ORDER BY cnt DESC").fetchall()
categories = [{"category": row["category"], "count": row["cnt"]} for row in cat_rows]
# District counts
dist_rows = cursor.execute("SELECT district, COUNT(*) as cnt FROM issues GROUP BY district ORDER BY district ASC").fetchall()
districts = [{"district": row["district"], "count": row["cnt"]} for row in dist_rows]
# Priority counts
prio_rows = cursor.execute("SELECT priority, COUNT(*) as cnt FROM issues GROUP BY priority").fetchall()
priorities = {row["priority"]: row["cnt"] for row in prio_rows}
conn.close()
return {
"total": total,
"pending": pending,
"reviewing": reviewing,
"in_progress": in_progress,
"resolved": resolved,
"total_upvotes": upvotes,
"categories": categories,
"districts": districts,
"priorities": priorities,
"resolution_rate": round((resolved / total * 100) if total > 0 else 0, 1)
}
@app.get("/api/issues")
def list_issues(
category: Optional[str] = None,
status: Optional[str] = None,
district: Optional[int] = None,
priority: Optional[str] = None,
search: Optional[str] = None,
sort_by: Optional[str] = "newest"
):
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT * FROM issues WHERE 1=1"
params = []
if category and category != "all":
query += " AND category = ?"
params.append(category)
if status and status != "all":
query += " AND status = ?"
params.append(status)
if district and district > 0:
query += " AND district = ?"
params.append(district)
if priority and priority != "all":
query += " AND priority = ?"
params.append(priority)
if search:
query += " AND (title LIKE ? OR description LIKE ? OR address LIKE ? OR tracking_code LIKE ?)"
s_pattern = f"%{search}%"
params.extend([s_pattern, s_pattern, s_pattern, s_pattern])
if sort_by == "upvotes":
query += " ORDER BY upvotes DESC, id DESC"
elif sort_by == "oldest":
query += " ORDER BY id ASC"
elif sort_by == "emergency":
query += " ORDER BY CASE priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, id DESC"
else: # newest
query += " ORDER BY id DESC"
rows = cursor.execute(query, params).fetchall()
issues = []
for r in rows:
item = dict(r)
# Fetch comment count
c_count = cursor.execute("SELECT COUNT(*) FROM comments WHERE issue_id = ?", (r["id"],)).fetchone()[0]
item["comment_count"] = c_count
issues.append(item)
conn.close()
return issues
@app.get("/api/issues/{issue_id}")
def get_issue(issue_id: int):
conn = get_db_connection()
cursor = conn.cursor()
issue_row = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not issue_row:
conn.close()
raise HTTPException(status_code=404, detail="گزارش مورد نظر یافت نشد.")
issue = dict(issue_row)
# Timeline
timeline_rows = cursor.execute("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC", (issue_id,)).fetchall()
issue["timeline"] = [dict(t) for t in timeline_rows]
# Comments
comment_rows = cursor.execute("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC", (issue_id,)).fetchall()
issue["comments"] = [dict(c) for c in comment_rows]
conn.close()
return issue
@app.get("/api/track/{tracking_code}")
def track_issue(tracking_code: str):
conn = get_db_connection()
cursor = conn.cursor()
code = tracking_code.strip().upper()
issue_row = cursor.execute("SELECT * FROM issues WHERE UPPER(tracking_code) = ?", (code,)).fetchone()
if not issue_row:
conn.close()
raise HTTPException(status_code=404, detail=f"هیچ گزارشی با کد رهگیری «{tracking_code}» یافت نشد.")
issue_id = issue_row["id"]
issue = dict(issue_row)
timeline_rows = cursor.execute("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC", (issue_id,)).fetchall()
issue["timeline"] = [dict(t) for t in timeline_rows]
comment_rows = cursor.execute("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC", (issue_id,)).fetchall()
issue["comments"] = [dict(c) for c in comment_rows]
conn.close()
return issue
@app.post("/api/issues")
def create_issue(payload: IssueCreate):
conn = get_db_connection()
cursor = conn.cursor()
# Generate unique tracking code
rand_num = random.randint(10000, 99999)
tracking_code = f"SHR-{rand_num}"
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, reporter_name, reporter_phone, upvotes, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, 1, ?)
""", (
tracking_code, payload.title, payload.category, payload.description, payload.address,
payload.district, payload.priority, payload.lat, payload.lng, payload.image_url,
payload.reporter_name or "شهروند", payload.reporter_phone or "", now_str
))
issue_id = cursor.lastrowid
# Add initial timeline event
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, 'pending', 'ثبت اولیه گزارش در سامانه', 'گزارش شما با موفقیت در سامانه شهرنگار ثبت گردید و در نوبت بررسی کارشناسی قرار گرفت.', ?)
""", (issue_id, now_str))
conn.commit()
created = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
res = dict(created)
conn.close()
return res
@app.post("/api/issues/{issue_id}/upvote")
def upvote_issue(issue_id: int):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("UPDATE issues SET upvotes = upvotes + 1 WHERE id = ?", (issue_id,))
if cursor.rowcount == 0:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
conn.commit()
new_upvotes = cursor.execute("SELECT upvotes FROM issues WHERE id = ?", (issue_id,)).fetchone()[0]
conn.close()
return {"id": issue_id, "upvotes": new_upvotes}
@app.post("/api/issues/{issue_id}/comment")
def add_comment(issue_id: int, payload: CommentCreate):
conn = get_db_connection()
cursor = conn.cursor()
check = cursor.execute("SELECT id FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not check:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO comments (issue_id, author_name, content, created_at)
VALUES (?, ?, ?, ?)
""", (issue_id, payload.author_name or "شهروند محترم", payload.content, now_str))
conn.commit()
comment_id = cursor.lastrowid
comment = cursor.execute("SELECT * FROM comments WHERE id = ?", (comment_id,)).fetchone()
conn.close()
return dict(comment)
@app.patch("/api/issues/{issue_id}/status")
def update_issue_status(issue_id: int, payload: StatusUpdate):
conn = get_db_connection()
cursor = conn.cursor()
check = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not check:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
resolved_at = now_str if payload.status == "resolved" else check["resolved_at"]
# Update issue
cursor.execute("""
UPDATE issues
SET status = ?,
official_response = COALESCE(?, official_response),
resolved_image_url = COALESCE(?, resolved_image_url),
resolved_at = ?
WHERE id = ?
""", (payload.status, payload.official_response, payload.resolved_image_url, resolved_at, issue_id))
# Add timeline entry if requested or automatically
status_titles = {
"pending": "در صف بررسی مجدد",
"reviewing": "بررسی کارشناسی و ارجاع به معاونت مربوطه",
"in_progress": "اعزام اکیپ اجرایی و آغاز عملیات میدانی",
"resolved": "اتمام عملیات و رفع کامل مسئله",
"rejected": "عدم احراز یا خارج از حیطه اختیارات شهرداری"
}
tl_title = payload.timeline_title or status_titles.get(payload.status, f"تغییر وضعیت به {payload.status}")
tl_desc = payload.timeline_desc or payload.official_response or "وضعیت پرونده توسط مدیریت سامانه به‌روزرسانی شد."
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, ?, ?, ?, ?)
""", (issue_id, payload.status, tl_title, tl_desc, now_str))
conn.commit()
updated = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
res = dict(updated)
conn.close()
return res
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
filename = f"{uuid.uuid4().hex[:10]}_{file.filename}"
filepath = os.path.join(UPLOAD_DIR, filename)
with open(filepath, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {"url": f"/static/uploads/{filename}"}
# Serve frontend static files
app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static")
@app.get("/")
def serve_index():
from fastapi.responses import FileResponse
return FileResponse(os.path.join(os.path.dirname(__file__), "static", "index.html"))
+959
View File
@@ -0,0 +1,959 @@
// 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' }
}
}
});
}
}
+763
View File
@@ -0,0 +1,763 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>شهرنگار | سامانه جامع مدیریت و رسیدگی به مسائل شهری</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
vazir: ['Vazirmatn', 'sans-serif'],
},
colors: {
brand: {
50: '#ecfdf5',
100: '#d1fae5',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
},
city: {
blue: '#2563eb',
dark: '#0f172a',
card: '#1e293b'
}
}
}
}
}
</script>
<!-- Google Font Vazirmatn -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Leaflet CSS & JS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body {
font-family: 'Vazirmatn', sans-serif;
}
.leaflet-popup-content-wrapper {
border-radius: 12px;
direction: rtl;
font-family: 'Vazirmatn', sans-serif;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f5f9;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.pulse-dot {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(1.15); }
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 antialiased min-h-screen flex flex-col">
<!-- Top Announcement Bar -->
<div class="bg-gradient-to-r from-emerald-700 via-teal-700 to-cyan-800 text-white text-xs py-2 px-4 text-center font-medium flex items-center justify-between shadow-inner">
<div class="flex items-center gap-2 mx-auto">
<span class="bg-white/20 px-2 py-0.5 rounded-full text-[11px] animate-pulse">سامانه برخط ۱۳۷</span>
<span>همراهی شما برای شهری زیباتر، ایمن‌تر و پاک‌تر • پاسخگویی ۲۴ ساعته</span>
</div>
<div class="hidden md:flex items-center gap-4 text-emerald-100">
<span id="currentDateTime"><i class="fa-regular fa-clock ml-1"></i> امروز شنبه</span>
<span>|</span>
<span><i class="fa-solid fa-phone-volume ml-1"></i> تلفن ارتباط مردمی: ۱۳۷</span>
</div>
</div>
<!-- Main Navigation -->
<header class="bg-white border-b border-slate-200 sticky top-0 z-40 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-20">
<!-- Logo & Title -->
<div class="flex items-center gap-3 cursor-pointer" onclick="switchTab('feed')">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-emerald-600 to-teal-500 flex items-center justify-center text-white shadow-lg shadow-emerald-500/20 text-2xl">
<i class="fa-solid fa-city"></i>
</div>
<div>
<div class="flex items-center gap-2">
<span class="text-2xl font-black bg-gradient-to-l from-emerald-700 to-teal-600 bg-clip-text text-transparent">شهرنگار</span>
<span class="text-[10px] uppercase tracking-wider bg-emerald-100 text-emerald-800 font-bold px-2 py-0.5 rounded">نسخه هوشمند</span>
</div>
<p class="text-xs text-slate-500 font-medium">سامانه یکپارچه گزارش و نظارت مردمی بر مسائل شهری</p>
</div>
</div>
<!-- Desktop Navigation Tabs -->
<nav class="hidden md:flex items-center gap-1 bg-slate-100/80 p-1.5 rounded-2xl border border-slate-200/80">
<button onclick="switchTab('feed')" id="nav-feed" class="nav-btn px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-200 bg-white text-emerald-700 shadow-sm">
<i class="fa-solid fa-list-check ml-1.5"></i>گزارش‌های مردمی
</button>
<button onclick="switchTab('map')" id="nav-map" class="nav-btn px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-200 text-slate-600 hover:text-emerald-600">
<i class="fa-solid fa-map-location-dot ml-1.5"></i>نقشه تعاملی
</button>
<button onclick="switchTab('track')" id="nav-track" class="nav-btn px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-200 text-slate-600 hover:text-emerald-600">
<i class="fa-solid fa-magnifying-glass ml-1.5"></i>پیگیری گزارش
</button>
<button onclick="switchTab('admin')" id="nav-admin" class="nav-btn px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-200 text-slate-600 hover:text-emerald-600">
<i class="fa-solid fa-chart-pie ml-1.5"></i>داشبورد مدیریت
</button>
</nav>
<!-- Call to Action Button -->
<div class="flex items-center gap-3">
<button onclick="openNewReportModal()" class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white px-4 sm:px-6 py-2.5 rounded-xl font-bold text-sm shadow-md shadow-emerald-600/20 hover:shadow-lg transition-all duration-200 flex items-center gap-2">
<i class="fa-solid fa-circle-plus text-base"></i>
<span>ثبت گزارش مشکل</span>
</button>
</div>
</div>
</div>
<!-- Mobile Nav Bar -->
<div class="md:hidden flex items-center justify-around border-t border-slate-200 py-2 bg-slate-50 text-xs">
<button onclick="switchTab('feed')" class="flex flex-col items-center gap-1 text-emerald-700 font-bold">
<i class="fa-solid fa-list-check text-base"></i>
<span>گزارش‌ها</span>
</button>
<button onclick="switchTab('map')" class="flex flex-col items-center gap-1 text-slate-600">
<i class="fa-solid fa-map-location-dot text-base"></i>
<span>نقشه</span>
</button>
<button onclick="switchTab('track')" class="flex flex-col items-center gap-1 text-slate-600">
<i class="fa-solid fa-magnifying-glass text-base"></i>
<span>پیگیری</span>
</button>
<button onclick="switchTab('admin')" class="flex flex-col items-center gap-1 text-slate-600">
<i class="fa-solid fa-chart-pie text-base"></i>
<span>مدیریت</span>
</button>
</div>
</header>
<!-- Hero & Quick Search Section -->
<section class="bg-gradient-to-b from-white to-slate-100 border-b border-slate-200 py-8 px-4 sm:px-6">
<div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-center">
<!-- Left Banner / Text -->
<div class="lg:col-span-7 space-y-4">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-100 text-emerald-800 text-xs font-semibold">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-ping"></span>
<span>صدای شما، اقدام سریع شهرداری و بازرسان شهری</span>
</div>
<h1 class="text-2xl sm:text-4xl font-extrabold text-slate-900 leading-tight">
شهر خود را با مشارکت هوشمند، <br class="hidden sm:inline">
<span class="text-emerald-600">زیباتر و امن‌تر</span> بسازید
</h1>
<p class="text-slate-600 text-sm sm:text-base leading-relaxed max-w-2xl">
خرابی آسفالت، سد معبر، نقص روشنایی پارک‌ها، انباشت پسماند یا مشکلات ترافیکی را در کمتر از ۱ دقیقه گزارش کنید و وضعیت رسیدگی را مرحله به مرحله رصد نمایید.
</p>
<!-- Quick Search / Track Form -->
<div class="pt-2 flex flex-col sm:flex-row gap-2 max-w-xl">
<div class="relative flex-1">
<i class="fa-solid fa-hashtag absolute right-3.5 top-3.5 text-slate-400 text-sm"></i>
<input type="text" id="quickTrackInput" placeholder="کد رهگیری (مثلاً SHR-78214) یا عبارت جستجو..." class="w-full pr-10 pl-4 py-3 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-200 outline-none text-sm font-medium transition-all shadow-sm">
</div>
<button onclick="handleQuickSearch()" class="bg-slate-900 hover:bg-slate-800 text-white px-6 py-3 rounded-xl font-bold text-sm transition-all shadow-sm flex items-center justify-center gap-2">
<i class="fa-solid fa-search"></i>
<span>جستجو و پیگیری</span>
</button>
</div>
</div>
<!-- Right: Real-time Stats Cards -->
<div class="lg:col-span-5 grid grid-cols-2 gap-3">
<div class="bg-white p-4 rounded-2xl border border-slate-200/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between text-slate-400 mb-2">
<span class="text-xs font-semibold text-slate-500">کل موارد ثبت‌شده</span>
<div class="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center text-sm">
<i class="fa-solid fa-file-lines"></i>
</div>
</div>
<div class="text-2xl sm:text-3xl font-black text-slate-900" id="stat-total">--</div>
<div class="text-[11px] text-slate-400 mt-1">گزارش‌های شهروندی</div>
</div>
<div class="bg-white p-4 rounded-2xl border border-slate-200/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between text-slate-400 mb-2">
<span class="text-xs font-semibold text-emerald-600">مسائل رفع شده</span>
<div class="w-8 h-8 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center text-sm">
<i class="fa-solid fa-circle-check"></i>
</div>
</div>
<div class="text-2xl sm:text-3xl font-black text-emerald-600" id="stat-resolved">--</div>
<div class="text-[11px] text-emerald-600/80 mt-1 font-medium" id="stat-rate">نرخ تکمیل: --٪</div>
</div>
<div class="bg-white p-4 rounded-2xl border border-slate-200/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between text-slate-400 mb-2">
<span class="text-xs font-semibold text-amber-600">در حال عملیات و اجرا</span>
<div class="w-8 h-8 rounded-lg bg-amber-50 text-amber-600 flex items-center justify-center text-sm">
<i class="fa-solid fa-screwdriver-wrench"></i>
</div>
</div>
<div class="text-2xl sm:text-3xl font-black text-amber-600" id="stat-in-progress">--</div>
<div class="text-[11px] text-slate-400 mt-1">اکیپ مستقر در محل</div>
</div>
<div class="bg-white p-4 rounded-2xl border border-slate-200/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between text-slate-400 mb-2">
<span class="text-xs font-semibold text-purple-600">تایید و همراهی شهروندان</span>
<div class="w-8 h-8 rounded-lg bg-purple-50 text-purple-600 flex items-center justify-center text-sm">
<i class="fa-solid fa-thumbs-up"></i>
</div>
</div>
<div class="text-2xl sm:text-3xl font-black text-purple-600" id="stat-upvotes">--</div>
<div class="text-[11px] text-slate-400 mt-1">رأی و حمایت مردمی</div>
</div>
</div>
</div>
</div>
</section>
<!-- Categories Horizontal Bar -->
<div class="bg-white border-b border-slate-200 py-3 px-4 shadow-sm overflow-x-auto custom-scrollbar">
<div class="max-w-7xl mx-auto flex items-center gap-2 min-w-max">
<span class="text-xs font-bold text-slate-400 ml-2">دسته‌بندی‌ها:</span>
<button onclick="filterByCategory('all')" class="cat-pill active px-3.5 py-1.5 rounded-full text-xs font-bold bg-slate-900 text-white transition-all">
همه موضوعات
</button>
<button onclick="filterByCategory('آسفالت و معابر')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-road text-amber-500"></i> آسفالت و چاله‌ها
</button>
<button onclick="filterByCategory('روشنایی و برق')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-lightbulb text-yellow-500"></i> روشنایی و تیر برق
</button>
<button onclick="filterByCategory('نظافت و پسماند')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-trash-can text-red-500"></i> نظافت و سطل زباله
</button>
<button onclick="filterByCategory('فضای سبز و بوستان‌ها')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-tree text-emerald-500"></i> بوستان و درختان
</button>
<button onclick="filterByCategory('ترافیک و حمل‌ونقل')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-traffic-light text-blue-500"></i> چراغ و علائم ترافیکی
</button>
<button onclick="filterByCategory('ساختمان‌سازی و سد معبر')" class="cat-pill px-3.5 py-1.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-emerald-50 hover:text-emerald-700 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-person-digging text-orange-500"></i> سد معبر و ساختمانی
</button>
</div>
</div>
<!-- Main Views Container -->
<main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- VIEW 1: PUBLIC FEED -->
<div id="view-feed" class="space-y-6">
<!-- Filters and Sorting Toolbar -->
<div class="bg-white p-4 rounded-2xl border border-slate-200 flex flex-wrap items-center justify-between gap-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<!-- District Filter -->
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate-500">منطقه شهرداری:</label>
<select id="filterDistrict" onchange="applyFilters()" class="text-xs bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 font-medium outline-none focus:ring-1 focus:ring-emerald-500">
<option value="0">تمام مناطق (۱ تا ۲۲)</option>
<option value="1">منطقه ۱ (شمرانات/تجریش)</option>
<option value="2">منطقه ۲ (سعادت‌آباد/شهرک غرب)</option>
<option value="3">منطقه ۳ (ونک/قلهک/میرداماد)</option>
<option value="4">منطقه ۴ (تهرانپارس/هروی)</option>
<option value="5">منطقه ۵ (صادقیه/پونک/جنت‌آباد)</option>
<option value="6">منطقه ۶ (یوسف‌آباد/امیرآباد/فاطمی)</option>
<option value="7">منطقه ۷ (سهروردی/مطهری)</option>
<option value="8">منطقه ۸ (نارمک/مجیدیه)</option>
<option value="9">منطقه ۹ (آزادی/استادمعین)</option>
<option value="10">منطقه ۱۰ (نواب/جیحون)</option>
<option value="11">منطقه ۱۱ (جمهوری/منیریه)</option>
<option value="12">منطقه ۱۲ (بازار/بهارستان)</option>
</select>
</div>
<!-- Status Filter -->
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate-500">وضعیت رسیدگی:</label>
<select id="filterStatus" onchange="applyFilters()" class="text-xs bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 font-medium outline-none focus:ring-1 focus:ring-emerald-500">
<option value="all">همه وضعیت‌ها</option>
<option value="pending">در صف بررسی اولیه</option>
<option value="reviewing">بررسی کارشناسی</option>
<option value="in_progress">در حال اجرای عملیات</option>
<option value="resolved">رفع و حل شده</option>
</select>
</div>
<!-- Priority Filter -->
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate-500">سطح فوریت:</label>
<select id="filterPriority" onchange="applyFilters()" class="text-xs bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 font-medium outline-none focus:ring-1 focus:ring-emerald-500">
<option value="all">همه اولویت‌ها</option>
<option value="emergency">🚨 فوری و خطرآفرین</option>
<option value="high">اولویت بالا</option>
<option value="medium">متوسط</option>
<option value="low">عادی</option>
</select>
</div>
</div>
<!-- Sort By -->
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate-500">مرتب‌سازی:</label>
<select id="sortBy" onchange="applyFilters()" class="text-xs bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 font-medium outline-none focus:ring-1 focus:ring-emerald-500">
<option value="newest">جدیدترین گزارش‌ها</option>
<option value="upvotes">بیشترین تایید شهروندی (محبوب‌ترین)</option>
<option value="emergency">بیشترین فوریت و خطر</option>
<option value="oldest">قدیمی‌ترین</option>
</select>
</div>
</div>
<!-- Issues Cards Grid -->
<div id="issuesContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Dynamic Cards rendered via JS -->
</div>
<!-- Empty State / Loading -->
<div id="feedLoading" class="py-16 text-center">
<div class="inline-block animate-spin text-4xl text-emerald-600 mb-3">
<i class="fa-solid fa-circle-notch"></i>
</div>
<p class="text-slate-500 text-sm font-medium">در حال بارگذاری گزارش‌های شهری...</p>
</div>
<div id="feedEmpty" class="hidden py-16 bg-white rounded-3xl border border-slate-200 text-center px-4">
<div class="w-16 h-16 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center mx-auto text-2xl mb-3">
<i class="fa-solid fa-magnifying-glass"></i>
</div>
<h3 class="text-lg font-bold text-slate-800 mb-1">موردی با این مشخصات یافت نشد</h3>
<p class="text-slate-500 text-sm mb-4">می‌توانید فیلترها را تغییر دهید یا خودتان اولین گزارش را ثبت کنید.</p>
<button onclick="openNewReportModal()" class="bg-emerald-600 text-white px-5 py-2 rounded-xl text-sm font-bold shadow hover:bg-emerald-700">
ثبت گزارش جدید
</button>
</div>
</div>
<!-- VIEW 2: INTERACTIVE MAP -->
<div id="view-map" class="hidden space-y-4">
<div class="bg-white p-4 rounded-2xl border border-slate-200 flex flex-wrap items-center justify-between gap-3 shadow-sm">
<div>
<h2 class="text-lg font-bold text-slate-900 flex items-center gap-2">
<i class="fa-solid fa-map-marked-alt text-emerald-600"></i>
<span>نقشه زنده و پراکندگی مسائل شهری</span>
</h2>
<p class="text-xs text-slate-500">برای مشاهده جزئیات، عکس‌ها و وضعیت هر موضوع، روی نشانگرهای نقشه کلیک کنید.</p>
</div>
<!-- Map Legend -->
<div class="flex flex-wrap items-center gap-3 text-xs">
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-full bg-amber-500"></span> در حال بررسی/اجرا</span>
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-full bg-emerald-500"></span> رفع شده</span>
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-full bg-red-500"></span> نیازمند اقدام فوری</span>
</div>
</div>
<!-- Map Container -->
<div class="bg-white p-2 rounded-3xl border border-slate-200 shadow-md">
<div id="leafletMap" class="w-full h-[600px] rounded-2xl z-10"></div>
</div>
</div>
<!-- VIEW 3: TRACKING PORTAL -->
<div id="view-track" class="hidden space-y-6">
<!-- Tracking Search Box -->
<div class="bg-gradient-to-br from-slate-900 via-slate-800 to-teal-950 text-white p-6 sm:p-10 rounded-3xl shadow-xl">
<div class="max-w-2xl mx-auto text-center space-y-4">
<div class="w-14 h-14 bg-emerald-500/20 text-emerald-400 rounded-2xl flex items-center justify-center mx-auto text-2xl">
<i class="fa-solid fa-route"></i>
</div>
<h2 class="text-2xl sm:text-3xl font-black">سامانه پیگیری و استعلام گزارش‌های شهری</h2>
<p class="text-slate-300 text-sm leading-relaxed">
با وارد کردن کد پیگیری (مانند <span class="font-mono bg-white/10 px-2 py-0.5 rounded text-emerald-300">SHR-78214</span>)، از آخرین وضعیت اقدامات شهرداری، اعزام اکیپ و تصاویر قبل و بعد مطلع شوید.
</p>
<div class="flex flex-col sm:flex-row gap-2 pt-2">
<input type="text" id="trackCodeInput" placeholder="کد رهگیری را اینجا وارد کنید..." class="flex-1 px-5 py-3.5 rounded-2xl bg-white/10 border border-white/20 text-white placeholder-slate-400 focus:bg-white/20 focus:border-emerald-400 outline-none text-center font-bold tracking-wider text-base">
<button onclick="fetchTrackingDetail()" class="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-black px-8 py-3.5 rounded-2xl transition-all shadow-lg shadow-emerald-500/30 flex items-center justify-center gap-2">
<i class="fa-solid fa-magnifying-glass"></i>
<span>استعلام وضعیت</span>
</button>
</div>
</div>
</div>
<!-- Tracking Result Container -->
<div id="trackResultContainer" class="hidden bg-white rounded-3xl border border-slate-200 p-6 sm:p-8 shadow-sm space-y-8">
<!-- Populated via JS -->
</div>
</div>
<!-- VIEW 4: ADMIN / MUNICIPALITY DASHBOARD -->
<div id="view-admin" class="hidden space-y-6">
<!-- Admin Header -->
<div class="bg-white p-6 rounded-3xl border border-slate-200 flex flex-wrap items-center justify-between gap-4 shadow-sm">
<div>
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-xs font-bold mb-2">
<i class="fa-solid fa-shield-halved"></i>
<span>مرکز کنترل و نظارت شهرداری (شهردید)</span>
</div>
<h2 class="text-xl sm:text-2xl font-black text-slate-900">میز خدمت کارشناسان و بازرسان شهری</h2>
<p class="text-xs sm:text-sm text-slate-500 mt-1">رسیدگی به تیکت‌های شهروندان، تغییر وضعیت، ثبت گزارش اکیپ اجرایی و صدور دستور کار</p>
</div>
<div class="flex items-center gap-3">
<button onclick="loadStats(); loadAdminIssues();" class="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2">
<i class="fa-solid fa-rotate"></i>
<span>بروزرسانی داده‌ها</span>
</button>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Categories Bar Chart -->
<div class="lg:col-span-8 bg-white p-6 rounded-3xl border border-slate-200 shadow-sm">
<h3 class="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
<i class="fa-solid fa-chart-column text-emerald-600"></i>
<span>توزیع موضوعی مسائل گزارش‌شده</span>
</h3>
<div class="h-64">
<canvas id="categoryChart"></canvas>
</div>
</div>
<!-- Status Doughnut Chart -->
<div class="lg:col-span-4 bg-white p-6 rounded-3xl border border-slate-200 shadow-sm">
<h3 class="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
<i class="fa-solid fa-chart-pie text-teal-600"></i>
<span>وضعیت پرونده‌ها</span>
</h3>
<div class="h-64 flex items-center justify-center">
<canvas id="statusChart"></canvas>
</div>
</div>
</div>
<!-- Admin Issues Table -->
<div class="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-5 border-b border-slate-100 flex flex-wrap items-center justify-between gap-3 bg-slate-50/50">
<h3 class="text-sm font-bold text-slate-900 flex items-center gap-2">
<i class="fa-solid fa-table-list text-slate-600"></i>
<span>جدول مدیریت و صدور دستور کار پرونده‌ها</span>
</h3>
<span class="text-xs text-slate-500" id="adminTableCount">در حال بارگذاری...</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-right text-xs">
<thead class="bg-slate-100/70 text-slate-600 font-bold border-b border-slate-200">
<tr>
<th class="py-3.5 px-4">کد رهگیری</th>
<th class="py-3.5 px-4">عنوان و موقعیت</th>
<th class="py-3.5 px-4">دسته‌بندی</th>
<th class="py-3.5 px-4">منطقه</th>
<th class="py-3.5 px-4">اولویت</th>
<th class="py-3.5 px-4">وضعیت جاری</th>
<th class="py-3.5 px-4">تاریخ ثبت</th>
<th class="py-3.5 px-4 text-center">عملیات بازرسی</th>
</tr>
</thead>
<tbody id="adminTableBody" class="divide-y divide-slate-100">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- MODAL 1: SUBMIT NEW REPORT -->
<div id="newReportModal" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-sm hidden flex items-center justify-center p-4 overflow-y-auto">
<div class="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-slate-100 custom-scrollbar">
<!-- Modal Header -->
<div class="bg-gradient-to-r from-emerald-700 to-teal-700 text-white p-6 sticky top-0 z-20 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center text-lg">
<i class="fa-solid fa-bullhorn"></i>
</div>
<div>
<h3 class="text-lg font-bold">ثبت گزارش جدید مشکل شهری</h3>
<p class="text-xs text-emerald-100">گزارش شما مستقیماً به شهرداری منطقه و سامانه ۱۳۷ ارسال خواهد شد</p>
</div>
</div>
<button onclick="closeNewReportModal()" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/25 flex items-center justify-center transition-all">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<!-- Modal Body Form -->
<form id="newReportForm" onsubmit="submitNewReport(event)" class="p-6 space-y-5">
<!-- Category Selector -->
<div>
<label class="block text-xs font-bold text-slate-700 mb-2">دسته‌بندی موضوع <span class="text-red-500">*</span></label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2" id="modalCategoryGrid">
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="آسفالت و معابر" checked class="hidden">
<i class="fa-solid fa-road text-amber-500 text-lg"></i>
<span class="text-xs font-semibold">آسفالت و معابر</span>
</label>
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="روشنایی و برق" class="hidden">
<i class="fa-solid fa-lightbulb text-yellow-500 text-lg"></i>
<span class="text-xs font-semibold">روشنایی و تیر برق</span>
</label>
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="نظافت و پسماند" class="hidden">
<i class="fa-solid fa-trash-can text-red-500 text-lg"></i>
<span class="text-xs font-semibold">نظافت و پسماند</span>
</label>
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="فضای سبز و بوستان‌ها" class="hidden">
<i class="fa-solid fa-tree text-emerald-500 text-lg"></i>
<span class="text-xs font-semibold">فضای سبز</span>
</label>
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="ترافیک و حمل‌ونقل" class="hidden">
<i class="fa-solid fa-traffic-light text-blue-500 text-lg"></i>
<span class="text-xs font-semibold">ترافیک و تابلوها</span>
</label>
<label class="category-radio border border-slate-200 rounded-xl p-3 flex flex-col items-center gap-1.5 cursor-pointer hover:border-emerald-500 transition-all text-center">
<input type="radio" name="category" value="ساختمان‌سازی و سد معبر" class="hidden">
<i class="fa-solid fa-person-digging text-orange-500 text-lg"></i>
<span class="text-xs font-semibold">سد معبر</span>
</label>
</div>
</div>
<!-- Title -->
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">عنوان خلاصه مشکل <span class="text-red-500">*</span></label>
<input type="text" id="reportTitle" required placeholder="مثال: چاله عمیق در تقاطع خیابان ولیعصر" class="w-full px-4 py-2.5 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 outline-none text-sm">
</div>
<!-- District & Priority Row -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">منطقه شهرداری <span class="text-red-500">*</span></label>
<select id="reportDistrict" required class="w-full px-4 py-2.5 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 outline-none text-sm">
<option value="1">منطقه ۱ (شمرانات/تجریش)</option>
<option value="2">منطقه ۲ (سعادت‌آباد/شهرک غرب)</option>
<option value="3" selected>منطقه ۳ (ونک/قلهک/میرداماد)</option>
<option value="4">منطقه ۴ (تهرانپارس/هروی)</option>
<option value="5">منطقه ۵ (صادقیه/پونک)</option>
<option value="6">منطقه ۶ (یوسف‌آباد/امیرآباد)</option>
<option value="7">منطقه ۷ (مطهری/سهروردی)</option>
<option value="8">منطقه ۸ (نارمک/مجیدیه)</option>
<option value="9">منطقه ۹ (آزادی/استادمعین)</option>
<option value="10">منطقه ۱۰ (نواب/جیحون)</option>
<option value="11">منطقه ۱۱ (جمهوری/منیریه)</option>
<option value="12">منطقه ۱۲ (بازار/بهارستان)</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">سطح فوریت و خطر</label>
<select id="reportPriority" class="w-full px-4 py-2.5 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 outline-none text-sm font-semibold">
<option value="low">🟢 عادی</option>
<option value="medium" selected>🟡 متوسط</option>
<option value="high">🟠 بالا</option>
<option value="emergency">🔴 فوری و خطر جانی/مالی</option>
</select>
</div>
</div>
<!-- Address -->
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">نشانی دقیق محل <span class="text-red-500">*</span></label>
<input type="text" id="reportAddress" required placeholder="خیابان، کوچه، پلاک یا نشانه بارز نزدیک محل..." class="w-full px-4 py-2.5 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 outline-none text-sm">
</div>
<!-- Interactive Map Location Picker -->
<div>
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-bold text-slate-700">تعیین دقیق موقعیت روی نقشه</label>
<span class="text-[11px] text-emerald-600 font-medium"><i class="fa-solid fa-location-crosshairs ml-1"></i>نشانگر را حرکت دهید یا روی نقشه کلیک کنید</span>
</div>
<div id="pickerMap" class="w-full h-44 rounded-xl border border-slate-300 z-10"></div>
<input type="hidden" id="reportLat" value="35.7538">
<input type="hidden" id="reportLng" value="51.4172">
</div>
<!-- Description -->
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">شرح کامل مشکل <span class="text-red-500">*</span></label>
<textarea id="reportDescription" rows="3" required placeholder="لطفاً جزئیات بیشتری درباره مشکل، ابعاد و خسارت‌های احتمالی بنویسید..." class="w-full px-4 py-2.5 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 outline-none text-sm"></textarea>
</div>
<!-- Photo Selection / Upload -->
<div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">تصویر مشکل (اختیاری)</label>
<div class="grid grid-cols-3 gap-2 mb-2">
<div onclick="selectSampleImage(this, 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80')" class="sample-img-card border-2 border-slate-200 rounded-xl overflow-hidden cursor-pointer hover:border-emerald-500 transition-all p-1 text-center">
<img src="https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=300&q=80" class="w-full h-14 object-cover rounded-lg mb-1">
<span class="text-[10px] text-slate-600 block">نمونه آسفالت</span>
</div>
<div onclick="selectSampleImage(this, 'https://images.unsplash.com/photo-1532996122724-e3c354a0b15b?auto=format&fit=crop&w=800&q=80')" class="sample-img-card border-2 border-slate-200 rounded-xl overflow-hidden cursor-pointer hover:border-emerald-500 transition-all p-1 text-center">
<img src="https://images.unsplash.com/photo-1532996122724-e3c354a0b15b?auto=format&fit=crop&w=300&q=80" class="w-full h-14 object-cover rounded-lg mb-1">
<span class="text-[10px] text-slate-600 block">نمونه پسماند</span>
</div>
<div onclick="selectSampleImage(this, 'https://images.unsplash.com/photo-1509114397022-ed747cca3f65?auto=format&fit=crop&w=800&q=80')" class="sample-img-card border-2 border-slate-200 rounded-xl overflow-hidden cursor-pointer hover:border-emerald-500 transition-all p-1 text-center">
<img src="https://images.unsplash.com/photo-1509114397022-ed747cca3f65?auto=format&fit=crop&w=300&q=80" class="w-full h-14 object-cover rounded-lg mb-1">
<span class="text-[10px] text-slate-600 block">نمونه روشنایی</span>
</div>
</div>
<input type="text" id="reportImageUrl" placeholder="یا آدرس تصویر اینترنتی را وارد کنید..." class="w-full px-4 py-2 rounded-xl border border-slate-300 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 outline-none text-xs">
</div>
<!-- Reporter Contact Info -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-2xl border border-slate-200">
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">نام گزارش‌دهنده (اختیاری)</label>
<input type="text" id="reporterName" placeholder="مثلاً: محمد رضایی" class="w-full px-3 py-2 rounded-lg border border-slate-300 bg-white text-xs outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">شماره همراه (جهت پیامک پیگیری)</label>
<input type="tel" id="reporterPhone" placeholder="0912..." class="w-full px-3 py-2 rounded-lg border border-slate-300 bg-white text-xs outline-none dir-ltr text-right">
</div>
</div>
<!-- Submit Button -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-end gap-3">
<button type="button" onclick="closeNewReportModal()" class="px-5 py-2.5 rounded-xl border border-slate-300 text-slate-600 hover:bg-slate-50 text-sm font-semibold transition-all">
انصراف
</button>
<button type="submit" id="submitReportBtn" class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white px-8 py-2.5 rounded-xl font-bold text-sm shadow-md shadow-emerald-600/30 transition-all flex items-center gap-2">
<i class="fa-solid fa-paper-plane"></i>
<span>ارسال و ثبت نهایی گزارش</span>
</button>
</div>
</form>
</div>
</div>
<!-- MODAL 2: ISSUE DETAIL & TIMELINE -->
<div id="detailModal" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-sm hidden flex items-center justify-center p-4 overflow-y-auto">
<div class="bg-white rounded-3xl max-w-3xl w-full max-h-[92vh] overflow-y-auto shadow-2xl border border-slate-100 custom-scrollbar">
<div id="detailModalContent">
<!-- Dynamically rendered via JS -->
</div>
</div>
</div>
<!-- MODAL 3: ADMIN STATUS UPDATE -->
<div id="adminActionModal" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-sm hidden flex items-center justify-center p-4">
<div class="bg-white rounded-3xl max-w-lg w-full p-6 shadow-2xl border border-slate-100 space-y-4">
<div class="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 class="text-base font-bold text-slate-900 flex items-center gap-2">
<i class="fa-solid fa-pen-to-square text-emerald-600"></i>
<span>به‌روزرسانی وضعیت و پاسخ کارشناسی</span>
</h3>
<button onclick="closeAdminActionModal()" class="text-slate-400 hover:text-slate-600">
<i class="fa-solid fa-xmark text-lg"></i>
</button>
</div>
<form id="adminActionForm" onsubmit="submitAdminAction(event)" class="space-y-4">
<input type="hidden" id="adminTargetIssueId">
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">تغییر وضعیت پرونده</label>
<select id="adminNewStatus" class="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm font-semibold outline-none focus:border-emerald-500">
<option value="pending">در صف بررسی اولیه</option>
<option value="reviewing">بررسی کارشناسی و ارجاع به اکیپ</option>
<option value="in_progress">در حال اجرای عملیات میدانی</option>
<option value="resolved">رفع و حل کامل مشکل</option>
<option value="rejected">رد گزارش (خارج از اختیارات/اطلاعات ناقص)</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">عنوان مرحله در تایم‌لاین</label>
<input type="text" id="adminTimelineTitle" placeholder="مثال: اعزام اکیپ آسفالت‌ریزی به محل..." class="w-full px-3.5 py-2 rounded-xl border border-slate-300 text-xs outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">پاسخ رسمی و توضیحات اکیپ اجرایی</label>
<textarea id="adminOfficialResponse" rows="3" placeholder="توضیحات دقیق بازرس، کد اکیپ اعزامی، اقدامات انجام شده..." class="w-full px-3.5 py-2 rounded-xl border border-slate-300 text-xs outline-none"></textarea>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 mb-1">آدرس تصویر رفع مشکل (پس از انجام کار)</label>
<input type="text" id="adminResolvedImage" placeholder="https://..." class="w-full px-3.5 py-2 rounded-xl border border-slate-300 text-xs outline-none dir-ltr text-right">
</div>
<div class="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<button type="button" onclick="closeAdminActionModal()" class="px-4 py-2 rounded-xl border border-slate-200 text-xs font-semibold">انصراف</button>
<button type="submit" class="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2 rounded-xl text-xs font-bold shadow-md shadow-emerald-600/20">ثبت تغییرات</button>
</div>
</form>
</div>
</div>
<!-- Footer -->
<footer class="bg-slate-900 text-slate-400 text-xs py-8 px-4 border-t border-slate-800 mt-12">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-xl bg-emerald-600 text-white flex items-center justify-center text-sm font-bold">
<i class="fa-solid fa-city"></i>
</div>
<div>
<p class="text-slate-200 font-bold">سامانه هوشمند گزارش و رسیدگی به مسائل شهری «شهرنگار»</p>
<p class="text-[11px] text-slate-500">طراحی و پیاده‌سازی برای ارتقای کیفیت زندگی و خدمات شهری</p>
</div>
</div>
<div class="flex items-center gap-6 text-slate-400">
<span class="hover:text-white cursor-pointer" onclick="switchTab('feed')">گزارش‌ها</span>
<span class="hover:text-white cursor-pointer" onclick="switchTab('map')">نقشه زنده</span>
<span class="hover:text-white cursor-pointer" onclick="switchTab('track')">استعلام کد رهگیری</span>
<span class="hover:text-white cursor-pointer" onclick="switchTab('admin')">پنل بازرسان</span>
</div>
</div>
</footer>
<!-- App Logic JS -->
<script src="/static/app.js"></script>
</body>
</html>
BIN
View File
Binary file not shown.