%%%------------------------------------------------------------------- %%% @doc Административный обработчик отзывов. %%% GET – список отзывов с пагинацией, фильтрацией и сортировкой. %%% PATCH – массовое обновление статусов отзывов с единой причиной. %%% @end %%%------------------------------------------------------------------- -module(admin_handler_reviews). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). %%% cowboy_handler callback init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> list_reviews(Req); <<"PATCH">> -> bulk_update_reviews(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %%% Swagger metadata trails() -> [ #{ % GET list path => <<"/v1/admin/reviews">>, method => <<"GET">>, description => <<"List all reviews (admin)">>, tags => [<<"Reviews">>], parameters => [ #{name => <<"target_type">>, in => <<"query">>, schema => #{type => string}, description => <<"calendar or event">>}, #{name => <<"target_id">>, in => <<"query">>, schema => #{type => string}, description => <<"ID of target">>}, #{name => <<"user_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by user">>}, #{name => <<"status">>, in => <<"query">>, schema => #{type => string}, description => <<"visible, hidden, deleted, or all">>}, #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"created_at">>, <<"updated_at">>, <<"rating">>]}, description => <<"Sort field">>}, #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], responses => #{ 200 => #{ description => <<"Array of reviews">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => review_schema() }}} } } }, #{ % PATCH bulk update path => <<"/v1/admin/reviews">>, method => <<"PATCH">>, description => <<"Bulk update review statuses with a single reason">>, tags => [<<"Reviews">>], requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => #{ type => object, required => [<<"operations">>, <<"reason">>], properties => #{ <<"operations">> => #{ type => array, items => #{ type => object, properties => #{ <<"id">> => #{type => string}, <<"status">> => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]} } } }, <<"reason">> => #{type => string, description => <<"Reason applied to all updates">>} } }}} }, responses => #{ 200 => #{description => <<"Number of updated reviews">>}, 400 => #{description => <<"Invalid request body or missing reason">>} } } ]. review_schema() -> #{ type => object, properties => #{ id => #{type => string}, user_id => #{type => string}, target_type => #{type => string, enum => [<<"calendar">>, <<"event">>]}, target_id => #{type => string}, rating => #{type => integer, minimum => 1, maximum => 5}, comment => #{type => string}, status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]}, reason => #{type => string, nullable => true}, likes => #{type => integer}, dislikes => #{type => integer}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } }. %%% Internal functions list_reviews(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> Qs = cowboy_req:parse_qs(Req1), Filters0 = #{ target_type => normalize_target_type(proplists:get_value(<<"target_type">>, Qs)), target_id => proplists:get_value(<<"target_id">>, Qs), user_id => proplists:get_value(<<"user_id">>, Qs), status => normalize_status(proplists:get_value(<<"status">>, Qs)) }, Filters = maps:filter(fun(_, V) -> V =/= undefined end, Filters0), Pagination = handler_utils:parse_pagination_params(Req1), {ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, Pagination), Json = [handler_utils:review_to_json(R) || R <- Reviews], ExtraHeaders = handler_utils:pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end. %% @private Преобразует бинарный target_type в атом. normalize_target_type(<<"event">>) -> event; normalize_target_type(<<"calendar">>) -> calendar; normalize_target_type(Other) -> Other. %% @private Преобразует бинарный status в атом. normalize_status(<<"visible">>) -> visible; normalize_status(<<"hidden">>) -> hidden; normalize_status(<<"deleted">>) -> deleted; normalize_status(Other) -> Other. bulk_update_reviews(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> try {ok, Body, Req2} = cowboy_req:read_body(Req1), #{<<"operations">> := Operations, <<"reason">> := Reason} = jsx:decode(Body, [return_maps]), true = is_list(Operations), case logic_review:bulk_update_status(Operations, Reason) of {ok, UpdatedCount} -> admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Req2), handler_utils:send_json(Req2, 200, #{updated_count => UpdatedCount}); {error, ReasonErr} -> handler_utils:send_error(Req2, 400, ReasonErr) end catch error:{badmatch, _} -> handler_utils:send_error(Req1, 400, <<"Missing 'reason' field">>); _:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON body">>) end; {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end.