feat: enhance Admin and report services with CRUD operations and intervention links
This commit is contained in:
@@ -69,18 +69,14 @@ void sendAdmin(Json::Value body,
|
|||||||
{
|
{
|
||||||
status = drogon::k409Conflict;
|
status = drogon::k409Conflict;
|
||||||
}
|
}
|
||||||
|
else if (error == "invalid_reference" || error.starts_with("invalid_"))
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void AdminController::listCategories(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listCategories()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createCategory(
|
void AdminController::createCategory(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -95,14 +91,6 @@ void AdminController::createCategory(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminController::listAreas(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listAreas()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createArea(
|
void AdminController::createArea(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -117,14 +105,6 @@ void AdminController::createArea(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminController::listTeams(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listTeams()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createTeam(
|
void AdminController::createTeam(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -139,6 +119,51 @@ void AdminController::createTeam(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AdminController::updateCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateCategory(*user, categoryId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::updateArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateArea(*user, areaId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::updateTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateTeam(*user, teamId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void AdminController::overviewStats(
|
void AdminController::overviewStats(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ class AdminController : public drogon::HttpController<AdminController>
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
ADD_METHOD_TO(AdminController::listCategories,
|
|
||||||
"/api/v1/admin/categories",
|
|
||||||
drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createCategory,
|
ADD_METHOD_TO(AdminController::createCategory,
|
||||||
"/api/v1/admin/categories",
|
"/api/v1/admin/categories",
|
||||||
drogon::Post);
|
drogon::Post);
|
||||||
ADD_METHOD_TO(AdminController::listAreas, "/api/v1/admin/areas", drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post);
|
ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post);
|
||||||
ADD_METHOD_TO(AdminController::listTeams, "/api/v1/admin/teams", drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post);
|
ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post);
|
||||||
|
ADD_METHOD_TO(AdminController::updateCategory,
|
||||||
|
"/api/v1/admin/categories/{1}",
|
||||||
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::updateArea,
|
||||||
|
"/api/v1/admin/areas/{1}",
|
||||||
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::updateTeam,
|
||||||
|
"/api/v1/admin/teams/{1}",
|
||||||
|
drogon::Patch);
|
||||||
ADD_METHOD_TO(AdminController::overviewStats,
|
ADD_METHOD_TO(AdminController::overviewStats,
|
||||||
"/api/v1/admin/stats/overview",
|
"/api/v1/admin/stats/overview",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
@@ -34,24 +38,27 @@ class AdminController : public drogon::HttpController<AdminController>
|
|||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
void listCategories(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createCategory(
|
void createCategory(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void listAreas(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createArea(
|
void createArea(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void listTeams(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createTeam(
|
void createTeam(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void updateCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId);
|
||||||
|
void updateArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId);
|
||||||
|
void updateTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId);
|
||||||
void overviewStats(
|
void overviewStats(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ std::optional<std::string> queryValue(const drogon::HttpRequestPtr &req,
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void PublicController::listAreas(
|
||||||
|
const drogon::HttpRequestPtr &,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
visione::services::ReportService service;
|
||||||
|
callback(visione::http::json(service.listAreas()));
|
||||||
|
}
|
||||||
|
|
||||||
void PublicController::openDataReports(
|
void PublicController::openDataReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -29,3 +37,15 @@ void PublicController::openDataReports(
|
|||||||
callback(visione::http::json(service.openDataReports(queryValue(req, "page"),
|
callback(visione::http::json(service.openDataReports(queryValue(req, "page"),
|
||||||
queryValue(req, "per_page"))));
|
queryValue(req, "per_page"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PublicController::interventionSummary(
|
||||||
|
const drogon::HttpRequestPtr &,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
const std::string &token)
|
||||||
|
{
|
||||||
|
visione::services::ReportService service;
|
||||||
|
auto body = service.publicInterventionSummary(token);
|
||||||
|
const auto status = body.get("ok", false).asBool() ? drogon::k200OK
|
||||||
|
: drogon::k404NotFound;
|
||||||
|
callback(visione::http::json(std::move(body), status));
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ class PublicController : public drogon::HttpController<PublicController>
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
|
ADD_METHOD_TO(PublicController::listAreas, "/api/v1/areas", drogon::Get);
|
||||||
ADD_METHOD_TO(PublicController::openDataReports,
|
ADD_METHOD_TO(PublicController::openDataReports,
|
||||||
"/api/v1/open-data/reports",
|
"/api/v1/open-data/reports",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(PublicController::interventionSummary,
|
||||||
|
"/api/v1/interventions/{1}",
|
||||||
|
drogon::Get);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
|
void listAreas(const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void openDataReports(
|
void openDataReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void interventionSummary(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
const std::string &token);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,10 +68,17 @@ void sendService(Json::Value body,
|
|||||||
status = drogon::k404NotFound;
|
status = drogon::k404NotFound;
|
||||||
}
|
}
|
||||||
else if (error == "invalid_status_transition" ||
|
else if (error == "invalid_status_transition" ||
|
||||||
error == "media_limit_reached")
|
error == "media_limit_reached" ||
|
||||||
|
error == "report_not_open" ||
|
||||||
|
error == "report_owned_by_other_operator")
|
||||||
{
|
{
|
||||||
status = drogon::k409Conflict;
|
status = drogon::k409Conflict;
|
||||||
}
|
}
|
||||||
|
else if (error == "invalid_reference" || error.starts_with("invalid_") ||
|
||||||
|
error == "missing_rating")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +148,32 @@ void ReportController::list(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportController::listCategories(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.listCategories(*user), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::listTeams(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.listOperatorTeams(*user), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void ReportController::assignedReports(
|
void ReportController::assignedReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -217,6 +250,48 @@ void ReportController::interventionSummary(
|
|||||||
sendService(service.interventionSummary(*user, reportId), std::move(callback));
|
sendService(service.interventionSummary(*user, reportId), std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportController::teamSuggestions(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.teamSuggestions(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::createInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.createInterventionLink(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::revokeInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user || blockPasswordChangeRequired(*user, callback))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.revokeInterventionLink(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void ReportController::updateStatus(
|
void ReportController::updateStatus(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
@@ -368,7 +443,7 @@ void ReportController::uploadMedia(
|
|||||||
auto allowed = service.canAddMedia(*user, reportId);
|
auto allowed = service.canAddMedia(*user, reportId);
|
||||||
if (!allowed.get("ok", false).asBool())
|
if (!allowed.get("ok", false).asBool())
|
||||||
{
|
{
|
||||||
callback(visione::http::json(std::move(allowed), drogon::k403Forbidden));
|
sendService(std::move(allowed), std::move(callback));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post);
|
ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post);
|
||||||
ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get);
|
ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::listCategories,
|
||||||
|
"/api/v1/report-categories",
|
||||||
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::listTeams,
|
||||||
|
"/api/v1/operator/teams",
|
||||||
|
drogon::Get);
|
||||||
ADD_METHOD_TO(ReportController::assignedReports,
|
ADD_METHOD_TO(ReportController::assignedReports,
|
||||||
"/api/v1/operator/reports/assigned",
|
"/api/v1/operator/reports/assigned",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
@@ -21,6 +27,15 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
ADD_METHOD_TO(ReportController::interventionSummary,
|
ADD_METHOD_TO(ReportController::interventionSummary,
|
||||||
"/api/v1/reports/{1}/intervention-summary",
|
"/api/v1/reports/{1}/intervention-summary",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::teamSuggestions,
|
||||||
|
"/api/v1/reports/{1}/team-suggestions",
|
||||||
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::createInterventionLink,
|
||||||
|
"/api/v1/reports/{1}/intervention-link",
|
||||||
|
drogon::Post);
|
||||||
|
ADD_METHOD_TO(ReportController::revokeInterventionLink,
|
||||||
|
"/api/v1/reports/{1}/intervention-link",
|
||||||
|
drogon::Delete);
|
||||||
ADD_METHOD_TO(ReportController::updateStatus,
|
ADD_METHOD_TO(ReportController::updateStatus,
|
||||||
"/api/v1/reports/{1}/status",
|
"/api/v1/reports/{1}/status",
|
||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
@@ -54,6 +69,11 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void list(const drogon::HttpRequestPtr &req,
|
void list(const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void listCategories(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void listTeams(const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void assignedReports(
|
void assignedReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
@@ -70,6 +90,18 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
long long reportId);
|
long long reportId);
|
||||||
|
void teamSuggestions(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
|
void createInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
|
void revokeInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
void updateStatus(
|
void updateStatus(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
|||||||
+13
-3
@@ -26,12 +26,17 @@ Authorization: Bearer <token>
|
|||||||
## Reports
|
## Reports
|
||||||
- `POST /reports`: create a report.
|
- `POST /reports`: create a report.
|
||||||
- `GET /reports`: list reports with optional `status`, `category_id`, `area_id`.
|
- `GET /reports`: list reports with optional `status`, `category_id`, `area_id`.
|
||||||
|
- `GET /report-categories`: list active report categories for authenticated users.
|
||||||
|
- `GET /operator/teams`: list active intervention teams for operators/admins.
|
||||||
- `GET /operator/reports/assigned`: list reports assigned to the current operator.
|
- `GET /operator/reports/assigned`: list reports assigned to the current operator.
|
||||||
- `GET /operator/reports/open`: list open unassigned reports.
|
- `GET /operator/reports/open`: list open unassigned reports.
|
||||||
- `GET /operator/reports/others`: list reports assigned to other operators.
|
- `GET /operator/reports/others`: list reports assigned to other operators.
|
||||||
- `GET /reports/{id}`: report detail.
|
- `GET /reports/{id}`: report detail.
|
||||||
- `GET /reports/{id}/intervention-summary`: operator-facing report summary with
|
- `GET /reports/{id}/intervention-summary`: operator-facing report summary with
|
||||||
citizen contact data, media and latest messages.
|
citizen contact data, media and latest messages.
|
||||||
|
- `GET /reports/{id}/team-suggestions`: suggest compatible teams by area/category.
|
||||||
|
- `POST /reports/{id}/intervention-link`: create or rotate public team link.
|
||||||
|
- `DELETE /reports/{id}/intervention-link`: revoke public team link.
|
||||||
- `PATCH /reports/{id}/status`: update status.
|
- `PATCH /reports/{id}/status`: update status.
|
||||||
- `POST /reports/{id}/take`: assigned-queue operator takes an open report.
|
- `POST /reports/{id}/take`: assigned-queue operator takes an open report.
|
||||||
- `POST /reports/{id}/messages`: write a report message.
|
- `POST /reports/{id}/messages`: write a report message.
|
||||||
@@ -51,9 +56,12 @@ Authorization: Bearer <token>
|
|||||||
- `GET /me/rewards`: return current points and reward event history.
|
- `GET /me/rewards`: return current points and reward event history.
|
||||||
|
|
||||||
## Admin Reference Data
|
## Admin Reference Data
|
||||||
- `GET|POST /admin/categories`
|
- `POST /admin/categories`
|
||||||
- `GET|POST /admin/areas`
|
- `PATCH /admin/categories/{id}`
|
||||||
- `GET|POST /admin/teams`
|
- `POST /admin/areas`
|
||||||
|
- `PATCH /admin/areas/{id}`
|
||||||
|
- `POST /admin/teams`
|
||||||
|
- `PATCH /admin/teams/{id}`
|
||||||
- `GET /admin/stats/overview`
|
- `GET /admin/stats/overview`
|
||||||
- `GET /admin/moderation`: list moderation flags with optional `status`.
|
- `GET /admin/moderation`: list moderation flags with optional `status`.
|
||||||
- `PATCH /admin/moderation/{id}`: resolve or dismiss a moderation flag, optionally
|
- `PATCH /admin/moderation/{id}`: resolve or dismiss a moderation flag, optionally
|
||||||
@@ -63,4 +71,6 @@ Authorization: Bearer <token>
|
|||||||
- `PATCH /admin/users/{id}/ban`
|
- `PATCH /admin/users/{id}/ban`
|
||||||
|
|
||||||
## Public Data
|
## Public Data
|
||||||
|
- `GET /areas`: list active municipal areas.
|
||||||
|
- `GET /interventions/{token}`: public intervention summary for external teams.
|
||||||
- `GET /open-data/reports`: anonymized resolved/closed report export.
|
- `GET /open-data/reports`: anonymized resolved/closed report export.
|
||||||
|
|||||||
@@ -136,6 +136,26 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AssignmentLinkResponse": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ok": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ok",
|
||||||
|
"url",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Category": {
|
"Category": {
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1037,7 +1057,22 @@
|
|||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"reports_by_team": {
|
"assignments_by_team": {
|
||||||
|
"items": {
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"team": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"resolved_reports_by_team": {
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1059,7 +1094,8 @@
|
|||||||
"reports_by_area",
|
"reports_by_area",
|
||||||
"avg_resolution_hours",
|
"avg_resolution_hours",
|
||||||
"reports_by_category",
|
"reports_by_category",
|
||||||
"reports_by_team",
|
"assignments_by_team",
|
||||||
|
"resolved_reports_by_team",
|
||||||
"monthly_reports",
|
"monthly_reports",
|
||||||
"avg_resolution_hours_by_category",
|
"avg_resolution_hours_by_category",
|
||||||
"avg_resolution_hours_by_area"
|
"avg_resolution_hours_by_area"
|
||||||
@@ -1468,6 +1504,26 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"TeamSuggestionListResponse": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"ok": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"items": {
|
||||||
|
"additionalProperties": true,
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ok",
|
||||||
|
"suggestions"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"TokenResponse": {
|
"TokenResponse": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1609,35 +1665,6 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/admin/areas": {
|
"/api/v1/admin/areas": {
|
||||||
"get": {
|
|
||||||
"description": "Lists configured municipal areas.",
|
|
||||||
"operationId": "AdminController.listAreas",
|
|
||||||
"parameters": [ ],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AreaListResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Area list."
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"$ref": "#/components/responses/Unauthorized"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": [ ]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "List areas",
|
|
||||||
"tags": [
|
|
||||||
"Admin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Creates a municipal area with GeoJSON boundary.",
|
"description": "Creates a municipal area with GeoJSON boundary.",
|
||||||
"operationId": "AdminController.createArea",
|
"operationId": "AdminController.createArea",
|
||||||
@@ -1684,24 +1711,50 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/admin/categories": {
|
"/api/v1/admin/areas/{id}": {
|
||||||
"get": {
|
"patch": {
|
||||||
"description": "Lists active report categories.",
|
"description": "Updates an existing municipal area.",
|
||||||
"operationId": "AdminController.listCategories",
|
"operationId": "AdminController.updateArea",
|
||||||
"parameters": [ ],
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateAreaRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/CategoryListResponse"
|
"$ref": "#/components/schemas/OkResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Report category list."
|
"description": "Updated area result."
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"$ref": "#/components/responses/Unauthorized"
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1709,11 +1762,13 @@
|
|||||||
"bearerAuth": [ ]
|
"bearerAuth": [ ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "List categories",
|
"summary": "Update area",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Admin"
|
"Admin"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/categories": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Creates a report category.",
|
"description": "Creates a report category.",
|
||||||
"operationId": "AdminController.createCategory",
|
"operationId": "AdminController.createCategory",
|
||||||
@@ -1760,6 +1815,63 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/admin/categories/{id}": {
|
||||||
|
"patch": {
|
||||||
|
"description": "Updates an existing report category.",
|
||||||
|
"operationId": "AdminController.updateCategory",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateCategoryRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OkResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Updated category result."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Update category",
|
||||||
|
"tags": [
|
||||||
|
"Admin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/admin/moderation": {
|
"/api/v1/admin/moderation": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Lists moderation flags for admin/operator review.",
|
"description": "Lists moderation flags for admin/operator review.",
|
||||||
@@ -1916,35 +2028,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/admin/teams": {
|
"/api/v1/admin/teams": {
|
||||||
"get": {
|
|
||||||
"description": "Lists intervention teams.",
|
|
||||||
"operationId": "AdminController.listTeams",
|
|
||||||
"parameters": [ ],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TeamListResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Team list."
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"$ref": "#/components/responses/Unauthorized"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": [ ]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "List teams",
|
|
||||||
"tags": [
|
|
||||||
"Admin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Creates an intervention team.",
|
"description": "Creates an intervention team.",
|
||||||
"operationId": "AdminController.createTeam",
|
"operationId": "AdminController.createTeam",
|
||||||
@@ -1991,6 +2074,86 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/admin/teams/{id}": {
|
||||||
|
"patch": {
|
||||||
|
"description": "Updates an existing intervention team.",
|
||||||
|
"operationId": "AdminController.updateTeam",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateTeamRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OkResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Updated team result."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Update team",
|
||||||
|
"tags": [
|
||||||
|
"Admin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/areas": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lists active municipal areas.",
|
||||||
|
"operationId": "PublicController.listAreas",
|
||||||
|
"parameters": [ ],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AreaListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Area list."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "List areas",
|
||||||
|
"tags": [
|
||||||
|
"Public"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/admin/users": {
|
"/api/v1/admin/users": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Lists platform users with role information.",
|
"description": "Lists platform users with role information.",
|
||||||
@@ -2697,6 +2860,41 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/interventions/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns public intervention summary for external intervention teams.",
|
||||||
|
"operationId": "PublicController.interventionSummary",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InterventionSummaryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Public intervention summary."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Public intervention summary",
|
||||||
|
"tags": [
|
||||||
|
"Public"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/operator/reports/assigned": {
|
"/api/v1/operator/reports/assigned": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Lists reports assigned to the authenticated operator.",
|
"description": "Lists reports assigned to the authenticated operator.",
|
||||||
@@ -2850,6 +3048,40 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/operator/teams": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lists active intervention teams for operators and admins.",
|
||||||
|
"operationId": "ReportController.listTeams",
|
||||||
|
"parameters": [ ],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TeamListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Team list."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "List teams",
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/ready": {
|
"/api/v1/ready": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Checks whether MySQL is reachable.",
|
"description": "Checks whether MySQL is reachable.",
|
||||||
@@ -2986,6 +3218,37 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/report-categories": {
|
||||||
|
"get": {
|
||||||
|
"description": "Lists active report categories for authenticated users.",
|
||||||
|
"operationId": "ReportController.listCategories",
|
||||||
|
"parameters": [ ],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CategoryListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Report category list."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "List categories",
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/reports/{id}": {
|
"/api/v1/reports/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns report detail, status history and media.",
|
"description": "Returns report detail, status history and media.",
|
||||||
@@ -3030,6 +3293,95 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/reports/{id}/intervention-link": {
|
||||||
|
"delete": {
|
||||||
|
"description": "Revokes current public intervention link for assigned team.",
|
||||||
|
"operationId": "ReportController.revokeInterventionLink",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OkResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Link revocation result."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Revoke intervention link",
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Creates or rotates public intervention link for external intervention team.",
|
||||||
|
"operationId": "ReportController.createInterventionLink",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssignmentLinkResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Generated public link."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"$ref": "#/components/responses/Conflict"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Create intervention link",
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/reports/{id}/assignments": {
|
"/api/v1/reports/{id}/assignments": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Assigns an intervention team to a report.",
|
"description": "Assigns an intervention team to a report.",
|
||||||
@@ -3245,6 +3597,53 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/reports/{id}/team-suggestions": {
|
||||||
|
"get": {
|
||||||
|
"description": "Suggests compatible teams for report category and area.",
|
||||||
|
"operationId": "ReportController.teamSuggestions",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TeamSuggestionListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Suggested teams."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": [ ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Team suggestions",
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/reports/{id}/media": {
|
"/api/v1/reports/{id}/media": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Uploads report images or videos as multipart files.",
|
"description": "Uploads report images or videos as multipart files.",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS report_category_team_categories (
|
||||||
|
report_category_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
team_category_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
PRIMARY KEY (report_category_id, team_category_id),
|
||||||
|
CONSTRAINT report_category_team_categories_report_fk
|
||||||
|
FOREIGN KEY (report_category_id) REFERENCES report_categories(id),
|
||||||
|
CONSTRAINT report_category_team_categories_team_fk
|
||||||
|
FOREIGN KEY (team_category_id) REFERENCES team_categories(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS intervention_access_links (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
report_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
assignment_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
token_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_by_user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
revoked_at DATETIME NULL,
|
||||||
|
last_accessed_at DATETIME NULL,
|
||||||
|
UNIQUE KEY intervention_access_links_token_hash_unique (token_hash),
|
||||||
|
KEY intervention_access_links_report_assignment_idx (report_id, assignment_id),
|
||||||
|
CONSTRAINT intervention_access_links_report_fk
|
||||||
|
FOREIGN KEY (report_id) REFERENCES reports(id),
|
||||||
|
CONSTRAINT intervention_access_links_assignment_fk
|
||||||
|
FOREIGN KEY (assignment_id) REFERENCES assignments(id),
|
||||||
|
CONSTRAINT intervention_access_links_user_fk
|
||||||
|
FOREIGN KEY (created_by_user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO report_category_team_categories (report_category_id, team_category_id)
|
||||||
|
SELECT rc.id, tc.id
|
||||||
|
FROM report_categories rc
|
||||||
|
JOIN team_categories tc ON
|
||||||
|
(rc.name = 'Buche stradali' AND tc.name = 'Manutenzione strade') OR
|
||||||
|
(rc.name = 'Illuminazione guasta' AND tc.name = 'Illuminazione pubblica') OR
|
||||||
|
(rc.name = 'Rifiuti abbandonati' AND tc.name = 'Igiene urbana');
|
||||||
+203
-36
@@ -82,24 +82,24 @@ const JsonRowSchema &adminUserSchema()
|
|||||||
{"must_change_password", JsonFieldType::Bool}};
|
{"must_change_password", JsonFieldType::Bool}};
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Json::Value AdminService::listCategories()
|
bool isActiveField(const Json::Value &body)
|
||||||
{
|
{
|
||||||
auto response = success();
|
return body.isMember("active") && body["active"].isBool();
|
||||||
response["categories"] = repositories::resultToJson(
|
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
|
||||||
"SELECT * FROM report_categories ORDER BY name"));
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user) || !validation::hasString(body, "name"))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
|
if (!validation::hasString(body, "name"))
|
||||||
|
{
|
||||||
|
return failure("invalid_category");
|
||||||
|
}
|
||||||
|
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
|
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
|
||||||
@@ -108,19 +108,14 @@ Json::Value AdminService::createCategory(const auth::UserContext &user,
|
|||||||
return success();
|
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,
|
Json::Value AdminService::createArea(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
|
if (!auth::isAdmin(user))
|
||||||
!body["boundary_geojson"].isObject())
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
if (!validation::hasString(body, "name") || !body["boundary_geojson"].isObject())
|
||||||
{
|
{
|
||||||
return failure("invalid_area");
|
return failure("invalid_area");
|
||||||
}
|
}
|
||||||
@@ -135,29 +130,38 @@ Json::Value AdminService::createArea(const auth::UserContext &user,
|
|||||||
return success();
|
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,
|
Json::Value AdminService::createTeam(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
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"))
|
!validation::hasNumber(body, "team_category_id"))
|
||||||
{
|
{
|
||||||
return failure("invalid_team");
|
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())
|
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) "
|
"INSERT INTO teams (team_category_id, area_id, name, description) "
|
||||||
"VALUES (?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?)",
|
||||||
body["team_category_id"].asInt64(),
|
body["team_category_id"].asInt64(),
|
||||||
@@ -167,7 +171,7 @@ Json::Value AdminService::createTeam(const auth::UserContext &user,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
|
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
|
||||||
body["team_category_id"].asInt64(),
|
body["team_category_id"].asInt64(),
|
||||||
body["name"].asString(),
|
body["name"].asString(),
|
||||||
@@ -198,10 +202,21 @@ Json::Value AdminService::overviewStats(const auth::UserContext &user)
|
|||||||
"SELECT c.name AS category, COUNT(*) AS total "
|
"SELECT c.name AS category, COUNT(*) AS total "
|
||||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||||
"GROUP BY c.name ORDER BY total DESC"));
|
"GROUP BY c.name ORDER BY total DESC"));
|
||||||
response["reports_by_team"] = repositories::resultToJson(db->execSqlSync(
|
response["assignments_by_team"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT t.name AS team, COUNT(*) AS total "
|
"SELECT t.name AS team, COUNT(DISTINCT ass.id) AS total "
|
||||||
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
|
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
|
||||||
"GROUP BY t.name ORDER BY total DESC"));
|
"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(
|
response["monthly_reports"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
|
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
|
||||||
"FROM reports GROUP BY DATE_FORMAT(created_at, '%Y-%m') "
|
"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> &page,
|
||||||
const std::optional<std::string> &perPage)
|
const std::optional<std::string> &perPage)
|
||||||
{
|
{
|
||||||
if (!auth::canManageReports(user))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
@@ -262,7 +277,7 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
|||||||
long long flagId,
|
long long flagId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::canManageReports(user))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
@@ -324,6 +339,158 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
|||||||
return success();
|
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)
|
Json::Value AdminService::listUsers(const auth::UserContext &user)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user))
|
if (!auth::isAdmin(user))
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ namespace visione::services
|
|||||||
class AdminService
|
class AdminService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Json::Value listCategories();
|
|
||||||
Json::Value createCategory(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createCategory(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value listAreas();
|
|
||||||
Json::Value createArea(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createArea(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value listTeams();
|
|
||||||
Json::Value createTeam(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createTeam(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value overviewStats(const auth::UserContext &user);
|
Json::Value overviewStats(const auth::UserContext &user);
|
||||||
Json::Value listUsers(const auth::UserContext &user);
|
Json::Value listUsers(const auth::UserContext &user);
|
||||||
@@ -27,6 +24,15 @@ class AdminService
|
|||||||
Json::Value updateModeration(const auth::UserContext &user,
|
Json::Value updateModeration(const auth::UserContext &user,
|
||||||
long long flagId,
|
long long flagId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
Json::Value updateCategory(const auth::UserContext &user,
|
||||||
|
long long categoryId,
|
||||||
|
const Json::Value &body);
|
||||||
|
Json::Value updateArea(const auth::UserContext &user,
|
||||||
|
long long areaId,
|
||||||
|
const Json::Value &body);
|
||||||
|
Json::Value updateTeam(const auth::UserContext &user,
|
||||||
|
long long teamId,
|
||||||
|
const Json::Value &body);
|
||||||
Json::Value updateUserRoles(const auth::UserContext &user,
|
Json::Value updateUserRoles(const auth::UserContext &user,
|
||||||
long long targetUserId,
|
long long targetUserId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
|||||||
+312
-13
@@ -1,5 +1,6 @@
|
|||||||
#include "services/ReportService.h"
|
#include "services/ReportService.h"
|
||||||
|
|
||||||
|
#include "common/Crypto.h"
|
||||||
#include "common/GeoJson.h"
|
#include "common/GeoJson.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
#include "repositories/Sql.h"
|
#include "repositories/Sql.h"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
namespace visione::services
|
namespace visione::services
|
||||||
{
|
{
|
||||||
@@ -34,6 +36,14 @@ Json::Value success()
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value successWithLink(const std::string &url, const std::string &expiresAt)
|
||||||
|
{
|
||||||
|
auto body = success();
|
||||||
|
body["url"] = url;
|
||||||
|
body["expires_at"] = expiresAt;
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
bool isAllowedStatus(const std::string &status)
|
bool isAllowedStatus(const std::string &status)
|
||||||
{
|
{
|
||||||
static const std::set<std::string> statuses = {
|
static const std::set<std::string> statuses = {
|
||||||
@@ -56,6 +66,15 @@ bool canTransition(const std::string &from, const std::string &to)
|
|||||||
return it != transitions.end() && it->second.contains(to);
|
return it != transitions.end() && it->second.contains(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string maskedLastName(const std::string &lastName)
|
||||||
|
{
|
||||||
|
if (lastName.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<long long> findArea(double latitude, double longitude)
|
std::optional<long long> findArea(double latitude, double longitude)
|
||||||
{
|
{
|
||||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
@@ -109,6 +128,27 @@ bool canModifyReport(const auth::UserContext &user, long long reportId)
|
|||||||
return !result.empty();
|
return !result.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canOwnOrModifyReport(const auth::UserContext &user, long long reportId)
|
||||||
|
{
|
||||||
|
if (auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!auth::canOperateReports(user))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT assigned_operator_id FROM reports WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
if (result.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result[0]["assigned_operator_id"].isNull() ||
|
||||||
|
result[0]["assigned_operator_id"].as<long long>() == user.id;
|
||||||
|
}
|
||||||
|
|
||||||
bool canFlagReportContent(const auth::UserContext &user, long long reportId)
|
bool canFlagReportContent(const auth::UserContext &user, long long reportId)
|
||||||
{
|
{
|
||||||
if (!auth::canOperateReports(user))
|
if (!auth::canOperateReports(user))
|
||||||
@@ -260,6 +300,27 @@ std::optional<long long> assignedOperator(long long reportId)
|
|||||||
return rows[0]["assigned_operator_id"].as<long long>();
|
return rows[0]["assigned_operator_id"].as<long long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<long long> activeAssignmentId(long long reportId)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT id FROM assignments WHERE report_id = ? AND ended_at IS NULL "
|
||||||
|
"ORDER BY assigned_at DESC LIMIT 1",
|
||||||
|
reportId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return rows[0]["id"].as<long long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void revokeActiveLinksForReport(long long reportId)
|
||||||
|
{
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"UPDATE intervention_access_links SET revoked_at = NOW() "
|
||||||
|
"WHERE report_id = ? AND revoked_at IS NULL AND expires_at > NOW()",
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
|
||||||
void notifyReportOwner(long long reportId,
|
void notifyReportOwner(long long reportId,
|
||||||
const std::string &type,
|
const std::string &type,
|
||||||
const std::string &title,
|
const std::string &title,
|
||||||
@@ -271,6 +332,50 @@ void notifyReportOwner(long long reportId,
|
|||||||
UserService::createNotification(*owner, type, title, body, reportId);
|
UserService::createNotification(*owner, type, title, body, reportId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value buildInterventionSummaryRow(const drogon::orm::Row &row,
|
||||||
|
const drogon::orm::Result &mediaRows,
|
||||||
|
const drogon::orm::Result &messageRows,
|
||||||
|
bool publicView)
|
||||||
|
{
|
||||||
|
auto response = success();
|
||||||
|
Json::Value summary = repositories::rowToJson(row, reportSchema());
|
||||||
|
summary["category_name"] = row["category_name"].as<std::string>();
|
||||||
|
if (!row["area_name"].isNull())
|
||||||
|
{
|
||||||
|
summary["area_name"] = row["area_name"].as<std::string>();
|
||||||
|
}
|
||||||
|
if (!row["assigned_team_name"].isNull())
|
||||||
|
{
|
||||||
|
summary["assigned_team_name"] = row["assigned_team_name"].as<std::string>();
|
||||||
|
}
|
||||||
|
if (publicView)
|
||||||
|
{
|
||||||
|
summary["citizen_name"] = row["first_name"].as<std::string>() + " " +
|
||||||
|
maskedLastName(row["last_name"].as<std::string>());
|
||||||
|
if (!row["phone"].isNull())
|
||||||
|
{
|
||||||
|
summary["phone"] = row["phone"].as<std::string>();
|
||||||
|
}
|
||||||
|
response["summary"] = std::move(summary);
|
||||||
|
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
|
||||||
|
response["latest_messages"] =
|
||||||
|
repositories::resultToJson(messageRows, messageSchema());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
summary["first_name"] = row["first_name"].as<std::string>();
|
||||||
|
summary["last_name"] = row["last_name"].as<std::string>();
|
||||||
|
if (!row["phone"].isNull())
|
||||||
|
{
|
||||||
|
summary["phone"] = row["phone"].as<std::string>();
|
||||||
|
}
|
||||||
|
summary["email"] = row["email"].as<std::string>();
|
||||||
|
response["summary"] = std::move(summary);
|
||||||
|
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
|
||||||
|
response["latest_messages"] =
|
||||||
|
repositories::resultToJson(messageRows, messageSchema());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Json::Value ReportService::createReport(const auth::UserContext &user,
|
Json::Value ReportService::createReport(const auth::UserContext &user,
|
||||||
@@ -493,6 +598,44 @@ Json::Value ReportService::otherOperatorReports(
|
|||||||
return pagedReportResponse(result, pageValue, perPageValue);
|
return pagedReportResponse(result, pageValue, perPageValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listCategories(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
if (user.id == 0)
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto response = success();
|
||||||
|
response["categories"] = repositories::resultToJson(
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT * FROM report_categories WHERE active = TRUE ORDER BY name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listAreas()
|
||||||
|
{
|
||||||
|
auto response = success();
|
||||||
|
response["areas"] = repositories::resultToJson(
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT * FROM areas WHERE active = TRUE ORDER BY name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listOperatorTeams(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
if (!auth::canManageReports(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
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 "
|
||||||
|
"WHERE t.active = TRUE ORDER BY t.name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value ReportService::reportDetail(const auth::UserContext &user,
|
Json::Value ReportService::reportDetail(const auth::UserContext &user,
|
||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
@@ -549,19 +692,64 @@ Json::Value ReportService::interventionSummary(const auth::UserContext &user,
|
|||||||
{
|
{
|
||||||
return failure("report_not_found");
|
return failure("report_not_found");
|
||||||
}
|
}
|
||||||
auto response = success();
|
return buildInterventionSummaryRow(report[0],
|
||||||
response["summary"] = repositories::rowToJson(report[0], reportSchema());
|
db->execSqlSync(
|
||||||
response["media"] = repositories::resultToJson(db->execSqlSync(
|
|
||||||
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
||||||
reportId),
|
reportId),
|
||||||
reportMediaSchema());
|
db->execSqlSync(
|
||||||
response["latest_messages"] = repositories::resultToJson(db->execSqlSync(
|
|
||||||
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
||||||
"FROM messages m JOIN users u ON u.id = m.user_id "
|
"FROM messages m JOIN users u ON u.id = m.user_id "
|
||||||
"WHERE m.report_id = ? ORDER BY m.created_at DESC LIMIT 5",
|
"WHERE m.report_id = ? ORDER BY m.created_at DESC LIMIT 5",
|
||||||
reportId),
|
reportId),
|
||||||
messageSchema());
|
false);
|
||||||
return response;
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::publicInterventionSummary(const std::string &token)
|
||||||
|
{
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync(
|
||||||
|
"SELECT l.report_id "
|
||||||
|
"FROM intervention_access_links l "
|
||||||
|
"WHERE l.token_hash = ? AND l.revoked_at IS NULL AND l.expires_at > NOW() "
|
||||||
|
"LIMIT 1",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("intervention_link_not_found");
|
||||||
|
}
|
||||||
|
const auto reportId = rows[0]["report_id"].as<long long>();
|
||||||
|
const auto report = db->execSqlSync(
|
||||||
|
"SELECT r.*, c.name AS category_name, a.name AS area_name, "
|
||||||
|
"u.first_name, u.last_name, u.phone, "
|
||||||
|
"t.name AS assigned_team_name "
|
||||||
|
"FROM reports r "
|
||||||
|
"JOIN users u ON u.id = r.user_id "
|
||||||
|
"JOIN report_categories c ON c.id = r.category_id "
|
||||||
|
"LEFT JOIN areas a ON a.id = r.area_id "
|
||||||
|
"LEFT JOIN assignments ass ON ass.report_id = r.id AND ass.ended_at IS NULL "
|
||||||
|
"LEFT JOIN teams t ON t.id = ass.team_id "
|
||||||
|
"WHERE r.id = ?",
|
||||||
|
reportId);
|
||||||
|
if (report.empty())
|
||||||
|
{
|
||||||
|
return failure("intervention_link_not_found");
|
||||||
|
}
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE intervention_access_links SET last_accessed_at = NOW() "
|
||||||
|
"WHERE token_hash = ?",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
return buildInterventionSummaryRow(report[0],
|
||||||
|
db->execSqlSync(
|
||||||
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
||||||
|
reportId),
|
||||||
|
db->execSqlSync(
|
||||||
|
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
||||||
|
"FROM messages m JOIN users u ON u.id = m.user_id "
|
||||||
|
"WHERE m.report_id = ? AND u.id <> ? "
|
||||||
|
"ORDER BY m.created_at DESC LIMIT 5",
|
||||||
|
reportId,
|
||||||
|
report[0]["user_id"].as<long long>()),
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
||||||
@@ -725,31 +913,57 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|||||||
long long reportId,
|
long long reportId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!canModifyReport(user, reportId) || !validation::hasNumber(body, "team_id"))
|
if (!canOwnOrModifyReport(user, reportId))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
|
if (!validation::hasNumber(body, "team_id"))
|
||||||
|
{
|
||||||
|
return failure("invalid_team");
|
||||||
|
}
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto current = db->execSqlSync(
|
const auto current = db->execSqlSync(
|
||||||
"SELECT status FROM reports WHERE id = ?",
|
"SELECT status, assigned_operator_id FROM reports WHERE id = ?",
|
||||||
reportId);
|
reportId);
|
||||||
if (current.empty())
|
if (current.empty())
|
||||||
{
|
{
|
||||||
return failure("report_not_found");
|
return failure("report_not_found");
|
||||||
}
|
}
|
||||||
|
if (!auth::isAdmin(user) && !current[0]["assigned_operator_id"].isNull() &&
|
||||||
|
current[0]["assigned_operator_id"].as<long long>() != user.id)
|
||||||
|
{
|
||||||
|
return failure("report_owned_by_other_operator");
|
||||||
|
}
|
||||||
const auto oldStatus = current[0]["status"].as<std::string>();
|
const auto oldStatus = current[0]["status"].as<std::string>();
|
||||||
if (!canTransition(oldStatus, "ASSIGNED"))
|
if (oldStatus != "OPEN" && !canTransition(oldStatus, "ASSIGNED"))
|
||||||
{
|
{
|
||||||
return failure("invalid_status_transition");
|
return failure("invalid_status_transition");
|
||||||
}
|
}
|
||||||
const auto team = db->execSqlSync(
|
const auto team = db->execSqlSync(
|
||||||
"SELECT id FROM teams WHERE id = ? AND active = TRUE",
|
"SELECT id, active FROM teams WHERE id = ?",
|
||||||
body["team_id"].asInt64());
|
body["team_id"].asInt64());
|
||||||
if (team.empty())
|
if (team.empty())
|
||||||
{
|
{
|
||||||
return failure("team_not_found");
|
return failure("team_not_found");
|
||||||
}
|
}
|
||||||
|
if (!team[0]["active"].as<bool>())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStatus == "OPEN")
|
||||||
|
{
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE reports SET assigned_operator_id = ?, status = 'ASSIGNED' WHERE id = ?",
|
||||||
|
user.id,
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"UPDATE assignments SET ended_at = NOW() "
|
"UPDATE assignments SET ended_at = NOW() "
|
||||||
@@ -761,8 +975,6 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|||||||
reportId,
|
reportId,
|
||||||
body["team_id"].asInt64(),
|
body["team_id"].asInt64(),
|
||||||
user.id);
|
user.id);
|
||||||
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
|
|
||||||
reportId);
|
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO report_status_history "
|
"INSERT INTO report_status_history "
|
||||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||||
@@ -770,6 +982,7 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|||||||
reportId,
|
reportId,
|
||||||
oldStatus,
|
oldStatus,
|
||||||
user.id);
|
user.id);
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
notifyReportOwner(reportId,
|
notifyReportOwner(reportId,
|
||||||
"team_assigned",
|
"team_assigned",
|
||||||
"Team assigned",
|
"Team assigned",
|
||||||
@@ -895,6 +1108,92 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::teamSuggestions(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!auth::canManageReports(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto reportRows = db->execSqlSync(
|
||||||
|
"SELECT id, category_id, area_id FROM reports WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
if (reportRows.empty())
|
||||||
|
{
|
||||||
|
return failure("report_not_found");
|
||||||
|
}
|
||||||
|
const auto result = db->execSqlSync(
|
||||||
|
"SELECT t.*, tc.name AS category_name, a.name AS area_name, "
|
||||||
|
"CASE "
|
||||||
|
" WHEN t.area_id = r.area_id THEN 'area_and_category' "
|
||||||
|
" WHEN t.area_id IS NULL THEN 'category_only' "
|
||||||
|
" ELSE 'fallback_global' "
|
||||||
|
"END AS match_reason, "
|
||||||
|
"CASE WHEN t.area_id = r.area_id THEN 0 "
|
||||||
|
" WHEN t.area_id IS NULL THEN 1 "
|
||||||
|
" ELSE 2 END AS rank_value "
|
||||||
|
"FROM reports r "
|
||||||
|
"JOIN report_category_team_categories map ON map.report_category_id = r.category_id "
|
||||||
|
"JOIN teams t ON t.team_category_id = map.team_category_id AND t.active = TRUE "
|
||||||
|
"JOIN team_categories tc ON tc.id = t.team_category_id "
|
||||||
|
"LEFT JOIN areas a ON a.id = t.area_id "
|
||||||
|
"WHERE r.id = ? AND (t.area_id = r.area_id OR t.area_id IS NULL) "
|
||||||
|
"ORDER BY rank_value, t.name",
|
||||||
|
reportId);
|
||||||
|
|
||||||
|
auto response = success();
|
||||||
|
response["suggestions"] = Json::arrayValue;
|
||||||
|
for (size_t i = 0; i < result.size(); ++i)
|
||||||
|
{
|
||||||
|
auto item = repositories::rowToJson(result[i]);
|
||||||
|
item["is_recommended"] = (i == 0);
|
||||||
|
response["suggestions"].append(std::move(item));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::createInterventionLink(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!canModifyReport(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
const auto assignmentId = activeAssignmentId(reportId);
|
||||||
|
if (!assignmentId)
|
||||||
|
{
|
||||||
|
return failure("invalid_status_transition");
|
||||||
|
}
|
||||||
|
auto token = crypto::randomToken();
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO intervention_access_links "
|
||||||
|
"(report_id, assignment_id, token_hash, created_by_user_id, expires_at) "
|
||||||
|
"VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))",
|
||||||
|
reportId,
|
||||||
|
*assignmentId,
|
||||||
|
crypto::hashSecret(token),
|
||||||
|
user.id);
|
||||||
|
const auto expiresRows = db->execSqlSync(
|
||||||
|
"SELECT expires_at FROM intervention_access_links WHERE token_hash = ?",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
return successWithLink("/api/v1/interventions/" + token,
|
||||||
|
expiresRows[0]["expires_at"].as<std::string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::revokeInterventionLink(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!canModifyReport(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value ReportService::openDataReports(const std::optional<std::string> &page,
|
Json::Value ReportService::openDataReports(const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage)
|
const std::optional<std::string> &perPage)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ class ReportService
|
|||||||
Json::Value assignedReports(const auth::UserContext &user,
|
Json::Value assignedReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
|
Json::Value listAreas();
|
||||||
Json::Value openReports(const auth::UserContext &user,
|
Json::Value openReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
Json::Value otherOperatorReports(const auth::UserContext &user,
|
Json::Value otherOperatorReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
|
Json::Value listCategories(const auth::UserContext &user);
|
||||||
|
Json::Value listOperatorTeams(const auth::UserContext &user);
|
||||||
Json::Value reportDetail(const auth::UserContext &user, long long reportId);
|
Json::Value reportDetail(const auth::UserContext &user, long long reportId);
|
||||||
Json::Value interventionSummary(const auth::UserContext &user, long long reportId);
|
Json::Value interventionSummary(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value publicInterventionSummary(const std::string &token);
|
||||||
Json::Value updateStatus(const auth::UserContext &user,
|
Json::Value updateStatus(const auth::UserContext &user,
|
||||||
long long reportId,
|
long long reportId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
@@ -48,6 +52,9 @@ class ReportService
|
|||||||
long long reportId,
|
long long reportId,
|
||||||
long long messageId,
|
long long messageId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
Json::Value teamSuggestions(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value createInterventionLink(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value revokeInterventionLink(const auth::UserContext &user, long long reportId);
|
||||||
Json::Value openDataReports(const std::optional<std::string> &page,
|
Json::Value openDataReports(const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
Json::Value canAddMedia(const auth::UserContext &user, long long reportId);
|
Json::Value canAddMedia(const auth::UserContext &user, long long reportId);
|
||||||
|
|||||||
Reference in New Issue
Block a user