1330 lines
43 KiB
C++
1330 lines
43 KiB
C++
#include "services/ReportService.h"
|
|
|
|
#include "common/Crypto.h"
|
|
#include "common/GeoJson.h"
|
|
#include "common/Validation.h"
|
|
#include "repositories/Sql.h"
|
|
#include "services/UserService.h"
|
|
|
|
#include <drogon/drogon.h>
|
|
#include <drogon/orm/Exception.h>
|
|
|
|
#include <map>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
|
|
namespace visione::services
|
|
{
|
|
namespace
|
|
{
|
|
using repositories::JsonFieldType;
|
|
using repositories::JsonRowSchema;
|
|
|
|
Json::Value failure(const std::string &code)
|
|
{
|
|
Json::Value body;
|
|
body["ok"] = false;
|
|
body["error"] = code;
|
|
return body;
|
|
}
|
|
|
|
Json::Value success()
|
|
{
|
|
Json::Value body;
|
|
body["ok"] = true;
|
|
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)
|
|
{
|
|
static const std::set<std::string> statuses = {
|
|
"OPEN", "UNDER_REVIEW", "ASSIGNED", "IN_PROGRESS",
|
|
"RESOLVED", "REJECTED", "CLOSED"};
|
|
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::string maskedLastName(const std::string &lastName)
|
|
{
|
|
if (lastName.empty())
|
|
{
|
|
return {};
|
|
}
|
|
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
|
|
}
|
|
|
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
|
{
|
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
|
code);
|
|
if (rows.empty())
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
Json::CharReaderBuilder builder;
|
|
Json::Value geojson;
|
|
std::string errors;
|
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
return geojson;
|
|
}
|
|
|
|
bool insideAllowedBoundary(double latitude, double longitude)
|
|
{
|
|
const auto boundary = loadAllowedBoundary("BARI");
|
|
return boundary && geojson::pointInGeometry(latitude, longitude, *boundary);
|
|
}
|
|
|
|
std::optional<long long> findArea(double latitude, double longitude)
|
|
{
|
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
|
"FROM areas");
|
|
|
|
Json::CharReaderBuilder builder;
|
|
for (const auto &row : result)
|
|
{
|
|
Json::Value geojson;
|
|
std::string errors;
|
|
const auto raw = row["geojson"].as<std::string>();
|
|
std::istringstream stream(raw);
|
|
if (Json::parseFromStream(builder, stream, &geojson, &errors) &&
|
|
geojson::pointInGeometry(latitude, longitude, geojson))
|
|
{
|
|
return row["id"].as<long long>();
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool canAccessReport(const auth::UserContext &user, long long reportId)
|
|
{
|
|
if (auth::canManageReports(user))
|
|
{
|
|
return true;
|
|
}
|
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT id FROM reports WHERE id = ? AND user_id = ?",
|
|
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();
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!auth::canOperateReports(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();
|
|
}
|
|
|
|
bool ownsReport(const auth::UserContext &user, long long reportId)
|
|
{
|
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT id FROM reports WHERE id = ? AND user_id = ?",
|
|
reportId,
|
|
user.id);
|
|
return !result.empty();
|
|
}
|
|
|
|
bool canInteractWithReport(const auth::UserContext &user, long long reportId)
|
|
{
|
|
return ownsReport(user, reportId) || canModifyReport(user, reportId);
|
|
}
|
|
|
|
bool canCreateCitizenReport(const auth::UserContext &user)
|
|
{
|
|
return !auth::canManageReports(user);
|
|
}
|
|
|
|
int boundedPage(const std::optional<std::string> &value)
|
|
{
|
|
if (!value)
|
|
{
|
|
return 1;
|
|
}
|
|
try
|
|
{
|
|
return std::max(1, std::stoi(*value));
|
|
}
|
|
catch (...)
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int boundedPerPage(const std::optional<std::string> &value)
|
|
{
|
|
if (!value)
|
|
{
|
|
return 50;
|
|
}
|
|
try
|
|
{
|
|
return std::clamp(std::stoi(*value), 1, 100);
|
|
}
|
|
catch (...)
|
|
{
|
|
return 50;
|
|
}
|
|
}
|
|
|
|
const JsonRowSchema &reportSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"user_id", JsonFieldType::UInt64},
|
|
{"category_id", JsonFieldType::UInt64},
|
|
{"area_id", JsonFieldType::UInt64},
|
|
{"assigned_operator_id", JsonFieldType::UInt64},
|
|
{"latitude", JsonFieldType::Double},
|
|
{"longitude", JsonFieldType::Double}};
|
|
return schema;
|
|
}
|
|
|
|
const JsonRowSchema &reportHistorySchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"report_id", JsonFieldType::UInt64},
|
|
{"changed_by_user_id", JsonFieldType::UInt64}};
|
|
return schema;
|
|
}
|
|
|
|
const JsonRowSchema &reportMediaSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"report_id", JsonFieldType::UInt64},
|
|
{"size_bytes", JsonFieldType::UInt64}};
|
|
return schema;
|
|
}
|
|
|
|
const JsonRowSchema &messageSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"report_id", JsonFieldType::UInt64},
|
|
{"user_id", JsonFieldType::UInt64}};
|
|
return schema;
|
|
}
|
|
|
|
const JsonRowSchema &openDataReportSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"category_id", JsonFieldType::UInt64},
|
|
{"area_id", JsonFieldType::UInt64},
|
|
{"latitude", JsonFieldType::Double},
|
|
{"longitude", JsonFieldType::Double}};
|
|
return schema;
|
|
}
|
|
|
|
Json::Value pagedReportResponse(const drogon::orm::Result &result,
|
|
int page,
|
|
int perPage)
|
|
{
|
|
auto response = success();
|
|
response["page"] = page;
|
|
response["per_page"] = perPage;
|
|
response["reports"] = repositories::resultToJson(result, reportSchema());
|
|
return response;
|
|
}
|
|
|
|
std::optional<long long> reportOwner(long long reportId)
|
|
{
|
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT user_id FROM reports WHERE id = ?",
|
|
reportId);
|
|
if (rows.empty())
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
return rows[0]["user_id"].as<long long>();
|
|
}
|
|
|
|
std::optional<long long> assignedOperator(long long reportId)
|
|
{
|
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT assigned_operator_id FROM reports WHERE id = ?",
|
|
reportId);
|
|
if (rows.empty() || rows[0]["assigned_operator_id"].isNull())
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
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,
|
|
const std::string &type,
|
|
const std::string &title,
|
|
const std::string &body)
|
|
{
|
|
const auto owner = reportOwner(reportId);
|
|
if (owner)
|
|
{
|
|
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
|
|
|
|
Json::Value ReportService::createReport(const auth::UserContext &user,
|
|
const Json::Value &body)
|
|
{
|
|
if (!canCreateCitizenReport(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
|
|
const auto missing =
|
|
validation::missingStrings(body, {"address", "description"});
|
|
if (!missing.empty() || !validation::hasNumber(body, "category_id") ||
|
|
!validation::hasNumber(body, "latitude") ||
|
|
!validation::hasNumber(body, "longitude"))
|
|
{
|
|
return failure("invalid_report_payload");
|
|
}
|
|
|
|
const auto latitude = body["latitude"].asDouble();
|
|
const auto longitude = body["longitude"].asDouble();
|
|
if (!insideAllowedBoundary(latitude, longitude))
|
|
{
|
|
return failure("coordinates_outside_allowed_boundary");
|
|
}
|
|
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(
|
|
"INSERT INTO reports "
|
|
"(user_id, category_id, area_id, latitude, longitude, address, "
|
|
"description) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
user.id,
|
|
body["category_id"].asInt64(),
|
|
*areaId,
|
|
latitude,
|
|
longitude,
|
|
body["address"].asString(),
|
|
body["description"].asString());
|
|
}
|
|
else
|
|
{
|
|
db->execSqlSync(
|
|
"INSERT INTO reports "
|
|
"(user_id, category_id, latitude, longitude, address, description) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
user.id,
|
|
body["category_id"].asInt64(),
|
|
latitude,
|
|
longitude,
|
|
body["address"].asString(),
|
|
body["description"].asString());
|
|
}
|
|
|
|
const auto inserted = db->execSqlSync("SELECT LAST_INSERT_ID() AS id");
|
|
const auto reportId = inserted[0]["id"].as<long long>();
|
|
db->execSqlSync(
|
|
"INSERT INTO report_status_history "
|
|
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
|
"VALUES (?, NULL, 'OPEN', ?, 'Report created')",
|
|
reportId,
|
|
user.id);
|
|
|
|
if (body["media"].isArray())
|
|
{
|
|
size_t mediaCount = 0;
|
|
for (const auto &item : body["media"])
|
|
{
|
|
if (mediaCount >= 5)
|
|
{
|
|
break;
|
|
}
|
|
if (item.isString() && !item.asString().empty())
|
|
{
|
|
db->execSqlSync(
|
|
"INSERT INTO report_photos (report_id, url, media_kind) "
|
|
"VALUES (?, ?, 'IMAGE')",
|
|
reportId,
|
|
item.asString());
|
|
++mediaCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto response = success();
|
|
response["report_id"] = Json::Int64(reportId);
|
|
if (areaId)
|
|
{
|
|
response["area_id"] = Json::Int64(*areaId);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::listReports(const auth::UserContext &user,
|
|
const std::optional<std::string> &status,
|
|
const std::optional<std::string> &categoryId,
|
|
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 (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 "
|
|
"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
|
|
{
|
|
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.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();
|
|
response["reports"] = repositories::resultToJson(result, reportSchema());
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::assignedReports(const auth::UserContext &user,
|
|
const std::optional<std::string> &page,
|
|
const std::optional<std::string> &perPage)
|
|
{
|
|
if (!auth::canManageReports(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
const auto pageValue = boundedPage(page);
|
|
const auto perPageValue = boundedPerPage(perPage);
|
|
const auto offset = (pageValue - 1) * perPageValue;
|
|
const auto result = drogon::app().getDbClient()->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.assigned_operator_id = ? "
|
|
"ORDER BY r.updated_at DESC LIMIT ? OFFSET ?",
|
|
user.id,
|
|
perPageValue,
|
|
offset);
|
|
return pagedReportResponse(result, pageValue, perPageValue);
|
|
}
|
|
|
|
Json::Value ReportService::openReports(const auth::UserContext &user,
|
|
const std::optional<std::string> &page,
|
|
const std::optional<std::string> &perPage)
|
|
{
|
|
if (!auth::canManageReports(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
const auto pageValue = boundedPage(page);
|
|
const auto perPageValue = boundedPerPage(perPage);
|
|
const auto offset = (pageValue - 1) * perPageValue;
|
|
const auto result = drogon::app().getDbClient()->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 = 'OPEN' AND r.assigned_operator_id IS NULL "
|
|
"ORDER BY r.created_at ASC LIMIT ? OFFSET ?",
|
|
perPageValue,
|
|
offset);
|
|
return pagedReportResponse(result, pageValue, perPageValue);
|
|
}
|
|
|
|
Json::Value ReportService::otherOperatorReports(
|
|
const auth::UserContext &user,
|
|
const std::optional<std::string> &page,
|
|
const std::optional<std::string> &perPage)
|
|
{
|
|
if (!auth::canManageReports(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
const auto pageValue = boundedPage(page);
|
|
const auto perPageValue = boundedPerPage(perPage);
|
|
const auto offset = (pageValue - 1) * perPageValue;
|
|
const auto result = drogon::app().getDbClient()->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.assigned_operator_id IS NOT NULL "
|
|
"AND r.assigned_operator_id <> ? "
|
|
"ORDER BY r.updated_at DESC LIMIT ? OFFSET ?",
|
|
user.id,
|
|
perPageValue,
|
|
offset);
|
|
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 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,
|
|
long long reportId)
|
|
{
|
|
if (!canAccessReport(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
|
|
auto db = drogon::app().getDbClient();
|
|
const auto report = 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.id = ?",
|
|
reportId);
|
|
if (report.empty())
|
|
{
|
|
return failure("report_not_found");
|
|
}
|
|
|
|
auto response = success();
|
|
response["report"] = repositories::rowToJson(report[0], reportSchema());
|
|
response["history"] = repositories::resultToJson(db->execSqlSync(
|
|
"SELECT * FROM report_status_history WHERE report_id = ? ORDER BY id",
|
|
reportId),
|
|
reportHistorySchema());
|
|
response["media"] = repositories::resultToJson(db->execSqlSync(
|
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
|
reportId),
|
|
reportMediaSchema());
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::interventionSummary(const auth::UserContext &user,
|
|
long long reportId)
|
|
{
|
|
if (!auth::canManageReports(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
auto db = drogon::app().getDbClient();
|
|
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, u.email, "
|
|
"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("report_not_found");
|
|
}
|
|
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 = ? ORDER BY m.created_at DESC LIMIT 5",
|
|
reportId),
|
|
false);
|
|
}
|
|
|
|
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,
|
|
long long reportId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!canModifyReport(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
if (!validation::hasString(body, "status") ||
|
|
!isAllowedStatus(body["status"].asString()))
|
|
{
|
|
return failure("invalid_status");
|
|
}
|
|
|
|
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>();
|
|
const auto newStatus = body["status"].asString();
|
|
if (!canTransition(oldStatus, newStatus))
|
|
{
|
|
return failure("invalid_status_transition");
|
|
}
|
|
|
|
db->execSqlSync("UPDATE reports SET status = ? WHERE id = ?",
|
|
newStatus,
|
|
reportId);
|
|
db->execSqlSync(
|
|
"INSERT INTO report_status_history "
|
|
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
reportId,
|
|
oldStatus,
|
|
newStatus,
|
|
user.id,
|
|
body.get("note", "").asString());
|
|
|
|
notifyReportOwner(reportId,
|
|
"report_status_changed",
|
|
"Report status changed",
|
|
"Your report status is now " + newStatus + ".");
|
|
if (const auto assignee = assignedOperator(reportId);
|
|
assignee && *assignee != user.id)
|
|
{
|
|
UserService::createNotification(*assignee,
|
|
"report_status_changed",
|
|
"Assigned report status changed",
|
|
"A report assigned to you is now " + newStatus + ".",
|
|
reportId);
|
|
}
|
|
if (newStatus == "RESOLVED" || newStatus == "CLOSED")
|
|
{
|
|
UserService::awardReportOwner(reportId, "report_resolved", 10);
|
|
}
|
|
|
|
return success();
|
|
}
|
|
|
|
Json::Value ReportService::takeReport(const auth::UserContext &user,
|
|
long long reportId)
|
|
{
|
|
if (!auth::canOperateReports(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);
|
|
notifyReportOwner(reportId,
|
|
"report_taken",
|
|
"Report taken in charge",
|
|
"An operator is reviewing your report.");
|
|
return success();
|
|
}
|
|
|
|
Json::Value ReportService::addMessage(const auth::UserContext &user,
|
|
long long reportId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!canInteractWithReport(user, reportId) ||
|
|
!validation::hasString(body, "body"))
|
|
{
|
|
return failure("invalid_message");
|
|
}
|
|
|
|
drogon::app().getDbClient()->execSqlSync(
|
|
"INSERT INTO messages (report_id, user_id, body) VALUES (?, ?, ?)",
|
|
reportId,
|
|
user.id,
|
|
body["body"].asString());
|
|
if (const auto owner = reportOwner(reportId); owner && *owner != user.id)
|
|
{
|
|
UserService::createNotification(*owner,
|
|
"new_message",
|
|
"New report message",
|
|
"There is a new message on your report.",
|
|
reportId);
|
|
}
|
|
if (const auto assignee = assignedOperator(reportId);
|
|
assignee && *assignee != user.id)
|
|
{
|
|
UserService::createNotification(*assignee,
|
|
"new_message",
|
|
"New report message",
|
|
"There is a new message on an assigned report.",
|
|
reportId);
|
|
}
|
|
return success();
|
|
}
|
|
|
|
Json::Value ReportService::listMessages(const auth::UserContext &user,
|
|
long long reportId)
|
|
{
|
|
if (!canInteractWithReport(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
|
|
auto response = success();
|
|
response["messages"] = repositories::resultToJson(
|
|
drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT m.*, u.first_name, u.last_name FROM messages m "
|
|
"JOIN users u ON u.id = m.user_id WHERE m.report_id = ? "
|
|
"ORDER BY m.created_at",
|
|
reportId),
|
|
messageSchema());
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|
long long reportId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!canOwnOrModifyReport(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
if (!validation::hasNumber(body, "team_id"))
|
|
{
|
|
return failure("invalid_team");
|
|
}
|
|
|
|
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 (!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>();
|
|
if (oldStatus != "OPEN" && !canTransition(oldStatus, "ASSIGNED"))
|
|
{
|
|
return failure("invalid_status_transition");
|
|
}
|
|
const auto team = db->execSqlSync(
|
|
"SELECT id, active FROM teams WHERE id = ?",
|
|
body["team_id"].asInt64());
|
|
if (team.empty())
|
|
{
|
|
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(
|
|
"UPDATE assignments SET ended_at = NOW() "
|
|
"WHERE report_id = ? AND ended_at IS NULL",
|
|
reportId);
|
|
db->execSqlSync(
|
|
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
|
|
"VALUES (?, ?, ?)",
|
|
reportId,
|
|
body["team_id"].asInt64(),
|
|
user.id);
|
|
db->execSqlSync(
|
|
"INSERT INTO report_status_history "
|
|
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
|
"VALUES (?, ?, 'ASSIGNED', ?, 'Team assigned')",
|
|
reportId,
|
|
oldStatus,
|
|
user.id);
|
|
revokeActiveLinksForReport(reportId);
|
|
notifyReportOwner(reportId,
|
|
"team_assigned",
|
|
"Team assigned",
|
|
"A municipal team has been assigned to your report.");
|
|
if (const auto assignee = assignedOperator(reportId);
|
|
assignee && *assignee != user.id)
|
|
{
|
|
UserService::createNotification(*assignee,
|
|
"team_assigned",
|
|
"Team assigned",
|
|
"A team has been assigned to your report.",
|
|
reportId);
|
|
}
|
|
return success();
|
|
}
|
|
|
|
Json::Value ReportService::addFeedback(const auth::UserContext &user,
|
|
long long reportId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!validation::hasNumber(body, "rating"))
|
|
{
|
|
return failure("missing_rating");
|
|
}
|
|
const auto rating = body["rating"].asInt();
|
|
if (rating < 1 || rating > 5)
|
|
{
|
|
return failure("invalid_rating");
|
|
}
|
|
|
|
auto db = drogon::app().getDbClient();
|
|
const auto report = db->execSqlSync(
|
|
"SELECT user_id, status FROM reports WHERE id = ?",
|
|
reportId);
|
|
if (report.empty() || report[0]["user_id"].as<long long>() != user.id)
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
if (report[0]["status"].as<std::string>() != "RESOLVED")
|
|
{
|
|
return failure("report_not_resolved");
|
|
}
|
|
|
|
const auto existingFeedback = db->execSqlSync(
|
|
"SELECT id FROM feedback WHERE report_id = ?",
|
|
reportId);
|
|
if (!existingFeedback.empty())
|
|
{
|
|
return failure("feedback_already_exists");
|
|
}
|
|
|
|
try
|
|
{
|
|
db->execSqlSync(
|
|
"INSERT INTO feedback (report_id, user_id, rating, comment) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
reportId,
|
|
user.id,
|
|
rating,
|
|
body.get("comment", "").asString());
|
|
}
|
|
catch (const drogon::orm::DrogonDbException &)
|
|
{
|
|
return failure("feedback_already_exists");
|
|
}
|
|
return success();
|
|
}
|
|
|
|
Json::Value ReportService::flagReport(const auth::UserContext &user,
|
|
long long reportId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!validation::hasString(body, "reason"))
|
|
{
|
|
return failure("invalid_flag");
|
|
}
|
|
|
|
if (!canFlagReportContent(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
|
|
auto db = drogon::app().getDbClient();
|
|
db->execSqlSync(
|
|
"INSERT INTO moderation_flags "
|
|
"(content_type, report_id, reason, details, created_by_user_id) "
|
|
"VALUES ('REPORT', ?, ?, ?, ?)",
|
|
reportId,
|
|
body["reason"].asString(),
|
|
body.get("details", "").asString(),
|
|
user.id);
|
|
auto response = success();
|
|
response["flag_id"] =
|
|
Json::Int64(db->execSqlSync("SELECT LAST_INSERT_ID() AS id")[0]["id"]
|
|
.as<long long>());
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::flagMessage(const auth::UserContext &user,
|
|
long long reportId,
|
|
long long messageId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!validation::hasString(body, "reason"))
|
|
{
|
|
return failure("invalid_flag");
|
|
}
|
|
if (!canFlagReportContent(user, reportId))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
auto db = drogon::app().getDbClient();
|
|
const auto message = db->execSqlSync(
|
|
"SELECT id, user_id FROM messages WHERE id = ? AND report_id = ?",
|
|
messageId,
|
|
reportId);
|
|
if (message.empty())
|
|
{
|
|
return failure("message_not_found");
|
|
}
|
|
if (message[0]["user_id"].as<long long>() == user.id)
|
|
{
|
|
return failure("cannot_flag_own_message");
|
|
}
|
|
db->execSqlSync(
|
|
"INSERT INTO moderation_flags "
|
|
"(content_type, report_id, message_id, reason, details, created_by_user_id) "
|
|
"VALUES ('MESSAGE', ?, ?, ?, ?, ?)",
|
|
reportId,
|
|
messageId,
|
|
body["reason"].asString(),
|
|
body.get("details", "").asString(),
|
|
user.id);
|
|
auto response = success();
|
|
response["flag_id"] =
|
|
Json::Int64(db->execSqlSync("SELECT LAST_INSERT_ID() AS id")[0]["id"]
|
|
.as<long long>());
|
|
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,
|
|
const std::optional<std::string> &perPage)
|
|
{
|
|
const auto pageValue = boundedPage(page);
|
|
const auto perPageValue = boundedPerPage(perPage);
|
|
const auto offset = (pageValue - 1) * perPageValue;
|
|
auto response = success();
|
|
response["page"] = pageValue;
|
|
response["per_page"] = perPageValue;
|
|
response["reports"] = repositories::resultToJson(
|
|
drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT r.id, r.category_id, c.name AS category_name, r.area_id, "
|
|
"a.name AS area_name, r.latitude, r.longitude, r.status, "
|
|
"r.created_at, r.updated_at "
|
|
"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 IN ('RESOLVED', 'CLOSED') "
|
|
"ORDER BY r.updated_at DESC LIMIT ? OFFSET ?",
|
|
perPageValue,
|
|
offset),
|
|
openDataReportSchema());
|
|
if (const auto boundary = loadAllowedBoundary("BARI"))
|
|
{
|
|
Json::Value filtered(Json::arrayValue);
|
|
for (const auto &report : response["reports"])
|
|
{
|
|
if (geojson::pointInGeometry(report["latitude"].asDouble(),
|
|
report["longitude"].asDouble(),
|
|
*boundary))
|
|
{
|
|
filtered.append(report);
|
|
}
|
|
}
|
|
response["reports"] = std::move(filtered);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::canAddMedia(const auth::UserContext &user,
|
|
long long reportId)
|
|
{
|
|
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("media_limit_reached");
|
|
}
|
|
auto response = success();
|
|
response["remaining_media_slots"] =
|
|
5 - (count.empty() ? 0 : count[0]["total"].as<int>());
|
|
return response;
|
|
}
|
|
|
|
Json::Value ReportService::addMedia(const auth::UserContext &user,
|
|
long long reportId,
|
|
const std::string &url,
|
|
const std::string &originalFilename,
|
|
const std::string &contentType,
|
|
const std::string &mediaKind,
|
|
size_t sizeBytes)
|
|
{
|
|
auto allowed = canAddMedia(user, reportId);
|
|
if (!allowed.get("ok", false).asBool() || url.empty())
|
|
{
|
|
return allowed;
|
|
}
|
|
|
|
drogon::app().getDbClient()->execSqlSync(
|
|
"INSERT INTO report_photos "
|
|
"(report_id, url, original_filename, content_type, media_kind, size_bytes) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
reportId,
|
|
url,
|
|
originalFilename,
|
|
contentType,
|
|
mediaKind,
|
|
static_cast<unsigned long long>(sizeBytes));
|
|
return success();
|
|
}
|
|
} // namespace visione::services
|