Add report workflow
This commit is contained in:
+169
-45
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user