-module(logic_review). -include("records.hrl"). -export([create_review/5, get_review/2, list_reviews/3, list_user_reviews/1, update_review/3, delete_review/2, hide_review/2, hide_review/3, unhide_review/2, unhide_review/3]). -export([can_review/3, update_target_rating/2, can_moderate_review/2]). -export([list_admin_reviews/1, bulk_update_status/2]). -export([list_admin_reviews/2, get_review_admin/1, update_review_admin/2]). %% Создание отзыва create_review(UserId, TargetType, TargetId, Rating, Comment) -> case target_exists(TargetType, TargetId) of true -> case can_review(UserId, TargetType, TargetId) of {ok, true} -> case core_review:has_user_reviewed(UserId, TargetType, TargetId) of false -> case core_review:create(UserId, TargetType, TargetId, Rating, Comment) of {ok, Review} -> update_target_rating(TargetType, TargetId), {ok, Review}; Error -> Error end; true -> {error, already_reviewed} end; {ok, false} -> {error, cannot_review}; {error, _} = Error -> Error end; false -> {error, target_not_found} end. %% Получение отзыва get_review(UserId, ReviewId) -> case core_review:get_by_id(ReviewId) of {ok, Review} -> case admin_utils:is_admin(UserId) orelse Review#review.status =:= visible of true -> {ok, Review}; false -> {error, access_denied} end; Error -> Error end. %% Список отзывов для цели list_reviews(UserId, TargetType, TargetId) -> case core_review:list_by_target(TargetType, TargetId) of {ok, Reviews} -> case admin_utils:is_admin(UserId) of true -> {ok, Reviews}; false -> {ok, [R || R <- Reviews, R#review.status =:= visible]} end; Error -> Error end. %% Список отзывов пользователя list_user_reviews(UserId) -> core_review:list_by_user(UserId). %% Обновление отзыва (только автор) update_review(UserId, ReviewId, Updates) -> io:format("Updating review ~p with ~p~n", [ReviewId, Updates]), case core_review:get_by_id(ReviewId) of {ok, Review} -> case Review#review.user_id =:= UserId of true -> % Преобразуем бинарные ключи в атомы ValidUpdates = lists:filtermap(fun ({<<"rating">>, V}) when is_integer(V), V >= 1, V =< 5 -> {true, {rating, V}}; ({rating, V}) when is_integer(V), V >= 1, V =< 5 -> {true, {rating, V}}; ({<<"comment">>, V}) when is_binary(V) -> {true, {comment, V}}; ({comment, V}) when is_binary(V) -> {true, {comment, V}}; (_) -> false end, Updates), io:format("Valid updates: ~p~n", [ValidUpdates]), case ValidUpdates of [] -> {error, no_valid_updates}; _ -> case core_review:update(ReviewId, ValidUpdates) of {ok, Updated} -> update_target_rating(Review#review.target_type, Review#review.target_id), {ok, Updated}; Error -> Error end end; false -> {error, access_denied} end; Error -> Error end. %% Удаление отзыва (автор или админ) delete_review(UserId, ReviewId) -> case core_review:get_by_id(ReviewId) of {ok, Review} -> case Review#review.user_id =:= UserId orelse admin_utils:is_admin(UserId) of true -> TargetType = Review#review.target_type, TargetId = Review#review.target_id, case core_review:delete(ReviewId) of {ok, deleted} -> update_target_rating(TargetType, TargetId), {ok, deleted}; Error -> Error end; false -> {error, access_denied} end; Error -> Error end. hide_review(UserId, ReviewId) -> hide_review(UserId, ReviewId, <<>>). hide_review(UserId, ReviewId, Reason) -> case can_moderate_review(UserId, ReviewId) of true -> case core_review:hide(ReviewId, Reason) of {ok, Review} -> update_target_rating(Review#review.target_type, Review#review.target_id), {ok, Review}; Error -> Error end; false -> {error, access_denied} end. unhide_review(UserId, ReviewId) -> unhide_review(UserId, ReviewId, <<>>). unhide_review(UserId, ReviewId, Reason) -> case can_moderate_review(UserId, ReviewId) of true -> case core_review:unhide(ReviewId, Reason) of {ok, Review} -> update_target_rating(Review#review.target_type, Review#review.target_id), {ok, Review}; Error -> Error end; false -> {error, access_denied} end. %% Проверка, может ли пользователь оставить отзыв can_review(UserId, TargetType, TargetId) -> case TargetType of event -> case core_booking:get_by_event_and_user(TargetId, UserId) of {ok, Booking} -> {ok, Booking#booking.status =:= confirmed}; {error, not_found} -> {ok, false} end; calendar -> {ok, true}; _ -> {ok, false} end. %% Проверка, может ли пользователь модерировать отзыв (скрыть/раскрыть) can_moderate_review(UserId, ReviewId) -> case admin_utils:is_admin(UserId) of true -> true; false -> % Проверяем, является ли пользователь владельцем календаря/события case core_review:get_by_id(ReviewId) of {ok, Review} -> case Review#review.target_type of event -> case core_event:get_by_id(Review#review.target_id) of {ok, Event} -> case core_calendar:get_by_id(Event#event.calendar_id) of {ok, Calendar} -> Calendar#calendar.owner_id =:= UserId; _ -> false end; _ -> false end; calendar -> case core_calendar:get_by_id(Review#review.target_id) of {ok, Calendar} -> Calendar#calendar.owner_id =:= UserId; _ -> false end; _ -> false end; _ -> false end end. %%%------------------------------------------------------------------- %%% @doc Получить список всех отзывов (административный режим). %%% Фильтры: [{target_type, Type}, {target_id, Id}, {user_id, UserId}] %%% Все фильтры опциональны. %%% @end %%%------------------------------------------------------------------- list_admin_reviews(Filters) -> Reviews = core_review:list_all(), % возвращает список Filtered = apply_filters(Reviews, Filters), {ok, Filtered}. %% Вспомогательная функция: фильтрация списка по proplist apply_filters(Reviews, []) -> Reviews; apply_filters(Reviews, [{status, Status} | Rest]) -> apply_filters( [R || R <- Reviews, R#review.status =:= Status], Rest ); apply_filters(Reviews, [{target_type, Type} | Rest]) -> apply_filters( [R || R <- Reviews, R#review.target_type =:= Type], Rest ); apply_filters(Reviews, [{target_id, Id} | Rest]) -> apply_filters( [R || R <- Reviews, R#review.target_id =:= Id], Rest ); apply_filters(Reviews, [{user_id, UserId} | Rest]) -> apply_filters( [R || R <- Reviews, R#review.user_id =:= UserId], Rest ); apply_filters(Reviews, [_ | Rest]) -> apply_filters(Reviews, Rest). %%%------------------------------------------------------------------- %%% @doc Массово обновить статусы отзывов. %%% %%% Принимает список операций и одну причину, которая будет %%% записана во все изменённые отзывы. Все изменения выполняются %%% в одной Mnesia‑транзакции. %%% %%% Параметры: %%% Operations :: [#{ id := binary(), status := binary() }] %%% Каждый элемент обязан содержать ключи `id` и `status`. %%% Допустимые значения `status`: `<<"visible">>`, `<<"hidden">>`, %%% `<<"deleted">>`. %%% Reason :: binary() %%% Причина, которая будет записана в поле `reason` каждого %%% обновлённого отзыва. %%% %%% Возвращает: %%% {ok, UpdatedCount :: non_neg_integer()} в случае успеха, %%% {error, Reason :: binary()} при ошибке. %%% @end %%%------------------------------------------------------------------- -spec bulk_update_status(Operations :: [#{binary() => binary()}], Reason :: binary()) -> {ok, non_neg_integer()} | {error, binary()}. bulk_update_status(Operations, Reason) when is_list(Operations) -> Fun = fun() -> lists:foreach(fun(Op) -> do_update_status(Op, Reason) end, Operations) end, case mnesia:transaction(Fun) of {atomic, ok} -> {ok, length(Operations)}; {aborted, ReasonErr} -> {error, ReasonErr} end. %% @private Выполняет обновление одного отзыва внутри транзакции. -spec do_update_status(map(), binary()) -> ok | no_return(). do_update_status(#{<<"id">> := Id, <<"status">> := NewStatus}, Reason) -> case core_review:get_by_id(Id) of {ok, _Review} -> Updates = [{status, NewStatus}, {reason, Reason}], case core_review:update(Id, Updates) of {ok, _} -> ok; {error, _} -> mnesia:abort(<<"review_update_failed">>) end; not_found -> mnesia:abort(<<"review_not_found">>) end. %% Внутренние функции target_exists(event, EventId) -> case core_event:get_by_id(EventId) of {ok, _} -> true; _ -> false end; target_exists(calendar, CalendarId) -> case core_calendar:get_by_id(CalendarId) of {ok, _} -> true; _ -> false end; target_exists(_, _) -> false. update_target_rating(event, EventId) -> {Avg, Count} = core_review:get_average_rating(event, EventId), %% io:format("Updating event ~p rating: avg=~p, count=~p~n", [EventId, Avg, Count]), core_event:update(EventId, [{rating_avg, Avg}, {rating_count, Count}]); update_target_rating(calendar, CalendarId) -> {Avg, Count} = core_review:get_average_rating(calendar, CalendarId), core_calendar:update(CalendarId, [{rating_avg, Avg}, {rating_count, Count}]); update_target_rating(_, _) -> ok. %% Административный список с пагинацией -spec list_admin_reviews(map(), #{limit => integer(), offset => integer(), sort => binary(), order => binary()}) -> {ok, non_neg_integer(), [#review{}]}. list_admin_reviews(Filters, Pagination) -> #{limit := Limit, offset := Offset, sort := Sort, order := Order} = Pagination, % Получаем все отзывы (можно временно через list_admin_reviews/1) {ok, All} = list_admin_reviews(maps:to_list(Filters)), Sorted = sort_reviews(All, Sort, Order), Total = length(Sorted), Page = lists:sublist(Sorted, Offset + 1, Limit), {ok, Total, Page}. %% Получить отзыв без проверки прав -spec get_review_admin(binary()) -> {ok, #review{}} | {error, not_found}. get_review_admin(ReviewId) -> core_review:get_by_id(ReviewId). %% Обновить отзыв без проверки прав -spec update_review_admin(binary(), proplists:proplist()) -> {ok, #review{}} | {error, not_found}. update_review_admin(ReviewId, Updates) -> case core_review:get_by_id(ReviewId) of {ok, _} -> core_review:update(ReviewId, Updates); Error -> Error end. %%%------------------------------------------------------------------- %%% Вспомогательные функции административной пагинации %%%------------------------------------------------------------------- %% @private Сортирует список отзывов по указанному полю. sort_reviews(Reviews, SortField, Order) -> Field = binary_to_existing_atom(SortField, utf8), Sorted = lists:sort( fun(A, B) -> ValA = review_field(A, Field), ValB = review_field(B, Field), if Order == <<"asc">> -> ValA =< ValB; true -> ValA >= ValB end end, Reviews), Sorted. %% @private Извлекает значение поля из записи отзыва для сортировки. review_field(#review{created_at = V}, created_at) -> V; review_field(#review{rating = V}, rating) -> V; review_field(#review{status = V}, status) -> V; review_field(_, _) -> undefined.