feat: complete modern urban issues management platform (ShahrNegar)
This commit is contained in:
+280
@@ -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))
|
||||
@@ -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
@@ -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' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Binary file not shown.
Reference in New Issue
Block a user