Add report workflow

This commit is contained in:
gabdevele
2026-06-02 20:04:59 +02:00 Unverified
parent 54aa4cb73e
commit bff22ea83a
9 changed files with 295 additions and 52 deletions
+169 -45
View File
@@ -6,7 +6,9 @@
#include <drogon/drogon.h>
#include <map>
#include <set>
#include <sstream>
namespace visione::services
{
@@ -35,6 +37,20 @@ bool isAllowedStatus(const std::string &status)
return statuses.contains(status);
}
bool canTransition(const std::string &from, const std::string &to)
{
static const std::map<std::string, std::set<std::string>> transitions = {
{"OPEN", {"UNDER_REVIEW", "REJECTED"}},
{"UNDER_REVIEW", {"ASSIGNED", "REJECTED"}},
{"ASSIGNED", {"IN_PROGRESS", "REJECTED"}},
{"IN_PROGRESS", {"RESOLVED", "REJECTED"}},
{"RESOLVED", {"CLOSED"}},
{"REJECTED", {"CLOSED"}},
{"CLOSED", {}}};
const auto it = transitions.find(from);
return it != transitions.end() && it->second.contains(to);
}
std::optional<long long> findArea(double latitude, double longitude)
{
const auto result = drogon::app().getDbClient()->execSqlSync(
@@ -69,6 +85,24 @@ bool canAccessReport(const auth::UserContext &user, long long reportId)
user.id);
return !result.empty();
}
bool canModifyReport(const auth::UserContext &user, long long reportId)
{
if (auth::isAdmin(user))
{
return true;
}
if (!auth::canManageReports(user))
{
return false;
}
const auto result = drogon::app().getDbClient()->execSqlSync(
"SELECT id FROM reports WHERE id = ? AND assigned_operator_id = ?",
reportId,
user.id);
return !result.empty();
}
} // namespace
Json::Value ReportService::createReport(const auth::UserContext &user,
@@ -88,6 +122,14 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
const auto areaId = findArea(latitude, longitude);
auto db = drogon::app().getDbClient();
const auto category = db->execSqlSync(
"SELECT id FROM report_categories WHERE id = ? AND active = TRUE",
body["category_id"].asInt64());
if (category.empty())
{
return failure("category_not_found");
}
if (areaId)
{
db->execSqlSync(
@@ -154,42 +196,27 @@ Json::Value ReportService::listReports(const auth::UserContext &user,
const std::optional<std::string> &areaId)
{
auto db = drogon::app().getDbClient();
const auto statusParam = status.value_or("");
const auto categoryParam = categoryId.value_or("");
const auto areaParam = areaId.value_or("");
drogon::orm::Result result(nullptr);
if (status)
{
result = db->execSqlSync(
"SELECT r.*, c.name AS category_name, a.name AS area_name "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.status = ? "
"ORDER BY r.created_at DESC LIMIT 100",
*status);
}
else if (categoryId)
{
result = db->execSqlSync(
"SELECT r.*, c.name AS category_name, a.name AS area_name "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.category_id = ? "
"ORDER BY r.created_at DESC LIMIT 100",
*categoryId);
}
else if (areaId)
{
result = db->execSqlSync(
"SELECT r.*, c.name AS category_name, a.name AS area_name "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.area_id = ? "
"ORDER BY r.created_at DESC LIMIT 100",
*areaId);
}
else if (auth::canManageReports(user))
if (auth::canManageReports(user))
{
result = db->execSqlSync(
"SELECT r.*, c.name AS category_name, a.name AS area_name "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id "
"ORDER BY r.created_at DESC LIMIT 100");
"WHERE (? = '' OR r.status = ?) "
"AND (? = '' OR r.category_id = ?) "
"AND (? = '' OR r.area_id = ?) "
"ORDER BY r.created_at DESC LIMIT 100",
statusParam,
statusParam,
categoryParam,
categoryParam,
areaParam,
areaParam);
}
else
{
@@ -197,8 +224,18 @@ Json::Value ReportService::listReports(const auth::UserContext &user,
"SELECT r.*, c.name AS category_name, a.name AS area_name "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id "
"WHERE r.user_id = ? ORDER BY r.created_at DESC LIMIT 100",
user.id);
"WHERE r.user_id = ? "
"AND (? = '' OR r.status = ?) "
"AND (? = '' OR r.category_id = ?) "
"AND (? = '' OR r.area_id = ?) "
"ORDER BY r.created_at DESC LIMIT 100",
user.id,
statusParam,
statusParam,
categoryParam,
categoryParam,
areaParam,
areaParam);
}
auto response = success();
@@ -240,7 +277,7 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
long long reportId,
const Json::Value &body)
{
if (!auth::canManageReports(user))
if (!canModifyReport(user, reportId))
{
return failure("forbidden");
}
@@ -251,29 +288,72 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
}
auto db = drogon::app().getDbClient();
const auto current = db->execSqlSync("SELECT status FROM reports WHERE id = ?",
reportId);
const auto current = db->execSqlSync(
"SELECT status FROM reports WHERE id = ?",
reportId);
if (current.empty())
{
return failure("report_not_found");
}
const auto oldStatus = current[0]["status"].as<std::string>();
const auto newStatus = body["status"].asString();
if (!canTransition(oldStatus, newStatus))
{
return failure("invalid_status_transition");
}
db->execSqlSync("UPDATE reports SET status = ? WHERE id = ?",
body["status"].asString(),
newStatus,
reportId);
db->execSqlSync(
"INSERT INTO report_status_history "
"(report_id, old_status, new_status, changed_by_user_id, note) "
"VALUES (?, ?, ?, ?, ?)",
reportId,
current[0]["status"].as<std::string>(),
body["status"].asString(),
oldStatus,
newStatus,
user.id,
body.get("note", "").asString());
return success();
}
Json::Value ReportService::takeReport(const auth::UserContext &user,
long long reportId)
{
if (!auth::canManageReports(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto current = db->execSqlSync(
"SELECT status, assigned_operator_id FROM reports WHERE id = ?",
reportId);
if (current.empty())
{
return failure("report_not_found");
}
if (current[0]["status"].as<std::string>() != "OPEN" ||
!current[0]["assigned_operator_id"].isNull())
{
return failure("report_not_open");
}
db->execSqlSync(
"UPDATE reports SET assigned_operator_id = ?, status = 'UNDER_REVIEW' "
"WHERE id = ?",
user.id,
reportId);
db->execSqlSync(
"INSERT INTO report_status_history "
"(report_id, old_status, new_status, changed_by_user_id, note) "
"VALUES (?, 'OPEN', 'UNDER_REVIEW', ?, 'Report taken by operator')",
reportId,
user.id);
return success();
}
Json::Value ReportService::addMessage(const auth::UserContext &user,
long long reportId,
const Json::Value &body)
@@ -313,12 +393,25 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
long long reportId,
const Json::Value &body)
{
if (!auth::canManageReports(user) || !validation::hasNumber(body, "team_id"))
if (!canModifyReport(user, reportId) || !validation::hasNumber(body, "team_id"))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto current = db->execSqlSync(
"SELECT status FROM reports WHERE id = ?",
reportId);
if (current.empty())
{
return failure("report_not_found");
}
const auto oldStatus = current[0]["status"].as<std::string>();
if (!canTransition(oldStatus, "ASSIGNED"))
{
return failure("invalid_status_transition");
}
db->execSqlSync(
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
"VALUES (?, ?, ?)",
@@ -330,8 +423,9 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
db->execSqlSync(
"INSERT INTO report_status_history "
"(report_id, old_status, new_status, changed_by_user_id, note) "
"VALUES (?, NULL, 'ASSIGNED', ?, 'Team assigned')",
"VALUES (?, ?, 'ASSIGNED', ?, 'Team assigned')",
reportId,
oldStatus,
user.id);
return success();
}
@@ -373,19 +467,49 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
return success();
}
Json::Value ReportService::addPhoto(const auth::UserContext &user,
long long reportId,
const std::string &url)
Json::Value ReportService::canAddPhoto(const auth::UserContext &user,
long long reportId)
{
if (!canAccessReport(user, reportId) || url.empty())
if (!canAccessReport(user, reportId))
{
return failure("forbidden");
}
const auto count = drogon::app().getDbClient()->execSqlSync(
"SELECT COUNT(*) AS total FROM report_photos WHERE report_id = ?",
reportId);
if (!count.empty() && count[0]["total"].as<int>() >= 5)
{
return failure("photo_limit_reached");
}
auto response = success();
response["remaining_photo_slots"] =
5 - (count.empty() ? 0 : count[0]["total"].as<int>());
return response;
}
Json::Value ReportService::addPhoto(const auth::UserContext &user,
long long reportId,
const std::string &url,
const std::string &originalFilename,
const std::string &contentType,
size_t sizeBytes)
{
auto allowed = canAddPhoto(user, reportId);
if (!allowed.get("ok", false).asBool() || url.empty())
{
return allowed;
}
drogon::app().getDbClient()->execSqlSync(
"INSERT INTO report_photos (report_id, url) VALUES (?, ?)",
"INSERT INTO report_photos "
"(report_id, url, original_filename, content_type, size_bytes) "
"VALUES (?, ?, ?, ?, ?)",
reportId,
url);
url,
originalFilename,
contentType,
static_cast<unsigned long long>(sizeBytes));
return success();
}
} // namespace visione::services
+6 -1
View File
@@ -20,6 +20,7 @@ class ReportService
Json::Value updateStatus(const auth::UserContext &user,
long long reportId,
const Json::Value &body);
Json::Value takeReport(const auth::UserContext &user, long long reportId);
Json::Value addMessage(const auth::UserContext &user,
long long reportId,
const Json::Value &body);
@@ -30,8 +31,12 @@ class ReportService
Json::Value addFeedback(const auth::UserContext &user,
long long reportId,
const Json::Value &body);
Json::Value canAddPhoto(const auth::UserContext &user, long long reportId);
Json::Value addPhoto(const auth::UserContext &user,
long long reportId,
const std::string &url);
const std::string &url,
const std::string &originalFilename,
const std::string &contentType,
size_t sizeBytes);
};
} // namespace visione::services