Complete backend MVP
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
|
||||
#include "common/Validation.h"
|
||||
#include "repositories/Sql.h"
|
||||
#include "services/UserService.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace visione::services
|
||||
{
|
||||
namespace
|
||||
@@ -23,6 +26,38 @@ Json::Value success()
|
||||
body["ok"] = true;
|
||||
return body;
|
||||
}
|
||||
|
||||
int boundedPage(const std::optional<std::string> &value)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
try
|
||||
{
|
||||
return std::max(1, std::stoi(*value));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int boundedPerPage(const std::optional<std::string> &value)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
try
|
||||
{
|
||||
return std::clamp(std::stoi(*value), 1, 100);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Json::Value AdminService::listCategories()
|
||||
@@ -135,9 +170,135 @@ Json::Value AdminService::overviewStats(const auth::UserContext &user)
|
||||
response["avg_resolution_hours"] = repositories::resultToJson(db->execSqlSync(
|
||||
"SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) AS value "
|
||||
"FROM reports WHERE status IN ('RESOLVED', 'CLOSED')"));
|
||||
response["reports_by_category"] = repositories::resultToJson(db->execSqlSync(
|
||||
"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 "
|
||||
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
|
||||
"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') "
|
||||
"ORDER BY month DESC LIMIT 12"));
|
||||
response["avg_resolution_hours_by_category"] =
|
||||
repositories::resultToJson(db->execSqlSync(
|
||||
"SELECT c.name AS category, "
|
||||
"AVG(TIMESTAMPDIFF(HOUR, r.created_at, r.updated_at)) AS value "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
|
||||
"GROUP BY c.name ORDER BY c.name"));
|
||||
response["avg_resolution_hours_by_area"] =
|
||||
repositories::resultToJson(db->execSqlSync(
|
||||
"SELECT COALESCE(a.name, 'Unassigned') AS area, "
|
||||
"AVG(TIMESTAMPDIFF(HOUR, r.created_at, r.updated_at)) AS value "
|
||||
"FROM reports r LEFT JOIN areas a ON a.id = r.area_id "
|
||||
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
|
||||
"GROUP BY COALESCE(a.name, 'Unassigned') ORDER BY area"));
|
||||
return response;
|
||||
}
|
||||
|
||||
Json::Value AdminService::listModeration(
|
||||
const auth::UserContext &user,
|
||||
const std::optional<std::string> &status,
|
||||
const std::optional<std::string> &page,
|
||||
const std::optional<std::string> &perPage)
|
||||
{
|
||||
if (!auth::canManageReports(user))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
const auto pageValue = boundedPage(page);
|
||||
const auto perPageValue = boundedPerPage(perPage);
|
||||
const auto offset = (pageValue - 1) * perPageValue;
|
||||
const auto statusValue = status.value_or("");
|
||||
|
||||
auto response = success();
|
||||
response["page"] = pageValue;
|
||||
response["per_page"] = perPageValue;
|
||||
response["flags"] = repositories::resultToJson(
|
||||
drogon::app().getDbClient()->execSqlSync(
|
||||
"SELECT f.*, creator.email AS created_by_email, "
|
||||
"resolver.email AS resolved_by_email "
|
||||
"FROM moderation_flags f "
|
||||
"JOIN users creator ON creator.id = f.created_by_user_id "
|
||||
"LEFT JOIN users resolver ON resolver.id = f.resolved_by_user_id "
|
||||
"WHERE (? = '' OR f.status = ?) "
|
||||
"ORDER BY f.created_at DESC LIMIT ? OFFSET ?",
|
||||
statusValue,
|
||||
statusValue,
|
||||
perPageValue,
|
||||
offset));
|
||||
return response;
|
||||
}
|
||||
|
||||
Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
||||
long long flagId,
|
||||
const Json::Value &body)
|
||||
{
|
||||
if (!auth::canManageReports(user))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
if (!validation::hasString(body, "status"))
|
||||
{
|
||||
return failure("invalid_moderation_payload");
|
||||
}
|
||||
const auto status = body["status"].asString();
|
||||
if (status != "RESOLVED" && status != "DISMISSED")
|
||||
{
|
||||
return failure("invalid_moderation_status");
|
||||
}
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto flag = db->execSqlSync(
|
||||
"SELECT id, report_id FROM moderation_flags WHERE id = ?",
|
||||
flagId);
|
||||
if (flag.empty())
|
||||
{
|
||||
return failure("moderation_flag_not_found");
|
||||
}
|
||||
|
||||
db->execSqlSync(
|
||||
"UPDATE moderation_flags "
|
||||
"SET status = ?, resolved_by_user_id = ?, resolution_note = ?, "
|
||||
"resolved_at = NOW() WHERE id = ?",
|
||||
status,
|
||||
user.id,
|
||||
body.get("resolution_note", "").asString(),
|
||||
flagId);
|
||||
|
||||
if (body.get("reject_report", false).asBool())
|
||||
{
|
||||
const auto reportId = flag[0]["report_id"].as<long long>();
|
||||
const auto current = db->execSqlSync(
|
||||
"SELECT status, user_id FROM reports WHERE id = ?",
|
||||
reportId);
|
||||
if (!current.empty() && current[0]["status"].as<std::string>() != "REJECTED")
|
||||
{
|
||||
const auto oldStatus = current[0]["status"].as<std::string>();
|
||||
db->execSqlSync("UPDATE reports SET status = 'REJECTED' WHERE id = ?",
|
||||
reportId);
|
||||
db->execSqlSync(
|
||||
"INSERT INTO report_status_history "
|
||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||
"VALUES (?, ?, 'REJECTED', ?, ?)",
|
||||
reportId,
|
||||
oldStatus,
|
||||
user.id,
|
||||
body.get("resolution_note", "Rejected after moderation").asString());
|
||||
UserService::createNotification(
|
||||
current[0]["user_id"].as<long long>(),
|
||||
"report_rejected",
|
||||
"Report rejected",
|
||||
"Your report was rejected after moderation review.",
|
||||
reportId);
|
||||
}
|
||||
}
|
||||
return success();
|
||||
}
|
||||
|
||||
Json::Value AdminService::listUsers(const auth::UserContext &user)
|
||||
{
|
||||
if (!auth::isAdmin(user))
|
||||
|
||||
Reference in New Issue
Block a user