feat: migrate backend to high-performance PHP 8.5 with SQLite PDO
This commit is contained in:
@@ -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);
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
// index.php - Main entry point
|
||||||
|
require __DIR__ . '/router.php';
|
||||||
+45
@@ -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 "سامانه مدیریت مسائل شهری شهرنگار فعال است.";
|
||||||
Binary file not shown.
Reference in New Issue
Block a user