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);