Files
webgis-lohne/public/api/contributions.php

1183 lines
40 KiB
PHP

<?php
// =====================================================================
// Contributions API Endpoint
// Handles CRUD Operations for Contributions (Points, Lines, Polygons)
// and Voting. Actions are determined by the 'action' Parameter in
// the Request.
//
// Supported Actions:
// read — Load approved Contributions
// create — Insert Contributions
// update — Update Contributions
// delete — Delete Contributions
// vote — Like or Dislike Contributions
// =====================================================================
require_once __DIR__ . '/db.php';
// ---------------------------------------------------------------------
// Reads Action Parameter and Routes to correct Handler
// ---------------------------------------------------------------------
$input = get_input();
$action = $input['action'] ?? '';
switch ($action) {
case 'read':
handle_read($input);
break;
case 'create':
handle_create($input);
break;
case 'update':
handle_update($input);
break;
case 'delete':
handle_delete($input);
break;
case 'vote':
handle_vote($input);
break;
case 'create_news':
handle_create_news($input);
break;
case 'update_news':
handle_update_news($input);
break;
case 'delete_news':
handle_delete_news($input);
break;
case 'read_comments':
handle_read_comments($input);
break;
case 'create_comment':
handle_create_comment($input);
break;
case 'delete_comment':
handle_delete_comment($input);
break;
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.');
}
// =====================================================================
// Action Handlers for Contributions
// =====================================================================
// ---------------------------------------------------------------------
// READ: Loads approved Contributions as GeoJSON FeatureCollection
// Required: municipality_id
// Optional: category
// ---------------------------------------------------------------------
function handle_read($input) {
$pdo = get_db();
// Validate Input
$missing = validate_required($input, ['municipality_id']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
$municipality_id = $input['municipality_id'];
// Builds SQL Query with Placeholders for prepared Statement
$sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson
FROM contributions
WHERE municipality_id = :mid";
$params = [':mid' => $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 Contributions or Tasks
// Required: contribution_id or task_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, ['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));
}
// 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.');
}
// Determines Vote Type
$is_task = isset($input['task_id']) && $input['task_id'] !== '';
if ($is_task) {
// Checks for Tasks
$stmt = $pdo->prepare("SELECT task_id FROM tasks WHERE task_id = :id");
$stmt->execute([':id' => $input['task_id']]);
if (!$stmt->fetch()) {
error_response('Task not found.', 404);
}
// Checks if Browser already voted on Task
$stmt = $pdo->prepare("
SELECT vote_id, vote_type FROM votes
WHERE task_id = :id AND browser_id = :bid
");
$stmt->execute([':id' => $input['task_id'], ':bid' => $browser_id]);
} else {
// Checks for Contributions
$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);
}
// Checks if Browser already voted on Contribution
$stmt = $pdo->prepare("
SELECT vote_id, vote_type FROM votes
WHERE contribution_id = :id AND browser_id = :bid
");
$stmt->execute([':id' => $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 — Removes old Vote before Inserting new one
$stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid");
$stmt->execute([':vid' => $existing['vote_id']]);
$this_insert = true;
}
} else {
// No existing Vote — Inserts Vote
$this_insert = true;
}
if (!empty($this_insert)) {
if ($is_task) {
$stmt = $pdo->prepare("
INSERT INTO votes (task_id, voter_name, vote_type, browser_id)
VALUES (:id, :voter, :vtype, :bid)
");
$stmt->execute([
':id' => $input['task_id'],
':voter' => $input['voter_name'],
':vtype' => $input['vote_type'],
':bid' => $browser_id
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id)
VALUES (:id, :voter, :vtype, :bid)
");
$stmt->execute([
':id' => $input['contribution_id'],
':voter' => $input['voter_name'],
':vtype' => $input['vote_type'],
':bid' => $browser_id
]);
}
// Returns changed or created
if ($existing) {
json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200);
} else {
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 Contributions or Tasks
// Returns Comments sorted by Date (oldest first)
// Required: contribution_id or task_id
// ---------------------------------------------------------------------
function handle_read_comments($input) {
$pdo = get_db();
// Checks for contribution_id or task_id
if (empty($input['contribution_id']) && empty($input['task_id'])) {
error_response('Either contribution_id or task_id is required.');
}
// Determines Vote Type
$is_task = isset($input['task_id']) && $input['task_id'] !== '';
try {
if ($is_task) {
$stmt = $pdo->prepare("
SELECT comment_id, task_id, author_name, browser_id, content, status, created_at
FROM comments
WHERE task_id = :id AND status = 'approved'
ORDER BY created_at ASC
");
} else {
$stmt = $pdo->prepare("
SELECT comment_id, contribution_id, author_name, browser_id, content, status, created_at
FROM comments
WHERE contribution_id = :id AND status = 'approved'
ORDER BY created_at ASC
");
}
// Prepared Statement
$stmt->execute([':id' => $is_task ? $input['task_id'] : $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 Contributions or Tasks
// Required: author_name, content, contribution_id or task_id
// Optional: browser_id
// ---------------------------------------------------------------------
function handle_create_comment($input) {
$pdo = get_db();
$missing = validate_required($input, ['author_name', 'content']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
// Checks for contribution_id or task_id
if (empty($input['contribution_id']) && empty($input['task_id'])) {
error_response('Either contribution_id or task_id is required.');
}
// Validates Length
if (strlen($input['content']) > 1000) {
error_response('Comment too long. Maximum 1000 Characters.');
}
// Determines Comment Type
$is_task = isset($input['task_id']) && $input['task_id'] !== '';
if ($is_task) {
// Checks for Tasks
$stmt = $pdo->prepare("SELECT task_id FROM tasks WHERE task_id = :id");
$stmt->execute([':id' => $input['task_id']]);
if (!$stmt->fetch()) {
error_response('Task not found.', 404);
}
} else {
// Checks for Contributions
$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 Statement
try {
$stmt = $pdo->prepare("
INSERT INTO comments (contribution_id, task_id, author_name, browser_id, content)
VALUES (:cid, :tid, :author, :bid, :content)
");
$stmt->execute([
':cid' => $is_task ? null : $input['contribution_id'],
':tid' => $is_task ? $input['task_id'] : null,
':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);
}
}