feat: migrate backend to high-performance PHP 8.5 with SQLite PDO

This commit is contained in:
Antigravity Bot
2026-08-30 08:20:43 +03:30
parent 06e1266662
commit ded79a9a9e
5 changed files with 655 additions and 0 deletions
+334
View File
@@ -0,0 +1,334 @@
<?php
// api.php - RESTful API handlers for ShahrNegar
require_once __DIR__ . '/db.php';
// Set response headers
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
initDB();
$pdo = getDB();
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Helper JSON response
function jsonResponse($data, int $statusCode = 200) {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
function getJsonInput(): array {
$input = file_get_contents('php://input');
return json_decode($input, true) ?? [];
}
// 1. GET /api/stats
if ($requestUri === '/api/stats' && $method === 'GET') {
$total = (int)$pdo->query("SELECT COUNT(*) FROM issues")->fetchColumn();
$pending = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'pending'")->fetchColumn();
$reviewing = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'reviewing'")->fetchColumn();
$inProgress = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'in_progress'")->fetchColumn();
$resolved = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'resolved'")->fetchColumn();
$upvotes = (int)$pdo->query("SELECT COALESCE(SUM(upvotes), 0) FROM issues")->fetchColumn();
$catStmt = $pdo->query("SELECT category, COUNT(*) as count FROM issues GROUP BY category ORDER BY count DESC");
$categories = $catStmt->fetchAll();
$distStmt = $pdo->query("SELECT district, COUNT(*) as count FROM issues GROUP BY district ORDER BY district ASC");
$districts = $distStmt->fetchAll();
$prioStmt = $pdo->query("SELECT priority, COUNT(*) as count FROM issues GROUP BY priority");
$priorities = [];
foreach ($prioStmt->fetchAll() as $row) {
$priorities[$row['priority']] = (int)$row['count'];
}
$resolutionRate = $total > 0 ? round(($resolved / $total) * 100, 1) : 0;
jsonResponse([
'total' => $total,
'pending' => $pending,
'reviewing' => $reviewing,
'in_progress' => $inProgress,
'resolved' => $resolved,
'total_upvotes' => $upvotes,
'categories' => $categories,
'districts' => $districts,
'priorities' => $priorities,
'resolution_rate' => $resolutionRate
]);
}
// 2. GET /api/issues
if ($requestUri === '/api/issues' && $method === 'GET') {
$category = $_GET['category'] ?? null;
$status = $_GET['status'] ?? null;
$district = isset($_GET['district']) ? (int)$_GET['district'] : 0;
$priority = $_GET['priority'] ?? null;
$search = $_GET['search'] ?? null;
$sortBy = $_GET['sort_by'] ?? 'newest';
$sql = "SELECT * FROM issues WHERE 1=1";
$params = [];
if ($category && $category !== 'all') {
$sql .= " AND category = ?";
$params[] = $category;
}
if ($status && $status !== 'all') {
$sql .= " AND status = ?";
$params[] = $status;
}
if ($district > 0) {
$sql .= " AND district = ?";
$params[] = $district;
}
if ($priority && $priority !== 'all') {
$sql .= " AND priority = ?";
$params[] = $priority;
}
if ($search) {
$sql .= " AND (title LIKE ? OR description LIKE ? OR address LIKE ? OR tracking_code LIKE ?)";
$searchParam = "%{$search}%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($sortBy === 'upvotes') {
$sql .= " ORDER BY upvotes DESC, id DESC";
} elseif ($sortBy === 'oldest') {
$sql .= " ORDER BY id ASC";
} elseif ($sortBy === 'emergency') {
$sql .= " ORDER BY CASE priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, id DESC";
} else {
$sql .= " ORDER BY id DESC";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$commentCountStmt = $pdo->prepare("SELECT COUNT(*) FROM comments WHERE issue_id = ?");
$issues = [];
foreach ($rows as $row) {
$commentCountStmt->execute([$row['id']]);
$row['comment_count'] = (int)$commentCountStmt->fetchColumn();
$issues[] = $row;
}
jsonResponse($issues);
}
// 3. POST /api/issues (Create new issue)
if ($requestUri === '/api/issues' && $method === 'POST') {
$data = getJsonInput();
if (empty($data['title']) || empty($data['category']) || empty($data['address'])) {
jsonResponse(['error' => 'اطلاعات ضروری تکمیل نشده است.'], 400);
}
$trackingCode = 'SHR-' . rand(10000, 99999);
$now = date('Y-m-d H:i');
$stmt = $pdo->prepare("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, ?)");
$stmt->execute([
$trackingCode,
$data['title'],
$data['category'],
$data['description'] ?? '',
$data['address'],
(int)($data['district'] ?? 1),
$data['priority'] ?? 'medium',
(float)($data['lat'] ?? 35.7538),
(float)($data['lng'] ?? 51.4172),
$data['image_url'] ?? 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80',
$data['reporter_name'] ?? 'شهروند مسئول',
$data['reporter_phone'] ?? '',
$now
]);
$issueId = $pdo->lastInsertId();
// Timeline initial record
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, 'pending', 'ثبت اولیه گزارش در سامانه', 'گزارش شما با موفقیت در سامانه شهرنگار ثبت گردید و در نوبت بررسی کارشناسی قرار گرفت.', ?)");
$tlStmt->execute([$issueId, $now]);
$fetchStmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$fetchStmt->execute([$issueId]);
jsonResponse($fetchStmt->fetch(), 201);
}
// 4. GET /api/track/{code}
if (preg_match('#^/api/track/([^/]+)$#', $requestUri, $matches) && $method === 'GET') {
$code = strtoupper(trim(urldecode($matches[1])));
$stmt = $pdo->prepare("SELECT * FROM issues WHERE UPPER(tracking_code) = ?");
$stmt->execute([$code]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => "هیچ گزارشی با کد رهگیری «{$code}» یافت نشد."], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issue['id']]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issue['id']]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 5. GET /api/issues/{id}
if (preg_match('#^/api/issues/(\d+)$#', $requestUri, $matches) && $method === 'GET') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$stmt->execute([$issueId]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => 'گزارش مورد نظر یافت نشد.'], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issueId]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issueId]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 6. POST /api/issues/{id}/upvote
if (preg_match('#^/api/issues/(\d+)/upvote$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("UPDATE issues SET upvotes = upvotes + 1 WHERE id = ?");
$stmt->execute([$issueId]);
if ($stmt->rowCount() === 0) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$fetch = $pdo->prepare("SELECT upvotes FROM issues WHERE id = ?");
$fetch->execute([$issueId]);
$newUpvotes = (int)$fetch->fetchColumn();
jsonResponse(['id' => $issueId, 'upvotes' => $newUpvotes]);
}
// 7. POST /api/issues/{id}/comment
if (preg_match('#^/api/issues/(\d+)/comment$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$data = getJsonInput();
if (empty($data['content'])) {
jsonResponse(['error' => 'متن نظر نمی‌تواند خالی باشد.'], 400);
}
$check = $pdo->prepare("SELECT id FROM issues WHERE id = ?");
$check->execute([$issueId]);
if (!$check->fetch()) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$now = date('Y-m-d H:i');
$author = !empty($data['author_name']) ? trim($data['author_name']) : 'شهروند محترم';
$stmt = $pdo->prepare("INSERT INTO comments (issue_id, author_name, content, created_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$issueId, $author, trim($data['content']), $now]);
$commentId = $pdo->lastInsertId();
$cm = $pdo->prepare("SELECT * FROM comments WHERE id = ?");
$cm->execute([$commentId]);
jsonResponse($cm->fetch(), 201);
}
// 8. PATCH / POST /api/issues/{id}/status (Admin Status Update)
if (preg_match('#^/api/issues/(\d+)/status$#', $requestUri, $matches) && in_array($method, ['PATCH', 'POST'])) {
$issueId = (int)$matches[1];
$data = getJsonInput();
$check = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$check->execute([$issueId]);
$current = $check->fetch();
if (!$current) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$newStatus = $data['status'] ?? $current['status'];
$officialResponse = $data['official_response'] ?? $current['official_response'];
$resolvedImageUrl = $data['resolved_image_url'] ?? $current['resolved_image_url'];
$now = date('Y-m-d H:i');
$resolvedAt = ($newStatus === 'resolved') ? $now : $current['resolved_at'];
$upd = $pdo->prepare("UPDATE issues SET status = ?, official_response = ?, resolved_image_url = ?, resolved_at = ? WHERE id = ?");
$upd->execute([$newStatus, $officialResponse, $resolvedImageUrl, $resolvedAt, $issueId]);
$statusTitles = [
'pending' => 'در صف بررسی مجدد',
'reviewing' => 'بررسی کارشناسی و ارجاع به معاونت مربوطه',
'in_progress' => 'اعزام اکیپ اجرایی و آغاز عملیات میدانی',
'resolved' => 'اتمام عملیات و رفع کامل مسئله',
'rejected' => 'عدم احراز یا خارج از حیطه اختیارات شهرداری'
];
$tlTitle = !empty($data['timeline_title']) ? $data['timeline_title'] : ($statusTitles[$newStatus] ?? "تغییر وضعیت به {$newStatus}");
$tlDesc = !empty($data['timeline_desc']) ? $data['timeline_desc'] : ($officialResponse ?: 'وضعیت پرونده توسط مدیریت سامانه به‌روزرسانی شد.');
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, ?, ?, ?, ?)");
$tlStmt->execute([$issueId, $newStatus, $tlTitle, $tlDesc, $now]);
$updated = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$updated->execute([$issueId]);
jsonResponse($updated->fetch());
}
// 9. POST /api/upload
if ($requestUri === '/api/upload' && $method === 'POST') {
if (!isset($_FILES['file'])) {
jsonResponse(['error' => 'فایلی ارسال نشده است.'], 400);
}
$uploadDir = __DIR__ . '/static/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$filename = uniqid('img_', true) . '.' . $ext;
$target = $uploadDir . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
jsonResponse(['url' => '/static/uploads/' . $filename]);
} else {
jsonResponse(['error' => 'خطا در ذخیره‌سازی فایل.'], 500);
}
}
// Default 404 for API
jsonResponse(['error' => 'مسیر مورد نظر در API یافت نشد.'], 404);
+273
View File
@@ -0,0 +1,273 @@
<?php
// db.php - Database initialization and PDO helper for ShahrNegar
$dbPath = __DIR__ . '/urban_issues.db';
function getDB(): PDO {
global $dbPath;
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
}
function initDB() {
$pdo = getDB();
// Create Issues Table
$pdo->exec("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,
status TEXT NOT NULL,
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
)");
// Create Comments Table
$pdo->exec("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
)");
// Create Timeline Table
$pdo->exec("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 count and seed if empty
$stmt = $pdo->query("SELECT COUNT(*) FROM issues");
$count = (int)$stmt->fetchColumn();
if ($count === 0) {
seedInitialData($pdo);
}
}
function seedInitialData(PDO $pdo) {
$now = new DateTime();
$sampleIssues = [
[
'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' => null,
'reporter_name' => 'علی رضایی',
'reporter_phone' => '09121112233',
'official_response' => 'اکیپ آسفالت‌ریزی ناحیه ۲ شهرداری منطقه ۳ اعزام گردیده و عملیات زیرسازی و لکه‌گیری در حال انجام است.',
'upvotes' => 42,
'days_ago' => 2,
'resolved_at' => null,
'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' => (clone $now)->modify('-1 day')->format('Y-m-d H:i'),
'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' => null,
'reporter_name' => 'سعید کریمی',
'reporter_phone' => '09355556677',
'official_response' => 'گزارش در نوبت بررسی ناظر پسماند منطقه قرار گرفته است.',
'upvotes' => 19,
'days_ago' => 1,
'resolved_at' => null,
'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' => null,
'reporter_name' => 'پویا سرمدی',
'reporter_phone' => '09124445566',
'official_response' => 'اکیپ سازمان بوستان‌ها و آتش‌نشانی با جرثقیل در حال هرس ایمن و رفع خطر هستند.',
'upvotes' => 54,
'days_ago' => 1,
'resolved_at' => null,
'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' => null,
'reporter_name' => 'فرشید نادری',
'reporter_phone' => '09127778899',
'official_response' => 'اخطاریه ماده ۱۰۰ و رفع سد معبر برای مالک صادر شد. مهلت رفع ۴۸ ساعت می‌باشد.',
'upvotes' => 37,
'days_ago' => 3,
'resolved_at' => null,
'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' => (clone $now)->modify('-2 days')->format('Y-m-d H:i'),
'timeline' => [
['pending', 'ثبت گزارش خرابی علائم ترافیکی', 'گزارش دریافت گردید.'],
['reviewing', 'ارجاع به شرکت کنترل ترافیک', 'تایید خرابی سخت‌افزاری کنترلر.'],
['in_progress', 'تعویض برد فرمان و سنسورها', 'عملیات فنی در محل انجام شد.'],
['resolved', 'راه‌اندازی و اتصال به مرکز کنترل', 'چراغ طبق برنامه زمان‌بندی هوشمند شروع به کار کرد.']
],
'comments' => [
['سامان یوسفی', 'دست مریزاد، امروز صبح ترافیک خیلی روان‌تر بود.']
]
]
];
$insertIssue = $pdo->prepare("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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$insertTimeline = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, ?, ?, ?, ?)");
$insertComment = $pdo->prepare("INSERT INTO comments (issue_id, author_name, content, created_at) VALUES (?, ?, ?, ?)");
foreach ($sampleIssues as $item) {
$createdTime = (clone $now)->modify("-{$item['days_ago']} days")->format('Y-m-d H:i');
$insertIssue->execute([
$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'], $createdTime, $item['resolved_at']
]);
$issueId = $pdo->lastInsertId();
foreach ($item['timeline'] as $tl) {
$insertTimeline->execute([$issueId, $tl[0], $tl[1], $tl[2], $createdTime]);
}
foreach ($item['comments'] as $cm) {
$insertComment->execute([$issueId, $cm[0], $cm[1], $createdTime]);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
<?php
// index.php - Main entry point
require __DIR__ . '/router.php';
+45
View File
@@ -0,0 +1,45 @@
<?php
// router.php - Front controller & router for PHP built-in server
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// 1. Route API requests
if (strpos($uri, '/api/') === 0) {
require __DIR__ . '/api.php';
exit;
}
// 2. Serve static files if they exist
$filePath = __DIR__ . $uri;
if ($uri !== '/' && file_exists($filePath) && !is_dir($filePath)) {
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
$mimes = [
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'ico' => 'image/x-icon',
'woff' => 'font/woff',
'woff2'=> 'font/woff2',
'ttf' => 'font/ttf',
];
if (isset($mimes[$ext])) {
header('Content-Type: ' . $mimes[$ext]);
}
readfile($filePath);
exit;
}
// 3. Serve Frontend Application
if (file_exists(__DIR__ . '/static/index.html')) {
header('Content-Type: text/html; charset=utf-8');
readfile(__DIR__ . '/static/index.html');
exit;
}
echo "سامانه مدیریت مسائل شهری شهرنگار فعال است.";
BIN
View File
Binary file not shown.