Add report workflow
This commit is contained in:
@@ -62,6 +62,28 @@ void sendService(Json::Value body,
|
||||
: drogon::k400BadRequest;
|
||||
callback(visione::http::json(std::move(body), status));
|
||||
}
|
||||
|
||||
bool isAllowedImage(const drogon::HttpFile &file)
|
||||
{
|
||||
const auto type = file.getContentType();
|
||||
return type == drogon::CT_IMAGE_JPG || type == drogon::CT_IMAGE_PNG ||
|
||||
type == drogon::CT_IMAGE_WEBP;
|
||||
}
|
||||
|
||||
std::string imageContentType(const drogon::HttpFile &file)
|
||||
{
|
||||
switch (file.getContentType())
|
||||
{
|
||||
case drogon::CT_IMAGE_JPG:
|
||||
return "image/jpeg";
|
||||
case drogon::CT_IMAGE_PNG:
|
||||
return "image/png";
|
||||
case drogon::CT_IMAGE_WEBP:
|
||||
return "image/webp";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ReportController::create(
|
||||
@@ -131,6 +153,20 @@ void ReportController::updateStatus(
|
||||
sendService(service.updateStatus(*user, reportId, body), std::move(callback));
|
||||
}
|
||||
|
||||
void ReportController::takeReport(
|
||||
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.takeReport(*user, reportId), std::move(callback));
|
||||
}
|
||||
|
||||
void ReportController::addMessage(
|
||||
const drogon::HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
@@ -216,9 +252,33 @@ void ReportController::uploadPhotos(
|
||||
|
||||
const auto reportDir =
|
||||
std::filesystem::path(reportsPath) / std::to_string(reportId);
|
||||
std::filesystem::create_directories(reportDir);
|
||||
|
||||
visione::services::ReportService service;
|
||||
auto allowed = service.canAddPhoto(*user, reportId);
|
||||
if (!allowed.get("ok", false).asBool())
|
||||
{
|
||||
callback(visione::http::json(std::move(allowed), drogon::k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.getFiles().size() >
|
||||
static_cast<size_t>(allowed.get("remaining_photo_slots", 0).asInt()))
|
||||
{
|
||||
callback(visione::http::error("too_many_photos", drogon::k400BadRequest));
|
||||
return;
|
||||
}
|
||||
for (const auto &file : parser.getFiles())
|
||||
{
|
||||
if (!isAllowedImage(file))
|
||||
{
|
||||
callback(visione::http::error("unsupported_photo_type",
|
||||
drogon::k400BadRequest));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(reportDir);
|
||||
|
||||
Json::Value response;
|
||||
response["ok"] = true;
|
||||
response["photos"] = Json::arrayValue;
|
||||
@@ -241,7 +301,12 @@ void ReportController::uploadPhotos(
|
||||
|
||||
const auto url = publicBasePath + "/reports/" + std::to_string(reportId) +
|
||||
"/" + fileName;
|
||||
auto saved = service.addPhoto(*user, reportId, url);
|
||||
auto saved = service.addPhoto(*user,
|
||||
reportId,
|
||||
url,
|
||||
file.getFileName(),
|
||||
imageContentType(file),
|
||||
file.fileLength());
|
||||
if (!saved.get("ok", false).asBool())
|
||||
{
|
||||
callback(visione::http::json(std::move(saved), drogon::k403Forbidden));
|
||||
|
||||
@@ -12,6 +12,9 @@ class ReportController : public drogon::HttpController<ReportController>
|
||||
ADD_METHOD_TO(ReportController::updateStatus,
|
||||
"/api/v1/reports/{1}/status",
|
||||
drogon::Patch);
|
||||
ADD_METHOD_TO(ReportController::takeReport,
|
||||
"/api/v1/reports/{1}/take",
|
||||
drogon::Post);
|
||||
ADD_METHOD_TO(ReportController::addMessage,
|
||||
"/api/v1/reports/{1}/messages",
|
||||
drogon::Post);
|
||||
@@ -40,6 +43,10 @@ class ReportController : public drogon::HttpController<ReportController>
|
||||
const drogon::HttpRequestPtr &req,
|
||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||
long long reportId);
|
||||
void takeReport(
|
||||
const drogon::HttpRequestPtr &req,
|
||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||
long long reportId);
|
||||
void addMessage(
|
||||
const drogon::HttpRequestPtr &req,
|
||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||
|
||||
+2
-1
@@ -25,11 +25,12 @@ Authorization: Bearer <token>
|
||||
- `GET /reports`: list reports with optional `status`, `category_id`, `area_id`.
|
||||
- `GET /reports/{id}`: report detail.
|
||||
- `PATCH /reports/{id}/status`: update status.
|
||||
- `POST /reports/{id}/take`: operator/admin takes an open report.
|
||||
- `POST /reports/{id}/messages`: write a report message.
|
||||
- `GET /reports/{id}/messages`: list report messages.
|
||||
- `POST /reports/{id}/assignments`: assign a team.
|
||||
- `POST /reports/{id}/feedback`: add citizen feedback.
|
||||
- `POST /reports/{id}/photos`: upload one or more multipart files for a report.
|
||||
- `POST /reports/{id}/photos`: upload up to five JPEG/PNG/WebP multipart files.
|
||||
|
||||
## Admin Reference Data
|
||||
- `GET|POST /admin/categories`
|
||||
|
||||
@@ -11,14 +11,13 @@ Important tables:
|
||||
- `report_categories`: report classification.
|
||||
- `areas`: named city areas with GeoJSON polygon boundaries.
|
||||
- `team_categories`, `teams`: municipal intervention teams.
|
||||
- `reports`: citizen reports with coordinates, address, status and area.
|
||||
- `reports`: citizen reports with coordinates, address, status, area and assigned operator.
|
||||
- `report_status_history`: immutable status transition log.
|
||||
- `assignments`: report-to-team assignment history.
|
||||
- `report_photos`: uploaded photo metadata.
|
||||
- `report_photos`: uploaded photo URL, original filename, content type and size.
|
||||
- `messages`: citizen/operator report conversation.
|
||||
- `feedback`: citizen rating and comment after resolution.
|
||||
- `reward_events`: indirect gamification points ledger.
|
||||
|
||||
GeoJSON is intentionally used instead of MySQL Spatial in milestone 1 to keep
|
||||
the local setup simple and portable.
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Milestone 02: Workflow Core and Hardening
|
||||
|
||||
## Implemented in this milestone
|
||||
- Documentation/config fixes from Milestone 01 review.
|
||||
- Authorization context with roles, permissions and forced password-change state.
|
||||
- Authenticated password change endpoint.
|
||||
- Admin user listing, role assignment and ban/unban endpoints.
|
||||
- Report ownership workflow with assigned operator and take endpoint.
|
||||
- Valid report status transitions.
|
||||
- Citizen report filters scoped to own reports.
|
||||
- Report photo upload validation and metadata storage.
|
||||
|
||||
## To do next
|
||||
- Add broader API tests once the frontend contracts stabilize.
|
||||
- Add notification delivery for status changes.
|
||||
- Add moderation flow for abusive reports/messages.
|
||||
- Add open-data exports and statistics refinements.
|
||||
- Design CIE/SPID integration.
|
||||
|
||||
@@ -23,6 +23,16 @@ or administrative APIs.
|
||||
- `REJECTED`: report was invalid or abusive.
|
||||
- `CLOSED`: administrative closure after feedback or final review.
|
||||
|
||||
Valid milestone 2 transitions:
|
||||
|
||||
- `OPEN -> UNDER_REVIEW` when an operator/admin takes the report.
|
||||
- `UNDER_REVIEW -> ASSIGNED` when a team is assigned.
|
||||
- `ASSIGNED -> IN_PROGRESS`.
|
||||
- `IN_PROGRESS -> RESOLVED`.
|
||||
- `RESOLVED -> CLOSED`.
|
||||
- `OPEN/UNDER_REVIEW/ASSIGNED/IN_PROGRESS -> REJECTED` only for invalid reports.
|
||||
- `REJECTED -> CLOSED`.
|
||||
|
||||
## MVP Scope
|
||||
- Local auth with email OTP delivered through MailHog.
|
||||
- MySQL persistence with migrations and seed data.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE reports
|
||||
ADD COLUMN assigned_operator_id BIGINT UNSIGNED NULL AFTER area_id,
|
||||
ADD CONSTRAINT reports_assigned_operator_fk
|
||||
FOREIGN KEY (assigned_operator_id) REFERENCES users(id);
|
||||
|
||||
CREATE INDEX reports_user_status_created_idx ON reports(user_id, status, created_at);
|
||||
CREATE INDEX assignments_report_ended_idx ON assignments(report_id, ended_at);
|
||||
|
||||
ALTER TABLE report_photos
|
||||
ADD COLUMN original_filename VARCHAR(255) NULL AFTER url,
|
||||
ADD COLUMN content_type VARCHAR(100) NULL AFTER original_filename,
|
||||
ADD COLUMN size_bytes BIGINT UNSIGNED NULL AFTER content_type;
|
||||
|
||||
+168
-44
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace visione::services
|
||||
{
|
||||
@@ -35,6 +37,20 @@ bool isAllowedStatus(const std::string &status)
|
||||
return statuses.contains(status);
|
||||
}
|
||||
|
||||
bool canTransition(const std::string &from, const std::string &to)
|
||||
{
|
||||
static const std::map<std::string, std::set<std::string>> transitions = {
|
||||
{"OPEN", {"UNDER_REVIEW", "REJECTED"}},
|
||||
{"UNDER_REVIEW", {"ASSIGNED", "REJECTED"}},
|
||||
{"ASSIGNED", {"IN_PROGRESS", "REJECTED"}},
|
||||
{"IN_PROGRESS", {"RESOLVED", "REJECTED"}},
|
||||
{"RESOLVED", {"CLOSED"}},
|
||||
{"REJECTED", {"CLOSED"}},
|
||||
{"CLOSED", {}}};
|
||||
const auto it = transitions.find(from);
|
||||
return it != transitions.end() && it->second.contains(to);
|
||||
}
|
||||
|
||||
std::optional<long long> findArea(double latitude, double longitude)
|
||||
{
|
||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||
@@ -69,6 +85,24 @@ bool canAccessReport(const auth::UserContext &user, long long reportId)
|
||||
user.id);
|
||||
return !result.empty();
|
||||
}
|
||||
|
||||
bool canModifyReport(const auth::UserContext &user, long long reportId)
|
||||
{
|
||||
if (auth::isAdmin(user))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!auth::canManageReports(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||
"SELECT id FROM reports WHERE id = ? AND assigned_operator_id = ?",
|
||||
reportId,
|
||||
user.id);
|
||||
return !result.empty();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Json::Value ReportService::createReport(const auth::UserContext &user,
|
||||
@@ -88,6 +122,14 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
|
||||
const auto areaId = findArea(latitude, longitude);
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto category = db->execSqlSync(
|
||||
"SELECT id FROM report_categories WHERE id = ? AND active = TRUE",
|
||||
body["category_id"].asInt64());
|
||||
if (category.empty())
|
||||
{
|
||||
return failure("category_not_found");
|
||||
}
|
||||
|
||||
if (areaId)
|
||||
{
|
||||
db->execSqlSync(
|
||||
@@ -154,42 +196,27 @@ Json::Value ReportService::listReports(const auth::UserContext &user,
|
||||
const std::optional<std::string> &areaId)
|
||||
{
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto statusParam = status.value_or("");
|
||||
const auto categoryParam = categoryId.value_or("");
|
||||
const auto areaParam = areaId.value_or("");
|
||||
drogon::orm::Result result(nullptr);
|
||||
|
||||
if (status)
|
||||
{
|
||||
result = db->execSqlSync(
|
||||
"SELECT r.*, c.name AS category_name, a.name AS area_name "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.status = ? "
|
||||
"ORDER BY r.created_at DESC LIMIT 100",
|
||||
*status);
|
||||
}
|
||||
else if (categoryId)
|
||||
{
|
||||
result = db->execSqlSync(
|
||||
"SELECT r.*, c.name AS category_name, a.name AS area_name "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.category_id = ? "
|
||||
"ORDER BY r.created_at DESC LIMIT 100",
|
||||
*categoryId);
|
||||
}
|
||||
else if (areaId)
|
||||
{
|
||||
result = db->execSqlSync(
|
||||
"SELECT r.*, c.name AS category_name, a.name AS area_name "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"LEFT JOIN areas a ON a.id = r.area_id WHERE r.area_id = ? "
|
||||
"ORDER BY r.created_at DESC LIMIT 100",
|
||||
*areaId);
|
||||
}
|
||||
else if (auth::canManageReports(user))
|
||||
if (auth::canManageReports(user))
|
||||
{
|
||||
result = db->execSqlSync(
|
||||
"SELECT r.*, c.name AS category_name, a.name AS area_name "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"LEFT JOIN areas a ON a.id = r.area_id "
|
||||
"ORDER BY r.created_at DESC LIMIT 100");
|
||||
"WHERE (? = '' OR r.status = ?) "
|
||||
"AND (? = '' OR r.category_id = ?) "
|
||||
"AND (? = '' OR r.area_id = ?) "
|
||||
"ORDER BY r.created_at DESC LIMIT 100",
|
||||
statusParam,
|
||||
statusParam,
|
||||
categoryParam,
|
||||
categoryParam,
|
||||
areaParam,
|
||||
areaParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -197,8 +224,18 @@ Json::Value ReportService::listReports(const auth::UserContext &user,
|
||||
"SELECT r.*, c.name AS category_name, a.name AS area_name "
|
||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||
"LEFT JOIN areas a ON a.id = r.area_id "
|
||||
"WHERE r.user_id = ? ORDER BY r.created_at DESC LIMIT 100",
|
||||
user.id);
|
||||
"WHERE r.user_id = ? "
|
||||
"AND (? = '' OR r.status = ?) "
|
||||
"AND (? = '' OR r.category_id = ?) "
|
||||
"AND (? = '' OR r.area_id = ?) "
|
||||
"ORDER BY r.created_at DESC LIMIT 100",
|
||||
user.id,
|
||||
statusParam,
|
||||
statusParam,
|
||||
categoryParam,
|
||||
categoryParam,
|
||||
areaParam,
|
||||
areaParam);
|
||||
}
|
||||
|
||||
auto response = success();
|
||||
@@ -240,7 +277,7 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body)
|
||||
{
|
||||
if (!auth::canManageReports(user))
|
||||
if (!canModifyReport(user, reportId))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
@@ -251,29 +288,72 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
||||
}
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto current = db->execSqlSync("SELECT status FROM reports WHERE id = ?",
|
||||
const auto current = db->execSqlSync(
|
||||
"SELECT status FROM reports WHERE id = ?",
|
||||
reportId);
|
||||
if (current.empty())
|
||||
{
|
||||
return failure("report_not_found");
|
||||
}
|
||||
const auto oldStatus = current[0]["status"].as<std::string>();
|
||||
const auto newStatus = body["status"].asString();
|
||||
if (!canTransition(oldStatus, newStatus))
|
||||
{
|
||||
return failure("invalid_status_transition");
|
||||
}
|
||||
|
||||
db->execSqlSync("UPDATE reports SET status = ? WHERE id = ?",
|
||||
body["status"].asString(),
|
||||
newStatus,
|
||||
reportId);
|
||||
db->execSqlSync(
|
||||
"INSERT INTO report_status_history "
|
||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
reportId,
|
||||
current[0]["status"].as<std::string>(),
|
||||
body["status"].asString(),
|
||||
oldStatus,
|
||||
newStatus,
|
||||
user.id,
|
||||
body.get("note", "").asString());
|
||||
|
||||
return success();
|
||||
}
|
||||
|
||||
Json::Value ReportService::takeReport(const auth::UserContext &user,
|
||||
long long reportId)
|
||||
{
|
||||
if (!auth::canManageReports(user))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto current = db->execSqlSync(
|
||||
"SELECT status, assigned_operator_id FROM reports WHERE id = ?",
|
||||
reportId);
|
||||
if (current.empty())
|
||||
{
|
||||
return failure("report_not_found");
|
||||
}
|
||||
if (current[0]["status"].as<std::string>() != "OPEN" ||
|
||||
!current[0]["assigned_operator_id"].isNull())
|
||||
{
|
||||
return failure("report_not_open");
|
||||
}
|
||||
|
||||
db->execSqlSync(
|
||||
"UPDATE reports SET assigned_operator_id = ?, status = 'UNDER_REVIEW' "
|
||||
"WHERE id = ?",
|
||||
user.id,
|
||||
reportId);
|
||||
db->execSqlSync(
|
||||
"INSERT INTO report_status_history "
|
||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||
"VALUES (?, 'OPEN', 'UNDER_REVIEW', ?, 'Report taken by operator')",
|
||||
reportId,
|
||||
user.id);
|
||||
return success();
|
||||
}
|
||||
|
||||
Json::Value ReportService::addMessage(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body)
|
||||
@@ -313,12 +393,25 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body)
|
||||
{
|
||||
if (!auth::canManageReports(user) || !validation::hasNumber(body, "team_id"))
|
||||
if (!canModifyReport(user, reportId) || !validation::hasNumber(body, "team_id"))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto current = db->execSqlSync(
|
||||
"SELECT status FROM reports WHERE id = ?",
|
||||
reportId);
|
||||
if (current.empty())
|
||||
{
|
||||
return failure("report_not_found");
|
||||
}
|
||||
const auto oldStatus = current[0]["status"].as<std::string>();
|
||||
if (!canTransition(oldStatus, "ASSIGNED"))
|
||||
{
|
||||
return failure("invalid_status_transition");
|
||||
}
|
||||
|
||||
db->execSqlSync(
|
||||
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
|
||||
"VALUES (?, ?, ?)",
|
||||
@@ -330,8 +423,9 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
||||
db->execSqlSync(
|
||||
"INSERT INTO report_status_history "
|
||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||
"VALUES (?, NULL, 'ASSIGNED', ?, 'Team assigned')",
|
||||
"VALUES (?, ?, 'ASSIGNED', ?, 'Team assigned')",
|
||||
reportId,
|
||||
oldStatus,
|
||||
user.id);
|
||||
return success();
|
||||
}
|
||||
@@ -373,19 +467,49 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
|
||||
return success();
|
||||
}
|
||||
|
||||
Json::Value ReportService::addPhoto(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const std::string &url)
|
||||
Json::Value ReportService::canAddPhoto(const auth::UserContext &user,
|
||||
long long reportId)
|
||||
{
|
||||
if (!canAccessReport(user, reportId) || url.empty())
|
||||
if (!canAccessReport(user, reportId))
|
||||
{
|
||||
return failure("forbidden");
|
||||
}
|
||||
|
||||
const auto count = drogon::app().getDbClient()->execSqlSync(
|
||||
"SELECT COUNT(*) AS total FROM report_photos WHERE report_id = ?",
|
||||
reportId);
|
||||
if (!count.empty() && count[0]["total"].as<int>() >= 5)
|
||||
{
|
||||
return failure("photo_limit_reached");
|
||||
}
|
||||
auto response = success();
|
||||
response["remaining_photo_slots"] =
|
||||
5 - (count.empty() ? 0 : count[0]["total"].as<int>());
|
||||
return response;
|
||||
}
|
||||
|
||||
Json::Value ReportService::addPhoto(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const std::string &url,
|
||||
const std::string &originalFilename,
|
||||
const std::string &contentType,
|
||||
size_t sizeBytes)
|
||||
{
|
||||
auto allowed = canAddPhoto(user, reportId);
|
||||
if (!allowed.get("ok", false).asBool() || url.empty())
|
||||
{
|
||||
return allowed;
|
||||
}
|
||||
|
||||
drogon::app().getDbClient()->execSqlSync(
|
||||
"INSERT INTO report_photos (report_id, url) VALUES (?, ?)",
|
||||
"INSERT INTO report_photos "
|
||||
"(report_id, url, original_filename, content_type, size_bytes) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
reportId,
|
||||
url);
|
||||
url,
|
||||
originalFilename,
|
||||
contentType,
|
||||
static_cast<unsigned long long>(sizeBytes));
|
||||
return success();
|
||||
}
|
||||
} // namespace visione::services
|
||||
|
||||
@@ -20,6 +20,7 @@ class ReportService
|
||||
Json::Value updateStatus(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body);
|
||||
Json::Value takeReport(const auth::UserContext &user, long long reportId);
|
||||
Json::Value addMessage(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body);
|
||||
@@ -30,8 +31,12 @@ class ReportService
|
||||
Json::Value addFeedback(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const Json::Value &body);
|
||||
Json::Value canAddPhoto(const auth::UserContext &user, long long reportId);
|
||||
Json::Value addPhoto(const auth::UserContext &user,
|
||||
long long reportId,
|
||||
const std::string &url);
|
||||
const std::string &url,
|
||||
const std::string &originalFilename,
|
||||
const std::string &contentType,
|
||||
size_t sizeBytes);
|
||||
};
|
||||
} // namespace visione::services
|
||||
|
||||
Reference in New Issue
Block a user