Files
myapp/api.php
T

335 lines
12 KiB
PHP

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