feat: enhance Admin and report services with CRUD operations and intervention links

This commit is contained in:
gabdevele
2026-06-15 11:47:44 +02:00 Unverified
parent d684477ac3
commit 170fadbfe0
13 changed files with 1258 additions and 164 deletions
+203 -36
View File
@@ -82,24 +82,24 @@ const JsonRowSchema &adminUserSchema()
{"must_change_password", JsonFieldType::Bool}};
return schema;
}
} // namespace
Json::Value AdminService::listCategories()
bool isActiveField(const Json::Value &body)
{
auto response = success();
response["categories"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT * FROM report_categories ORDER BY name"));
return response;
return body.isMember("active") && body["active"].isBool();
}
} // namespace
Json::Value AdminService::createCategory(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name"))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name"))
{
return failure("invalid_category");
}
drogon::app().getDbClient()->execSqlSync(
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
@@ -108,19 +108,14 @@ Json::Value AdminService::createCategory(const auth::UserContext &user,
return success();
}
Json::Value AdminService::listAreas()
{
auto response = success();
response["areas"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name"));
return response;
}
Json::Value AdminService::createArea(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
!body["boundary_geojson"].isObject())
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name") || !body["boundary_geojson"].isObject())
{
return failure("invalid_area");
}
@@ -135,29 +130,38 @@ Json::Value AdminService::createArea(const auth::UserContext &user,
return success();
}
Json::Value AdminService::listTeams()
{
auto response = success();
response["teams"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT t.*, tc.name AS category_name, a.name AS area_name "
"FROM teams t JOIN team_categories tc ON tc.id = t.team_category_id "
"LEFT JOIN areas a ON a.id = t.area_id ORDER BY t.name"));
return response;
}
Json::Value AdminService::createTeam(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name") ||
!validation::hasNumber(body, "team_category_id"))
{
return failure("invalid_team");
}
auto db = drogon::app().getDbClient();
const auto teamCategoryRows = db->execSqlSync(
"SELECT id FROM team_categories WHERE id = ?",
body["team_category_id"].asInt64());
if (teamCategoryRows.empty())
{
return failure("invalid_reference");
}
if (body["area_id"].isNumeric())
{
drogon::app().getDbClient()->execSqlSync(
const auto areaRows = db->execSqlSync(
"SELECT id FROM areas WHERE id = ?",
body["area_id"].asInt64());
if (areaRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync(
"INSERT INTO teams (team_category_id, area_id, name, description) "
"VALUES (?, ?, ?, ?)",
body["team_category_id"].asInt64(),
@@ -167,7 +171,7 @@ Json::Value AdminService::createTeam(const auth::UserContext &user,
}
else
{
drogon::app().getDbClient()->execSqlSync(
db->execSqlSync(
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
body["team_category_id"].asInt64(),
body["name"].asString(),
@@ -198,10 +202,21 @@ Json::Value AdminService::overviewStats(const auth::UserContext &user)
"SELECT c.name AS category, COUNT(*) AS total "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"GROUP BY c.name ORDER BY total DESC"));
response["reports_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(*) AS total "
response["assignments_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(DISTINCT ass.id) AS total "
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
"GROUP BY t.name ORDER BY total DESC"));
response["resolved_reports_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(DISTINCT ass.report_id) AS total "
"FROM assignments ass "
"JOIN teams t ON t.id = ass.team_id "
"JOIN reports r ON r.id = ass.report_id "
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
"AND NOT EXISTS ("
" SELECT 1 FROM assignments ass2 "
" WHERE ass2.report_id = ass.report_id AND ass2.assigned_at > ass.assigned_at"
") "
"GROUP BY t.name ORDER BY total DESC"));
response["monthly_reports"] = repositories::resultToJson(db->execSqlSync(
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
"FROM reports GROUP BY DATE_FORMAT(created_at, '%Y-%m') "
@@ -229,7 +244,7 @@ Json::Value AdminService::listModeration(
const std::optional<std::string> &page,
const std::optional<std::string> &perPage)
{
if (!auth::canManageReports(user))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
@@ -262,7 +277,7 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
long long flagId,
const Json::Value &body)
{
if (!auth::canManageReports(user))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
@@ -324,6 +339,158 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
return success();
}
Json::Value AdminService::updateCategory(const auth::UserContext &user,
long long categoryId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync(
"SELECT id FROM report_categories WHERE id = ?",
categoryId);
if (rows.empty())
{
return failure("category_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE report_categories SET name = ? WHERE id = ?",
body["name"].asString(),
categoryId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE report_categories SET description = ? WHERE id = ?",
body["description"].asString(),
categoryId);
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE report_categories SET active = ? WHERE id = ?",
body["active"].asBool(),
categoryId);
}
return success();
}
Json::Value AdminService::updateArea(const auth::UserContext &user,
long long areaId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync("SELECT id FROM areas WHERE id = ?", areaId);
if (rows.empty())
{
return failure("area_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE areas SET name = ? WHERE id = ?",
body["name"].asString(),
areaId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE areas SET description = ? WHERE id = ?",
body["description"].asString(),
areaId);
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE areas SET active = ? WHERE id = ?",
body["active"].asBool(),
areaId);
}
if (body["boundary_geojson"].isObject())
{
Json::StreamWriterBuilder writer;
db->execSqlSync(
"UPDATE areas SET boundary_geojson = CAST(? AS JSON) WHERE id = ?",
Json::writeString(writer, body["boundary_geojson"]),
areaId);
}
return success();
}
Json::Value AdminService::updateTeam(const auth::UserContext &user,
long long teamId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync("SELECT id FROM teams WHERE id = ?", teamId);
if (rows.empty())
{
return failure("team_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE teams SET name = ? WHERE id = ?",
body["name"].asString(),
teamId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE teams SET description = ? WHERE id = ?",
body["description"].asString(),
teamId);
}
if (validation::hasNumber(body, "team_category_id"))
{
const auto categoryRows = db->execSqlSync(
"SELECT id FROM team_categories WHERE id = ?",
body["team_category_id"].asInt64());
if (categoryRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync("UPDATE teams SET team_category_id = ? WHERE id = ?",
body["team_category_id"].asInt64(),
teamId);
}
if (body.isMember("area_id"))
{
if (body["area_id"].isNull())
{
db->execSqlSync("UPDATE teams SET area_id = NULL WHERE id = ?", teamId);
}
else if (body["area_id"].isNumeric())
{
const auto areaRows =
db->execSqlSync("SELECT id FROM areas WHERE id = ?",
body["area_id"].asInt64());
if (areaRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync("UPDATE teams SET area_id = ? WHERE id = ?",
body["area_id"].asInt64(),
teamId);
}
else
{
return failure("invalid_reference");
}
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE teams SET active = ? WHERE id = ?",
body["active"].asBool(),
teamId);
}
return success();
}
Json::Value AdminService::listUsers(const auth::UserContext &user)
{
if (!auth::isAdmin(user))