$municipality_id]; // Optional: Filters by Status (Default: only approved) $status = $input['status'] ?? 'approved'; if ($status !== 'all') { $sql .= " AND status = :status"; $params[':status'] = $status; } // Optional: Filters by Category if (!empty($input['category'])) { $sql .= " AND category = :cat"; $params[':cat'] = $input['category']; } $sql .= " ORDER BY created_at DESC"; try { // Prepared Statement to prevent SQL Injection $stmt = $pdo->prepare($sql); $stmt->execute($params); // Fetches Results as PHP-Array $rows = $stmt->fetchAll(); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } // Builds GeoJSON FeatureCollection $features = []; foreach ($rows as $row) { $geometry = json_decode($row['geojson']); // Removes raw Geometry Columns from Properties unset($row['geom']); unset($row['geojson']); $features[] = [ 'type' => 'Feature', 'geometry' => $geometry, 'properties' => $row ]; } $featureCollection = [ 'type' => 'FeatureCollection', 'features' => $features ]; // Includes User's Votes for persistent Vote Display // Returns which Contributions the current Browser has voted on $browser_id = $input['browser_id'] ?? ''; if ($browser_id !== '') { $stmt = $pdo->prepare(" SELECT contribution_id, vote_type FROM votes WHERE browser_id = :bid "); $stmt->execute([':bid' => $browser_id]); $user_votes = []; foreach ($stmt->fetchAll() as $v) { $user_votes[$v['contribution_id']] = $v['vote_type']; } $featureCollection['user_votes'] = $user_votes; } json_response($featureCollection); } // --------------------------------------------------------------------- // CREATE: Inserts new Contributions // Required: municipality_id, geom, geom_type, category, title, author_name // Optional: description // --------------------------------------------------------------------- // --------------------------------------------------------------------- // CREATE: Inserts new Contributions with optional Photo Upload // Required: municipality_id, geom, geom_type, category, title, author_name // Optional: description, browser_id, photo (File Upload) // --------------------------------------------------------------------- function handle_create($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, [ 'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name' ]); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Geometry Type $valid_geom_types = ['point', 'line', 'polygon']; if (!in_array($input['geom_type'], $valid_geom_types)) { error_response('Invalid Geometry Type. Must be: ' . implode(', ', $valid_geom_types)); } // Validates GeoJSON $geojson = json_decode($input['geom']); if (!$geojson || !isset($geojson->type)) { error_response('Invalid GeoJSON in Geometry Field.'); } // Handles Photo Upload $photo_path = null; if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) { $photo_path = handle_photo_upload($_FILES['photo']); if (!$photo_path) { error_response('Photo Upload failed. JPG, PNG, GIF and WebP up to 5 MB are allowed.'); } } // Prepared SQL Statement try { $stmt = $pdo->prepare(" INSERT INTO contributions (municipality_id, geom, geom_type, category, title, description, author_name, browser_id, photo_path) VALUES (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, :category, :title, :description, :author_name, :browser_id, :photo_path) "); $stmt->execute([ ':mid' => $input['municipality_id'], ':geom' => $input['geom'], ':geom_type' => $input['geom_type'], ':category' => $input['category'], ':title' => $input['title'], ':description' => $input['description'] ?? '', ':author_name' => $input['author_name'], ':browser_id' => $input['browser_id'] ?? null, ':photo_path' => $photo_path ]); json_response([ 'message' => 'Contribution created successfully.', 'contribution_id' => (int) $pdo->lastInsertId() ], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE: Updates existing Contributions // Required: contribution_id // Optional: category, title, description, status // Provided Fields are updated. Others remain unchanged. // --------------------------------------------------------------------- function handle_update($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $contribution_id = $input['contribution_id']; // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Builds dynamic SQL Query to only update sent Fields $updatable_fields = ['category', 'title', 'description', 'status', 'address']; $set_clauses = []; $params = [':id' => $contribution_id]; foreach ($updatable_fields as $field) { if (isset($input[$field]) && $input[$field] !== '') { $set_clauses[] = "$field = :$field"; $params[":$field"] = $input[$field]; } } if (empty($set_clauses)) { error_response('No Fields to update. Provide at least one of: ' . implode(', ', $updatable_fields)); } // Validates Status if (isset($params[':status'])) { $valid_statuses = ['pending', 'approved', 'rejected', 'in_progress', 'done']; if (!in_array($params[':status'], $valid_statuses)) { error_response('Invalid Status. Must be: ' . implode(', ', $valid_statuses)); } } // Builds SQL Statement $sql = "UPDATE contributions SET " . implode(', ', $set_clauses) . " WHERE contribution_id = :id"; // Prepared SQL Statement try { $stmt = $pdo->prepare($sql); $stmt->execute($params); json_response(['message' => 'Contribution updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE: Deletes existing Contributions // Required: contribution_id // Associated Votes are deleted automatically // --------------------------------------------------------------------- function handle_delete($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $contribution_id = $input['contribution_id']; // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Prepared SQL Statement try { $stmt = $pdo->prepare("DELETE FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); json_response(['message' => 'Contribution deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // VOTE: Likes or Dislikes a Contribution // Required: contribution_id, voter_name, vote_type // Database Trigger automatically updates Likes and Dislikes Count // UNIQUE Constraint prevents duplicate Votes per Voter. // --------------------------------------------------------------------- function handle_vote($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id', 'voter_name', 'vote_type']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Vote Type $valid_vote_types = ['like', 'dislike']; if (!in_array($input['vote_type'], $valid_vote_types)) { error_response('Invalid vote_type. Must be: ' . implode(', ', $valid_vote_types)); } // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $input['contribution_id']]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Prepared SQL Statement try { // Checks if Voter already voted on this Contribution $browser_id = $input['browser_id'] ?? ''; if (empty($browser_id)) { error_response('Browser ID required for Voting.'); } $stmt = $pdo->prepare(" SELECT vote_id, vote_type FROM votes WHERE contribution_id = :cid AND browser_id = :bid "); $stmt->execute([':cid' => $input['contribution_id'], ':bid' => $browser_id]); $existing = $stmt->fetch(); if ($existing) { if ($existing['vote_type'] === $input['vote_type']) { // Same Vote Type — Removes Vote $stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid"); $stmt->execute([':vid' => $existing['vote_id']]); json_response(['message' => 'Vote removed.', 'action' => 'removed']); } else { // Different Vote Type — Switches Vote $stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid"); $stmt->execute([':vid' => $existing['vote_id']]); $stmt = $pdo->prepare(" INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) VALUES (:cid, :voter, :vtype, :bid) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':voter' => $input['voter_name'], ':vtype' => $input['vote_type'], ':bid' => $browser_id ]); json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200); } } else { // No existing Vote — Inserts Vote $stmt = $pdo->prepare(" INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) VALUES (:cid, :voter, :vtype, :bid) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':voter' => $input['voter_name'], ':vtype' => $input['vote_type'], ':bid' => $browser_id ]); json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201); } } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // ===================================================================== // Action Handlers for News // ===================================================================== // --------------------------------------------------------------------- // CREATE NEWS: Inserts new News Entry // Required: municipality_id, title, content // --------------------------------------------------------------------- function handle_create_news($input) { $pdo = get_db(); $missing = validate_required($input, ['municipality_id', 'title', 'content']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare(" INSERT INTO news (municipality_id, title, content, author_name) VALUES (:mid, :title, :content, :author) "); $stmt->execute([ ':mid' => $input['municipality_id'], ':title' => $input['title'], ':content' => $input['content'], ':author' => $input['author_name'] ?? 'Stadtverwaltung' ]); json_response(['message' => 'News created successfully.', 'news_id' => (int) $pdo->lastInsertId()], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE NEWS: Updates existing News Entry // Required: news_id // Optional: title, content // --------------------------------------------------------------------- function handle_update_news($input) { $pdo = get_db(); $missing = validate_required($input, ['news_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $set = []; $params = [':id' => $input['news_id']]; foreach (['title', 'content', 'author_name'] as $field) { if (isset($input[$field]) && $input[$field] !== '') { $set[] = "$field = :$field"; $params[":$field"] = $input[$field]; } } if (empty($set)) { error_response('No Fields to update.'); } try { $stmt = $pdo->prepare("UPDATE news SET " . implode(', ', $set) . " WHERE news_id = :id"); $stmt->execute($params); json_response(['message' => 'News updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE NEWS: Deletes existing News Entry // Required: news_id // --------------------------------------------------------------------- function handle_delete_news($input) { $pdo = get_db(); $missing = validate_required($input, ['news_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare("DELETE FROM news WHERE news_id = :id"); $stmt->execute([':id' => $input['news_id']]); json_response(['message' => 'News deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // ===================================================================== // Action Handlers for Photos // ===================================================================== // --------------------------------------------------------------------- // PHOTO UPLOAD: Validates and Saves uploaded Photo Files // Returns relative Path on Success, null on Failure. // Allowed: JPG, PNG, GIF, WebP. with maximum Size of 5 MB. // --------------------------------------------------------------------- function handle_photo_upload($file) { // Validates File Size $max_size = 5 * 1024 * 1024; if ($file['size'] > $max_size) { return null; } // Validates MIME Type $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!in_array($mime, $allowed_types)) { return null; } // Generates unique Filename $ext = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp' ][$mime]; $filename = uniqid('photo_', true) . '.' . $ext; $upload_dir = __DIR__ . '/../uploads/photos/'; $target_path = $upload_dir . $filename; // Creates Upload Directory if (!is_dir($upload_dir)) { mkdir($upload_dir, 0755, true); } // Moves uploaded File if (move_uploaded_file($file['tmp_name'], $target_path)) { return 'uploads/photos/' . $filename; } return null; } // ===================================================================== // Action Handlers for Comments // ===================================================================== // --------------------------------------------------------------------- // READ COMMENTS: Loads Comments for a Contribution // Returns Comments sorted by Date (newest first) // Required: contribution_id // --------------------------------------------------------------------- function handle_read_comments($input) { $pdo = get_db(); $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare(" SELECT comment_id, contribution_id, author_name, browser_id, content, status, created_at FROM comments WHERE contribution_id = :cid AND status = 'approved' ORDER BY created_at ASC "); $stmt->execute([':cid' => $input['contribution_id']]); $comments = $stmt->fetchAll(); json_response(['comments' => $comments, 'count' => count($comments)]); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // CREATE COMMENT: Adds Comments to Contributions // Required: contribution_id, author_name, content // Optional: browser_id // --------------------------------------------------------------------- function handle_create_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['contribution_id', 'author_name', 'content']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Content Length if (strlen($input['content']) > 1000) { error_response('Comment too long. Maximum 1000 Characters.'); } // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $input['contribution_id']]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } try { $stmt = $pdo->prepare(" INSERT INTO comments (contribution_id, author_name, browser_id, content) VALUES (:cid, :author, :bid, :content) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':author' => $input['author_name'], ':bid' => $input['browser_id'] ?? null, ':content' => $input['content'] ]); json_response([ 'message' => 'Comment created successfully.', 'comment_id' => (int) $pdo->lastInsertId() ], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE COMMENT: Removes a Comment // Required: comment_id // --------------------------------------------------------------------- function handle_delete_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['comment_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare("DELETE FROM comments WHERE comment_id = :id"); $stmt->execute([':id' => $input['comment_id']]); json_response(['message' => 'Comment deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE COMMENT: Changes Comment Status or Content // Required: comment_id // Optional: status, content // --------------------------------------------------------------------- function handle_update_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['comment_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $set = []; $params = [':id' => $input['comment_id']]; // Updates Status if provided if (isset($input['status']) && $input['status'] !== '') { $valid = ['pending', 'approved', 'rejected']; if (!in_array($input['status'], $valid)) { error_response('Invalid Status.'); } $set[] = "status = :status"; $params[':status'] = $input['status']; } // Updates Content if provided if (isset($input['content']) && $input['content'] !== '') { $set[] = "content = :content"; $params[':content'] = $input['content']; } if (empty($set)) { error_response('No Fields to update.'); } try { $stmt = $pdo->prepare("UPDATE comments SET " . implode(', ', $set) . " WHERE comment_id = :id"); $stmt->execute($params); json_response(['message' => 'Comment updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // ===================================================================== // Action Handlers for Tasks // ===================================================================== // --------------------------------------------------------------------- // READ TASKS: Loads Tasks as GeoJSON FeatureCollection // Required: municipality_id // Optional: status, browser_id // --------------------------------------------------------------------- function handle_read_tasks($input) { $pdo = get_db(); $missing = validate_required($input, ['municipality_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson FROM tasks WHERE municipality_id = :mid"; $params = [':mid' => $input['municipality_id']]; // Status Filter $status = $input['status'] ?? 'visible'; if ($status === 'visible') { $sql .= " AND status IN ('open', 'completed', 'verified')"; } elseif ($status !== 'all') { $sql .= " AND status = :status"; $params[':status'] = $status; } $sql .= " ORDER BY created_at DESC"; try { $stmt = $pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } // Builds GeoJSON FeatureCollection $features = []; foreach ($rows as $row) { $geometry = json_decode($row['geojson']); unset($row['geom'], $row['geojson']); $features[] = [ 'type' => 'Feature', 'geometry' => $geometry, 'properties' => $row ]; } $result = [ 'type' => 'FeatureCollection', 'features' => $features ]; // User Votes for Tasks $browser_id = $input['browser_id'] ?? ''; if ($browser_id !== '') { $stmt = $pdo->prepare(" SELECT task_id, vote_type FROM votes WHERE browser_id = :bid AND task_id IS NOT NULL "); $stmt->execute([':bid' => $browser_id]); $user_votes = []; foreach ($stmt->fetchAll() as $v) { $user_votes[$v['task_id']] = $v['vote_type']; } $result['user_votes'] = $user_votes; } json_response($result); } // --------------------------------------------------------------------- // CREATE TASK: Inserts new Task with optional Photo // Required: municipality_id, geom, geom_type, category, title, author_name // Optional: description, browser_id, photo // --------------------------------------------------------------------- function handle_create_task($input) { $pdo = get_db(); $missing = validate_required($input, [ 'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name' ]); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $valid_geom_types = ['point', 'line', 'polygon']; if (!in_array($input['geom_type'], $valid_geom_types)) { error_response('Invalid Geometry Type.'); } $geojson = json_decode($input['geom']); if (!$geojson || !isset($geojson->type)) { error_response('Invalid GeoJSON.'); } // Handles optional Photo Upload $photo_path = null; if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) { $photo_path = handle_photo_upload($_FILES['photo']); if (!$photo_path) { error_response('Photo Upload failed. JPG, PNG, GIF and WebP up to 5 MB.'); } } try { $stmt = $pdo->prepare(" INSERT INTO tasks (municipality_id, geom, geom_type, category, title, description, author_name, browser_id, photo_path) VALUES (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, :category, :title, :description, :author_name, :browser_id, :photo_path) "); $stmt->execute([ ':mid' => $input['municipality_id'], ':geom' => $input['geom'], ':geom_type' => $input['geom_type'], ':category' => $input['category'], ':title' => $input['title'], ':description' => $input['description'] ?? '', ':author_name' => $input['author_name'], ':browser_id' => $input['browser_id'] ?? null, ':photo_path' => $photo_path ]); json_response([ 'message' => 'Task created successfully.', 'task_id' => (int) $pdo->lastInsertId() ], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE TASK: Updates existing Tasks or Status // Required: task_id // Optional: category, title, description, status, address // --------------------------------------------------------------------- function handle_update_task($input) { $pdo = get_db(); $missing = validate_required($input, ['task_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $updatable = ['category', 'title', 'description', 'status', 'address']; $set = []; $params = [':id' => $input['task_id']]; foreach ($updatable as $field) { if (isset($input[$field]) && $input[$field] !== '') { $set[] = "$field = :$field"; $params[":$field"] = $input[$field]; } } if (empty($set)) { error_response('No Fields to update.'); } if (isset($params[':status'])) { $valid = ['pending', 'rejected', 'open', 'completed', 'verified']; if (!in_array($params[':status'], $valid)) { error_response('Invalid Status.'); } } try { $stmt = $pdo->prepare("UPDATE tasks SET " . implode(', ', $set) . " WHERE task_id = :id"); $stmt->execute($params); json_response(['message' => 'Task updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE TASK: Removes existing Tasks // Required: task_id // --------------------------------------------------------------------- function handle_delete_task($input) { $pdo = get_db(); $missing = validate_required($input, ['task_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare("DELETE FROM tasks WHERE task_id = :id"); $stmt->execute([':id' => $input['task_id']]); json_response(['message' => 'Task deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // COMPLETE TASK: Completes existing Tasks with Photo Proof // Required: task_id, author_name, browser_id // Required File: completion_photo // Optional: completion_comment // --------------------------------------------------------------------- function handle_complete_task($input) { $pdo = get_db(); $missing = validate_required($input, ['task_id', 'author_name', 'browser_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Checks if Task exists and is open $stmt = $pdo->prepare("SELECT task_id, status FROM tasks WHERE task_id = :id"); $stmt->execute([':id' => $input['task_id']]); $task = $stmt->fetch(); if (!$task) { error_response('Task not found.', 404); } if ($task['status'] !== 'open') { error_response('Task is not available for Completion.'); } // Handles required Completion Photo if (!isset($_FILES['completion_photo']) || $_FILES['completion_photo']['error'] !== UPLOAD_ERR_OK) { error_response('Completion Photo is required.'); } $photo_path = handle_photo_upload($_FILES['completion_photo']); if (!$photo_path) { error_response('Photo Upload failed. JPG, PNG, GIF and WebP up to 5 MB.'); } try { $stmt = $pdo->prepare(" UPDATE tasks SET status = 'completed', completed_by_name = :name, completed_by_browser = :browser, completion_photo = :photo, completion_comment = :comment, completed_at = NOW() WHERE task_id = :id "); $stmt->execute([ ':id' => $input['task_id'], ':name' => $input['author_name'], ':browser' => $input['browser_id'], ':photo' => $photo_path, ':comment' => $input['completion_comment'] ?? '' ]); json_response(['message' => 'Task Completion submitted for Review.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // VERIFY TASK: Moderator confirms or rejects Completions // Required: task_id, action // Awards Points and sets Status if verified // Clears Completion Fields, resets Status if rejected // --------------------------------------------------------------------- function handle_verify_task($input) { $pdo = get_db(); $missing = validate_required($input, ['task_id', 'verify_action']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Loads Task $stmt = $pdo->prepare("SELECT * FROM tasks WHERE task_id = :id"); $stmt->execute([':id' => $input['task_id']]); $task = $stmt->fetch(); if (!$task) { error_response('Task not found.', 404); } if ($task['status'] !== 'completed') { error_response('Task is not in completed State.'); } try { if ($input['verify_action'] === 'verify') { // Accepts Completion and Awards Points $stmt = $pdo->prepare("UPDATE tasks SET status = 'verified' WHERE task_id = :id"); $stmt->execute([':id' => $input['task_id']]); // Awards Points to User $stmt = $pdo->prepare(" INSERT INTO user_points (municipality_id, user_name, points, task_id) VALUES (:mid, :name, :points, :tid) "); $stmt->execute([ ':mid' => $task['municipality_id'], ':name' => $task['completed_by_name'], ':points' => $task['points_reward'], ':tid' => $input['task_id'] ]); json_response(['message' => 'Task verified. Points awarded.']); } elseif ($input['verify_action'] === 'reject') { // Rejects Completion and Clears Fields $stmt = $pdo->prepare(" UPDATE tasks SET status = 'open', completed_by_name = NULL, completed_by_browser = NULL, completion_photo = NULL, completion_comment = NULL, completed_at = NULL WHERE task_id = :id "); $stmt->execute([':id' => $input['task_id']]); json_response(['message' => 'Completion rejected. Task is open again.']); } else { error_response('Invalid Action. Must be: verify or reject.'); } } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // READ LEADERBOARD: Returns Citizen Leaderboard // Required: municipality_id // Optional: limit // --------------------------------------------------------------------- function handle_read_leaderboard($input) { $pdo = get_db(); $missing = validate_required($input, ['municipality_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $limit = min((int)($input['limit'] ?? 10), 50); try { $stmt = $pdo->prepare(" SELECT user_name, SUM(points) AS total_points, COUNT(*) AS tasks_completed FROM user_points WHERE municipality_id = :mid GROUP BY user_name ORDER BY total_points DESC LIMIT :lim "); $stmt->bindValue(':mid', $input['municipality_id'], PDO::PARAM_INT); $stmt->bindValue(':lim', $limit, PDO::PARAM_INT); $stmt->execute(); json_response(['leaderboard' => $stmt->fetchAll()]); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } }