diff --git a/public/api/contributions.php b/public/api/contributions.php index 71b5a4d..ffd673f 100644 --- a/public/api/contributions.php +++ b/public/api/contributions.php @@ -17,7 +17,7 @@ require_once __DIR__ . '/db.php'; // --------------------------------------------------------------------- -// Read Action Parameter and Route to correct Handler +// Reads Action Parameter and Routes to correct Handler // --------------------------------------------------------------------- $input = get_input(); $action = $input['action'] ?? ''; @@ -59,6 +59,27 @@ switch ($action) { case 'update_comment': handle_update_comment($input); break; + case 'read_tasks': + handle_read_tasks($input); + break; + case 'create_task': + handle_create_task($input); + break; + case 'update_task': + handle_update_task($input); + break; + case 'delete_task': + handle_delete_task($input); + break; + case 'complete_task': + handle_complete_task($input); + break; + case 'verify_task': + handle_verify_task($input); + break; + case 'read_leaderboard': + handle_read_leaderboard($input); + break; default: error_response('Unknown Action. Supported Actions are read, create, update, delete, vote.'); } @@ -709,4 +730,387 @@ function handle_update_comment($input) { } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } -} \ No newline at end of file +} + + +// ===================================================================== +// 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); + } +}