Расширена статистика для разделов #22
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
-export([count_calendars/0, list_all/0]).
|
-export([count_calendars/0, list_all/0]).
|
||||||
-export([freeze/2, unfreeze/2]).
|
-export([freeze/2, unfreeze/2]).
|
||||||
-export([count_calendars_by_date/2]).
|
-export([count_calendars_by_date/2]).
|
||||||
|
-export([count_calendars_by_type/0, count_calendars_by_status/0,
|
||||||
|
get_top_calendars_by_reviews/1, get_top_calendars_by_positive_reviews/1,
|
||||||
|
get_top_calendars_by_negative_reviews/1, get_top_calendars_by_rating/1]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -174,6 +177,92 @@ count_calendars_by_date(From, To) ->
|
|||||||
|
|
||||||
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт календарей по типам (personal, commercial).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_calendars_by_type() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_calendars_by_type() ->
|
||||||
|
Calendars = mnesia:dirty_match_object(#calendar{_ = '_'}),
|
||||||
|
lists:foldl(fun(#calendar{type = Type}, Acc) ->
|
||||||
|
case lists:keyfind(Type, 1, Acc) of
|
||||||
|
false -> [{Type, 1} | Acc];
|
||||||
|
{Type, C} -> lists:keyreplace(Type, 1, Acc, {Type, C+1})
|
||||||
|
end
|
||||||
|
end, [], Calendars).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт календарей по статусам (active, frozen, deleted).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_calendars_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_calendars_by_status() ->
|
||||||
|
Calendars = mnesia:dirty_match_object(#calendar{_ = '_'}),
|
||||||
|
lists:foldl(fun(#calendar{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Calendars).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N календарей по количеству отзывов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_calendars_by_reviews(N :: non_neg_integer()) -> [#calendar{}].
|
||||||
|
get_top_calendars_by_reviews(N) ->
|
||||||
|
Calendars = mnesia:dirty_match_object(#calendar{_ = '_'}),
|
||||||
|
Sorted = lists:reverse(lists:sort(
|
||||||
|
fun(A, B) -> A#calendar.rating_count =< B#calendar.rating_count end,
|
||||||
|
Calendars)),
|
||||||
|
lists:sublist(Sorted, N).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N календарей по количеству позитивных отзывов (rating >= 4).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_calendars_by_positive_reviews(N :: non_neg_integer()) -> [#calendar{}].
|
||||||
|
get_top_calendars_by_positive_reviews(N) ->
|
||||||
|
top_by_review_filter(N, fun(R) -> R#review.rating >= 4 end).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N календарей по количеству негативных отзывов (rating <= 2).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_calendars_by_negative_reviews(N :: non_neg_integer()) -> [#calendar{}].
|
||||||
|
get_top_calendars_by_negative_reviews(N) ->
|
||||||
|
top_by_review_filter(N, fun(R) -> R#review.rating =< 2 end).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N календарей по среднему рейтингу.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_calendars_by_rating(N :: non_neg_integer()) -> [#calendar{}].
|
||||||
|
get_top_calendars_by_rating(N) ->
|
||||||
|
Calendars = mnesia:dirty_match_object(#calendar{_ = '_'}),
|
||||||
|
Sorted = lists:reverse(lists:sort(
|
||||||
|
fun(A, B) -> A#calendar.rating_avg =< B#calendar.rating_avg end,
|
||||||
|
Calendars)),
|
||||||
|
lists:sublist(Sorted, N).
|
||||||
|
|
||||||
|
%% @private Вспомогательная функция для фильтрации отзывов и подсчёта по календарям.
|
||||||
|
top_by_review_filter(N, FilterFun) ->
|
||||||
|
Reviews = mnesia:dirty_match_object(#review{target_type = calendar, _ = '_'}),
|
||||||
|
Filtered = lists:filter(FilterFun, Reviews),
|
||||||
|
% Считаем количество отзывов для каждого календаря
|
||||||
|
Counts = lists:foldl(fun(#review{target_id = CalId}, Acc) ->
|
||||||
|
case lists:keyfind(CalId, 1, Acc) of
|
||||||
|
false -> [{CalId, 1} | Acc];
|
||||||
|
{CalId, C} -> lists:keyreplace(CalId, 1, Acc, {CalId, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
% Сортируем по убыванию количества
|
||||||
|
SortedIds = [Id || {Id, _} <- lists:reverse(lists:sort(fun({_,C1},{_,C2}) -> C1 =< C2 end, Counts))],
|
||||||
|
TopIds = lists:sublist(SortedIds, N),
|
||||||
|
% Получаем объекты календарей
|
||||||
|
[Cal || Id <- TopIds,
|
||||||
|
{ok, Cal} <- [core_calendar:get_by_id(Id)]].
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
-export([count_events/0, count_events_by_date/2]).
|
-export([count_events/0, count_events_by_date/2]).
|
||||||
-export([freeze/2, unfreeze/2]).
|
-export([freeze/2, unfreeze/2]).
|
||||||
-export([list_all/0]).
|
-export([list_all/0]).
|
||||||
|
-export([count_events_by_type/0, count_events_by_status/0, get_top_events_by_rating/1]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -251,6 +252,46 @@ count_events_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт событий по типам (single, recurring).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_events_by_type() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_events_by_type() ->
|
||||||
|
Events = mnesia:dirty_match_object(#event{_ = '_'}),
|
||||||
|
lists:foldl(fun(#event{event_type = Type}, Acc) ->
|
||||||
|
case lists:keyfind(Type, 1, Acc) of
|
||||||
|
false -> [{Type, 1} | Acc];
|
||||||
|
{Type, C} -> lists:keyreplace(Type, 1, Acc, {Type, C+1})
|
||||||
|
end
|
||||||
|
end, [], Events).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт событий по статусам (active, cancelled, completed).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_events_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_events_by_status() ->
|
||||||
|
Events = mnesia:dirty_match_object(#event{_ = '_'}),
|
||||||
|
lists:foldl(fun(#event{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Events).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Возвращает топ-N событий по среднему рейтингу (по убыванию).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_events_by_rating(N :: non_neg_integer()) -> [#event{}].
|
||||||
|
get_top_events_by_rating(N) ->
|
||||||
|
Events = mnesia:dirty_match_object(#event{_ = '_'}),
|
||||||
|
Sorted = lists:reverse(lists:sort(
|
||||||
|
fun(A, B) -> A#event.rating_avg =< B#event.rating_avg end,
|
||||||
|
Events)),
|
||||||
|
lists:sublist(Sorted, N).
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
||||||
%%% @end
|
%%% @end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
-export([create/4, get_by_id/1, list_all/0, list_by_reporter/1, update_status/3, count_reports/0]).
|
-export([create/4, get_by_id/1, list_all/0, list_by_reporter/1, update_status/3, count_reports/0]).
|
||||||
-export([list_by_target/2, get_count_by_target/2]).
|
-export([list_by_target/2, get_count_by_target/2]).
|
||||||
-export([count_reports_by_status/1, count_reports_resolved_by_admin/2, avg_resolution_time/1]).
|
-export([count_reports_by_status/1, count_reports_resolved_by_admin/2, avg_resolution_time/1]).
|
||||||
-export([count_reports_by_date/2]).
|
-export([count_reports_by_date/2, get_top_targets_by_reports/1, count_reports_by_status/0, count_reports_by_target_type/0]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -178,4 +178,55 @@ count_reports_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт жалоб по типам цели (event, calendar, review).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reports_by_target_type() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_reports_by_target_type() ->
|
||||||
|
Reports = mnesia:dirty_match_object(#report{_ = '_'}),
|
||||||
|
lists:foldl(fun(#report{target_type = Type}, Acc) ->
|
||||||
|
case lists:keyfind(Type, 1, Acc) of
|
||||||
|
false -> [{Type, 1} | Acc];
|
||||||
|
{Type, C} -> lists:keyreplace(Type, 1, Acc, {Type, C+1})
|
||||||
|
end
|
||||||
|
end, [], Reports).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт жалоб по статусам.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reports_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_reports_by_status() ->
|
||||||
|
Reports = mnesia:dirty_match_object(#report{_ = '_'}),
|
||||||
|
lists:foldl(fun(#report{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Reports).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N целей по количеству жалоб.
|
||||||
|
%%% Возвращает список [{TargetType, TargetId, Count}], отсортированный по убыванию Count.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_targets_by_reports(N :: non_neg_integer()) ->
|
||||||
|
[{TargetType :: atom(), TargetId :: binary(), Count :: non_neg_integer()}].
|
||||||
|
get_top_targets_by_reports(N) ->
|
||||||
|
Reports = mnesia:dirty_match_object(#report{_ = '_'}),
|
||||||
|
% Группируем по {TargetType, TargetId}
|
||||||
|
Dict = lists:foldl(fun(#report{target_type = Type, target_id = Id}, Acc) ->
|
||||||
|
Key = {Type, Id},
|
||||||
|
case lists:keyfind(Key, 1, Acc) of
|
||||||
|
false -> [{Key, 1} | Acc];
|
||||||
|
{Key, C} -> lists:keyreplace(Key, 1, Acc, {Key, C+1})
|
||||||
|
end
|
||||||
|
end, [], Reports),
|
||||||
|
% Сортируем по убыванию Count
|
||||||
|
Sorted = lists:reverse(lists:sort(fun({_, C1}, {_, C2}) -> C1 =< C2 end, Dict)),
|
||||||
|
% Преобразуем в тройки и берём первые N
|
||||||
|
Top = lists:sublist([{Type, Id, Count} || {{Type, Id}, Count} <- Sorted], N),
|
||||||
|
Top.
|
||||||
|
|
||||||
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
-export([get_average_rating/2, has_user_reviewed/3]).
|
-export([get_average_rating/2, has_user_reviewed/3]).
|
||||||
-export([count_reviews/0, list_all/0]).
|
-export([count_reviews/0, list_all/0]).
|
||||||
-export([count_reviews_by_date/2]).
|
-export([count_reviews_by_date/2]).
|
||||||
|
-export([count_reviews_by_target_type/0, count_reviews_by_status/0,
|
||||||
|
get_top_targets_by_reviews/1, get_top_targets_by_positive_reviews/1,
|
||||||
|
get_top_targets_by_negative_reviews/1]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -204,6 +207,75 @@ count_reviews_by_date(From, To) ->
|
|||||||
|
|
||||||
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт отзывов по типам цели (event, calendar).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reviews_by_target_type() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_reviews_by_target_type() ->
|
||||||
|
Reviews = mnesia:dirty_match_object(#review{_ = '_'}),
|
||||||
|
lists:foldl(fun(#review{target_type = Type}, Acc) ->
|
||||||
|
case lists:keyfind(Type, 1, Acc) of
|
||||||
|
false -> [{Type, 1} | Acc];
|
||||||
|
{Type, C} -> lists:keyreplace(Type, 1, Acc, {Type, C+1})
|
||||||
|
end
|
||||||
|
end, [], Reviews).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт отзывов по статусам (visible, hidden, deleted).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reviews_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_reviews_by_status() ->
|
||||||
|
Reviews = mnesia:dirty_match_object(#review{_ = '_'}),
|
||||||
|
lists:foldl(fun(#review{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Reviews).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N целей по общему количеству отзывов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_targets_by_reviews(N :: non_neg_integer()) ->
|
||||||
|
[{TargetType :: atom(), TargetId :: binary(), Count :: non_neg_integer()}].
|
||||||
|
get_top_targets_by_reviews(N) ->
|
||||||
|
aggregate_targets_by(fun(_Review) -> true end, N).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N целей по количеству позитивных отзывов (rating >= 4).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_targets_by_positive_reviews(N :: non_neg_integer()) ->
|
||||||
|
[{TargetType :: atom(), TargetId :: binary(), Count :: non_neg_integer()}].
|
||||||
|
get_top_targets_by_positive_reviews(N) ->
|
||||||
|
aggregate_targets_by(fun(#review{rating = R}) -> R >= 4 end, N).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Топ-N целей по количеству негативных отзывов (rating <= 2).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_top_targets_by_negative_reviews(N :: non_neg_integer()) ->
|
||||||
|
[{TargetType :: atom(), TargetId :: binary(), Count :: non_neg_integer()}].
|
||||||
|
get_top_targets_by_negative_reviews(N) ->
|
||||||
|
aggregate_targets_by(fun(#review{rating = R}) -> R =< 2 end, N).
|
||||||
|
|
||||||
|
%% @private Вспомогательная функция агрегации целей с фильтром.
|
||||||
|
aggregate_targets_by(Pred, N) ->
|
||||||
|
Reviews = mnesia:dirty_match_object(#review{_ = '_'}),
|
||||||
|
Filtered = lists:filter(Pred, Reviews),
|
||||||
|
Dict = lists:foldl(fun(#review{target_type = Type, target_id = Id}, Acc) ->
|
||||||
|
Key = {Type, Id},
|
||||||
|
case lists:keyfind(Key, 1, Acc) of
|
||||||
|
false -> [{Key, 1} | Acc];
|
||||||
|
{Key, C} -> lists:keyreplace(Key, 1, Acc, {Key, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
Sorted = lists:reverse(lists:sort(fun({_, C1}, {_, C2}) -> C1 =< C2 end, Dict)),
|
||||||
|
lists:sublist([{Type, Id, Cnt} || {{Type, Id}, Cnt} <- Sorted], N).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
-export([create/3, get_by_id/1, get_active_by_user/1, list_by_user/1, list_all/0]).
|
-export([create/3, get_by_id/1, get_active_by_user/1, list_by_user/1, list_all/0]).
|
||||||
-export([update_status/2, check_expired/0]).
|
-export([update_status/2, check_expired/0]).
|
||||||
-export([list_subscriptions/0, create_subscription/1, update_subscription/2, delete_subscription/1]).
|
-export([list_subscriptions/0, create_subscription/1, update_subscription/2, delete_subscription/1]).
|
||||||
-export([count_subscription/0]).
|
-export([count_subscriptions/0]).
|
||||||
-export([count_subscriptions_by_date/2]).
|
-export([count_subscriptions_by_date/2]).
|
||||||
|
-export([count_subscriptions_by_plan/0, count_subscriptions_by_status/0,
|
||||||
|
count_trial_subscriptions/0, get_ending_paid_subscriptions/1]).
|
||||||
|
|
||||||
-define(TRIAL_DAYS, 30).
|
-define(TRIAL_DAYS, 30).
|
||||||
|
|
||||||
@@ -291,8 +293,8 @@ parse_iso_datetime(Other) -> Other.
|
|||||||
%%% @doc Количество подписок.
|
%%% @doc Количество подписок.
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-spec count_subscription() -> non_neg_integer().
|
-spec count_subscriptions() -> non_neg_integer().
|
||||||
count_subscription() ->
|
count_subscriptions() ->
|
||||||
mnesia:table_info(subscription, size).
|
mnesia:table_info(subscription, size).
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
@@ -315,3 +317,55 @@ count_subscriptions_by_date(From, To) ->
|
|||||||
lists:sort(Counts).
|
lists:sort(Counts).
|
||||||
|
|
||||||
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт подписок по планам.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_subscriptions_by_plan() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_subscriptions_by_plan() ->
|
||||||
|
Subs = mnesia:dirty_match_object(#subscription{_ = '_'}),
|
||||||
|
lists:foldl(fun(#subscription{plan = Plan}, Acc) ->
|
||||||
|
case lists:keyfind(Plan, 1, Acc) of
|
||||||
|
false -> [{Plan, 1} | Acc];
|
||||||
|
{Plan, C} -> lists:keyreplace(Plan, 1, Acc, {Plan, C+1})
|
||||||
|
end
|
||||||
|
end, [], Subs).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт подписок по статусам.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_subscriptions_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_subscriptions_by_status() ->
|
||||||
|
Subs = mnesia:dirty_match_object(#subscription{_ = '_'}),
|
||||||
|
lists:foldl(fun(#subscription{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Subs).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество пробных подписок (trial_used = false).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_trial_subscriptions() -> non_neg_integer().
|
||||||
|
count_trial_subscriptions() ->
|
||||||
|
Match = #subscription{trial_used = false, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Платные подписки (trial_used = true), истекающие в течение Days дней.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_ending_paid_subscriptions(Days :: non_neg_integer()) -> [#subscription{}].
|
||||||
|
get_ending_paid_subscriptions(Days) ->
|
||||||
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
||||||
|
ThresholdSec = NowSec + Days * 86400,
|
||||||
|
Match = #subscription{status = active, trial_used = true, _ = '_'},
|
||||||
|
ActivePaid = mnesia:dirty_match_object(Match),
|
||||||
|
lists:filter(fun(#subscription{expires_at = Exp}) ->
|
||||||
|
ExpSec = calendar:datetime_to_gregorian_seconds(Exp),
|
||||||
|
ExpSec >= NowSec andalso ExpSec =< ThresholdSec
|
||||||
|
end, ActivePaid).
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
-export([count_tickets/0]).
|
-export([count_tickets/0]).
|
||||||
-export([create_ticket/1]).
|
-export([create_ticket/1]).
|
||||||
-export([list_by_user/1]).
|
-export([list_by_user/1]).
|
||||||
-export([count_tickets_by_status/1, avg_resolution_time/0, count_tickets_by_admin/2]).
|
-export([count_tickets_by_status/1, avg_resolution_time/0, count_tickets_by_admin/2, stats/0]).
|
||||||
-export([update_ticket/2]).
|
-export([update_ticket/2]).
|
||||||
-export([delete_ticket/1]).
|
-export([delete_ticket/1]).
|
||||||
-export([count_tickets_by_date/2]).
|
-export([count_tickets_by_date/2]).
|
||||||
@@ -236,6 +236,23 @@ avg_resolution_time() ->
|
|||||||
Total / length(Closed) / 3600.0
|
Total / length(Closed) / 3600.0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc Статистика по тикетам (используется в admin_handler_ticket_stats)
|
||||||
|
stats() ->
|
||||||
|
Tickets = list_all(),
|
||||||
|
#{
|
||||||
|
total => length(Tickets),
|
||||||
|
open => count_by_status(open, Tickets),
|
||||||
|
in_progress => count_by_status(in_progress, Tickets),
|
||||||
|
resolved => count_by_status(resolved, Tickets),
|
||||||
|
closed => count_by_status(closed, Tickets)
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% ── функции подсчёта с нормализацией статуса ─────────────
|
||||||
|
|
||||||
|
%% @private Подсчитывает тикеты с заданным статусом (атом или бинарный)
|
||||||
|
count_by_status(Status, Tickets) ->
|
||||||
|
length([T || T <- Tickets, normalize_status(T#ticket.status) =:= Status]).
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Количество тикетов, назначенных на администратора.
|
%%% @doc Количество тикетов, назначенных на администратора.
|
||||||
%%% @end
|
%%% @end
|
||||||
@@ -274,6 +291,14 @@ date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
|||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private Преобразует бинарный статус в атом, если нужно.
|
||||||
|
%% Атомы возвращает без изменений.
|
||||||
|
normalize_status(Status) when is_atom(Status) -> Status;
|
||||||
|
normalize_status(Status) when is_binary(Status) ->
|
||||||
|
try binary_to_existing_atom(Status, utf8)
|
||||||
|
catch error:badarg -> Status
|
||||||
|
end.
|
||||||
|
|
||||||
apply_updates(Ticket, Updates) ->
|
apply_updates(Ticket, Updates) ->
|
||||||
Updated = lists:foldl(fun({Field, Value}, T) -> set_field(Field, Value, T) end,
|
Updated = lists:foldl(fun({Field, Value}, T) -> set_field(Field, Value, T) end,
|
||||||
Ticket, Updates),
|
Ticket, Updates),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
-export([list_users/0]).
|
-export([list_users/0]).
|
||||||
-export([block/2, unblock/2]).
|
-export([block/2, unblock/2]).
|
||||||
-export([count_users/0, count_users_by_date/2, count_pending_users/0, count_pending_users_by_date/2, list_all/0]).
|
-export([count_users/0, count_users_by_date/2, count_pending_users/0, count_pending_users_by_date/2, list_all/0]).
|
||||||
|
-export([count_users_by_role/0, count_users_by_status/0]).
|
||||||
-export([create_bot/2, delete_bot/1]).
|
-export([create_bot/2, delete_bot/1]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
@@ -306,6 +307,38 @@ count_pending_users_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт пользователей по ролям.
|
||||||
|
%%% Возвращает список [{Role :: atom(), Count :: non_neg_integer()}].
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_users_by_role() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_users_by_role() ->
|
||||||
|
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
|
Dict = lists:foldl(fun(#user{role = Role}, Acc) ->
|
||||||
|
case lists:keyfind(Role, 1, Acc) of
|
||||||
|
false -> [{Role, 1} | Acc];
|
||||||
|
{Role, C} -> lists:keyreplace(Role, 1, Acc, {Role, C+1})
|
||||||
|
end
|
||||||
|
end, [], Users),
|
||||||
|
Dict.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт пользователей по статусам.
|
||||||
|
%%% Возвращает список [{Status :: atom(), Count :: non_neg_integer()}].
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_users_by_status() -> [{atom(), non_neg_integer()}].
|
||||||
|
count_users_by_status() ->
|
||||||
|
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
|
Dict = lists:foldl(fun(#user{status = Status}, Acc) ->
|
||||||
|
case lists:keyfind(Status, 1, Acc) of
|
||||||
|
false -> [{Status, 1} | Acc];
|
||||||
|
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
|
||||||
|
end
|
||||||
|
end, [], Users),
|
||||||
|
Dict.
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
||||||
%%% @end
|
%%% @end
|
||||||
|
|||||||
@@ -113,19 +113,24 @@ start_admin_http() ->
|
|||||||
{"/v1/admin/refresh", admin_handler_refresh, []},
|
{"/v1/admin/refresh", admin_handler_refresh, []},
|
||||||
% ================== ПОЛЬЗОВАТЕЛИ ==================
|
% ================== ПОЛЬЗОВАТЕЛИ ==================
|
||||||
{"/v1/admin/users", admin_handler_users, []},
|
{"/v1/admin/users", admin_handler_users, []},
|
||||||
|
{"/v1/admin/users/stats", admin_handler_user_stats, []},
|
||||||
{"/v1/admin/users/:id", admin_handler_user_by_id, []},
|
{"/v1/admin/users/:id", admin_handler_user_by_id, []},
|
||||||
{"/v1/admin/users/:id/verification-token", admin_handler_user_verification_token, []},
|
{"/v1/admin/users/:id/verification-token", admin_handler_user_verification_token, []},
|
||||||
% ================== КАЛЕНДАРИ ==================
|
% ================== КАЛЕНДАРИ ==================
|
||||||
{"/v1/admin/calendars", admin_handler_calendars, []},
|
{"/v1/admin/calendars", admin_handler_calendars, []},
|
||||||
|
{"/v1/admin/calendars/stats", admin_handler_calendar_stats, []},
|
||||||
{"/v1/admin/calendars/:id", admin_handler_calendar_by_id, []},
|
{"/v1/admin/calendars/:id", admin_handler_calendar_by_id, []},
|
||||||
% ================== СОБЫТИЯ ==================
|
% ================== СОБЫТИЯ ==================
|
||||||
{"/v1/admin/events", admin_handler_events, []},
|
{"/v1/admin/events", admin_handler_events, []},
|
||||||
|
{"/v1/admin/events/stats", admin_handler_event_stats, []},
|
||||||
{"/v1/admin/events/:id", admin_handler_event_by_id, []},
|
{"/v1/admin/events/:id", admin_handler_event_by_id, []},
|
||||||
% ================== ОТЧЁТЫ ==================
|
% ================== ОТЧЁТЫ ==================
|
||||||
{"/v1/admin/reports", admin_handler_reports, []},
|
{"/v1/admin/reports", admin_handler_reports, []},
|
||||||
|
{"/v1/admin/reports/stats", admin_handler_report_stats, []},
|
||||||
{"/v1/admin/reports/:id", admin_handler_report_by_id, []},
|
{"/v1/admin/reports/:id", admin_handler_report_by_id, []},
|
||||||
% ================== ОТЗЫВЫ ==================
|
% ================== ОТЗЫВЫ ==================
|
||||||
{"/v1/admin/reviews", admin_handler_reviews, []},
|
{"/v1/admin/reviews", admin_handler_reviews, []},
|
||||||
|
{"/v1/admin/reviews/stats", admin_handler_reviews_stats, []},
|
||||||
{"/v1/admin/reviews/:id", admin_handler_reviews_by_id, []},
|
{"/v1/admin/reviews/:id", admin_handler_reviews_by_id, []},
|
||||||
% ================== БАН-СЛОВА ==================
|
% ================== БАН-СЛОВА ==================
|
||||||
{"/v1/admin/banned-words/batch", admin_handler_banned_words, []},
|
{"/v1/admin/banned-words/batch", admin_handler_banned_words, []},
|
||||||
@@ -137,6 +142,7 @@ start_admin_http() ->
|
|||||||
{"/v1/admin/tickets", admin_handler_tickets, []},
|
{"/v1/admin/tickets", admin_handler_tickets, []},
|
||||||
% ================== ПОДПИСКИ ==================
|
% ================== ПОДПИСКИ ==================
|
||||||
{"/v1/admin/subscriptions", admin_handler_subscriptions, []},
|
{"/v1/admin/subscriptions", admin_handler_subscriptions, []},
|
||||||
|
{"/v1/admin/subscriptions/stats", admin_handler_subscription_stats, []},
|
||||||
{"/v1/admin/subscriptions/:id", admin_handler_subscriptions_by_id, []},
|
{"/v1/admin/subscriptions/:id", admin_handler_subscriptions_by_id, []},
|
||||||
% ================== Управление ролями (только для superadmin) ==================
|
% ================== Управление ролями (только для superadmin) ==================
|
||||||
{"/v1/admin/me", admin_handler_me, []},
|
{"/v1/admin/me", admin_handler_me, []},
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по календарям.
|
||||||
|
%%% GET /v1/admin/calendars/stats
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_calendar_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_calendar_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[ #{
|
||||||
|
path => <<"/v1/admin/calendars/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Calendar statistics">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Calendar statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => calendar_stats_schema()}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ].
|
||||||
|
|
||||||
|
calendar_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_calendars">> => #{type => integer},
|
||||||
|
<<"calendars_by_type">> => #{
|
||||||
|
type => object,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"calendars_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"top_calendars_by_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
items => calendar_rank_schema()
|
||||||
|
},
|
||||||
|
<<"top_calendars_by_positive_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
items => calendar_rank_schema()
|
||||||
|
},
|
||||||
|
<<"top_calendars_by_negative_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
items => calendar_rank_schema()
|
||||||
|
},
|
||||||
|
<<"top_calendars_by_rating">> => #{
|
||||||
|
type => array,
|
||||||
|
items => calendar_rank_schema()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
calendar_rank_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"id">> => #{type => string},
|
||||||
|
<<"title">> => #{type => string},
|
||||||
|
<<"rating_avg">> => #{type => number, format => float},
|
||||||
|
<<"review_count">> => #{type => integer}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
get_calendar_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_calendar:count_calendars(),
|
||||||
|
ByType = core_calendar:count_calendars_by_type(),
|
||||||
|
ByStatus = core_calendar:count_calendars_by_status(),
|
||||||
|
TopReviews = core_calendar:get_top_calendars_by_reviews(10),
|
||||||
|
TopPositive = core_calendar:get_top_calendars_by_positive_reviews(10),
|
||||||
|
TopNegative = core_calendar:get_top_calendars_by_negative_reviews(10),
|
||||||
|
TopRating = core_calendar:get_top_calendars_by_rating(10),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_calendars">> => Total,
|
||||||
|
<<"calendars_by_type">> => maps:from_list([{atom_to_binary(T, utf8), C} || {T, C} <- ByType]),
|
||||||
|
<<"calendars_by_status">> => maps:from_list([{atom_to_binary(S, utf8), C} || {S, C} <- ByStatus]),
|
||||||
|
<<"top_calendars_by_reviews">> => format_calendar_list(TopReviews),
|
||||||
|
<<"top_calendars_by_positive_reviews">> => format_calendar_list(TopPositive),
|
||||||
|
<<"top_calendars_by_negative_reviews">> => format_calendar_list(TopNegative),
|
||||||
|
<<"top_calendars_by_rating">> => format_calendar_list(TopRating)
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
|
|
||||||
|
format_calendar_list(List) ->
|
||||||
|
lists:map(fun(#calendar{id = Id, title = Title, rating_avg = RAvg}) ->
|
||||||
|
#{<<"id">> => Id, <<"title">> => Title, <<"rating_avg">> => RAvg}
|
||||||
|
end, List).
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по событиям.
|
||||||
|
%%% GET /v1/admin/events/stats – возвращает общее количество,
|
||||||
|
%%% распределение по типам, статусам и топ-10 по рейтингу.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_event_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%%% cowboy_handler callback
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_event_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%% Swagger metadata
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/admin/events/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Get detailed event statistics (total, by type, by status, top 10 by rating)">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Event statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => event_stats_schema()}}
|
||||||
|
},
|
||||||
|
403 => #{description => <<"Admin access required">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
event_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_events">> => #{type => integer, description => <<"Total number of events (master, active)">>},
|
||||||
|
<<"events_by_type">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of events grouped by type (single, recurring)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"events_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of events grouped by status (active, cancelled, completed)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"top_events_by_rating">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Top 10 events by average rating">>,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"id">> => #{type => string},
|
||||||
|
<<"title">> => #{type => string},
|
||||||
|
<<"rating_avg">> => #{type => number, format => float},
|
||||||
|
<<"rating_count">> => #{type => integer}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
%% @doc GET /v1/admin/events/stats – статистика по событиям.
|
||||||
|
-spec get_event_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
|
get_event_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_event:count_events(),
|
||||||
|
ByType = core_event:count_events_by_type(),
|
||||||
|
ByStatus = core_event:count_events_by_status(),
|
||||||
|
Top10 = core_event:get_top_events_by_rating(10),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_events">> => Total,
|
||||||
|
<<"events_by_type">> => maps:from_list([{atom_to_binary(Type, utf8), Count} || {Type, Count} <- ByType]),
|
||||||
|
<<"events_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]),
|
||||||
|
<<"top_events_by_rating">> => lists:map(fun event_to_map/1, Top10)
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
|
|
||||||
|
event_to_map(Event) ->
|
||||||
|
#{
|
||||||
|
<<"id">> => Event#event.id,
|
||||||
|
<<"title">> => Event#event.title,
|
||||||
|
<<"rating_avg">> => Event#event.rating_avg,
|
||||||
|
<<"rating_count">> => Event#event.rating_count
|
||||||
|
}.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по жалобам.
|
||||||
|
%%% GET /v1/admin/reports/stats – возвращает общее количество,
|
||||||
|
%%% распределение по типам цели, статусам и топ-10 целей по жалобам.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_report_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%%% cowboy_handler callback
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_report_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%% Swagger metadata
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/admin/reports/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Get detailed report statistics (total, by target type, by status, top 10 targets by reports)">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Report statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => report_stats_schema()}}
|
||||||
|
},
|
||||||
|
403 => #{description => <<"Admin access required">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
report_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_reports">> => #{type => integer, description => <<"Total number of reports">>},
|
||||||
|
<<"reports_by_target_type">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of reports grouped by target type (event, calendar, review)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"reports_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of reports grouped by status (pending, reviewed, dismissed)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"top_targets_by_reports">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Top 10 targets by number of reports">>,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"target_type">> => #{type => string},
|
||||||
|
<<"target_id">> => #{type => string},
|
||||||
|
<<"report_count">> => #{type => integer}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
%% @doc GET /v1/admin/reports/stats – статистика по жалобам.
|
||||||
|
-spec get_report_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
|
get_report_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_report:count_reports(),
|
||||||
|
ByTargetType = core_report:count_reports_by_target_type(),
|
||||||
|
ByStatus = core_report:count_reports_by_status(),
|
||||||
|
Top10 = core_report:get_top_targets_by_reports(10),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_reports">> => Total,
|
||||||
|
<<"reports_by_target_type">> => maps:from_list([{atom_to_binary(Type, utf8), Count} || {Type, Count} <- ByTargetType]),
|
||||||
|
<<"reports_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]),
|
||||||
|
<<"top_targets_by_reports">> => lists:map(fun({TargetType, TargetId, Count}) ->
|
||||||
|
#{
|
||||||
|
<<"target_type">> => atom_to_binary(TargetType, utf8),
|
||||||
|
<<"target_id">> => TargetId,
|
||||||
|
<<"report_count">> => Count
|
||||||
|
}
|
||||||
|
end, Top10)
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по отзывам.
|
||||||
|
%%% GET /v1/admin/reviews/stats – возвращает общее количество,
|
||||||
|
%%% распределение по типам цели, статусам, топ-10 целей по отзывам
|
||||||
|
%%% (всего, позитивных, негативных).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_reviews_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%%% cowboy_handler callback
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_review_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%% Swagger metadata
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/admin/reviews/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Get detailed review statistics (total, by target type, by status, top targets by reviews/positive/negative)">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Review statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => review_stats_schema()}}
|
||||||
|
},
|
||||||
|
403 => #{description => <<"Admin access required">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
review_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_reviews">> => #{type => integer, description => <<"Total number of reviews">>},
|
||||||
|
<<"reviews_by_target_type">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of reviews grouped by target type (event, calendar)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"reviews_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of reviews grouped by status (visible, hidden, deleted)">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"top_targets_by_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Top 10 targets by total number of reviews">>,
|
||||||
|
items => target_review_count_schema()
|
||||||
|
},
|
||||||
|
<<"top_targets_by_positive_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Top 10 targets by positive reviews (rating >= 4)">>,
|
||||||
|
items => target_review_count_schema()
|
||||||
|
},
|
||||||
|
<<"top_targets_by_negative_reviews">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Top 10 targets by negative reviews (rating <= 2)">>,
|
||||||
|
items => target_review_count_schema()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
target_review_count_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"target_type">> => #{type => string},
|
||||||
|
<<"target_id">> => #{type => string},
|
||||||
|
<<"review_count">> => #{type => integer}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
%% @doc GET /v1/admin/reviews/stats – статистика по отзывам.
|
||||||
|
-spec get_review_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
|
get_review_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_review:count_reviews(),
|
||||||
|
ByTargetType = core_review:count_reviews_by_target_type(),
|
||||||
|
ByStatus = core_review:count_reviews_by_status(),
|
||||||
|
TopAll = core_review:get_top_targets_by_reviews(10),
|
||||||
|
TopPositive = core_review:get_top_targets_by_positive_reviews(10),
|
||||||
|
TopNegative = core_review:get_top_targets_by_negative_reviews(10),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_reviews">> => Total,
|
||||||
|
<<"reviews_by_target_type">> => maps:from_list([{atom_to_binary(Type, utf8), Count} || {Type, Count} <- ByTargetType]),
|
||||||
|
<<"reviews_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]),
|
||||||
|
<<"top_targets_by_reviews">> => lists:map(fun target_to_map/1, TopAll),
|
||||||
|
<<"top_targets_by_positive_reviews">> => lists:map(fun target_to_map/1, TopPositive),
|
||||||
|
<<"top_targets_by_negative_reviews">> => lists:map(fun target_to_map/1, TopNegative)
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
|
|
||||||
|
target_to_map({TargetType, TargetId, Count}) ->
|
||||||
|
#{
|
||||||
|
<<"target_type">> => atom_to_binary(TargetType, utf8),
|
||||||
|
<<"target_id">> => TargetId,
|
||||||
|
<<"review_count">> => Count
|
||||||
|
}.
|
||||||
@@ -47,15 +47,18 @@ stats_schema() ->
|
|||||||
type => object,
|
type => object,
|
||||||
properties => #{
|
properties => #{
|
||||||
<<"users_total">> => #{type => integer, description => <<"Total number of users">>},
|
<<"users_total">> => #{type => integer, description => <<"Total number of users">>},
|
||||||
|
<<"pending_users_total">> => #{type => integer, description => <<"Number of users awaiting email verification">>},
|
||||||
<<"events_total">> => #{type => integer, description => <<"Total number of events">>},
|
<<"events_total">> => #{type => integer, description => <<"Total number of events">>},
|
||||||
<<"reviews_total">> => #{type => integer, description => <<"Total number of reviews">>},
|
|
||||||
<<"calendars_total">> => #{type => integer, description => <<"Total number of calendars">>},
|
<<"calendars_total">> => #{type => integer, description => <<"Total number of calendars">>},
|
||||||
<<"reports_total">> => #{type => integer, description => <<"Total number of reports">>},
|
<<"reviews_total">> => #{type => integer, description => <<"Total number of reviews">>},
|
||||||
|
<<"reports_total">> => #{type => integer, description => <<"Total number of reports (all statuses)">>},
|
||||||
|
<<"reports_pending">> => #{type => integer, description => <<"Number of pending reports">>},
|
||||||
|
<<"reports_reviewed">> => #{type => integer, description => <<"Number of reviewed reports">>},
|
||||||
|
<<"reports_dismissed">> => #{type => integer, description => <<"Number of dismissed reports">>},
|
||||||
<<"tickets_total">> => #{type => integer, description => <<"Total number of tickets">>},
|
<<"tickets_total">> => #{type => integer, description => <<"Total number of tickets">>},
|
||||||
<<"tickets_open">> => #{type => integer, description => <<"Number of open tickets">>},
|
<<"tickets_open">> => #{type => integer, description => <<"Number of open tickets">>},
|
||||||
<<"subscriptions_total">> => #{type => integer, description => <<"Total number of subscriptions">>},
|
<<"subscriptions_total">> => #{type => integer, description => <<"Total number of subscriptions">>},
|
||||||
<<"active_subscriptions">> => #{type => integer, description => <<"Number of active subscriptions">>},
|
<<"trial_subscriptions">> => #{type => integer, description => <<"Number of trial subscriptions">>},
|
||||||
<<"pending_users_total">> => #{type => integer, description => <<"Number of users awaiting email verification">>},
|
|
||||||
<<"avg_ticket_resolution_h">> => #{type => number, format => float, description => <<"Average ticket resolution time in hours">>},
|
<<"avg_ticket_resolution_h">> => #{type => number, format => float, description => <<"Average ticket resolution time in hours">>},
|
||||||
<<"registrations_by_day">> => #{
|
<<"registrations_by_day">> => #{
|
||||||
type => array,
|
type => array,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по подпискам.
|
||||||
|
%%% GET /v1/admin/subscriptions/stats – возвращает общее количество,
|
||||||
|
%%% распределение по планам, статусам, пробные, заканчивающиеся платные.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_subscription_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%%% cowboy_handler callback
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_subscription_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%% Swagger metadata
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/admin/subscriptions/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Get detailed subscription statistics (total, by plan, by status, trial count, ending paid subscriptions)">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Subscription statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => subscription_stats_schema()}}
|
||||||
|
},
|
||||||
|
403 => #{description => <<"Admin access required">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
subscription_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_subscriptions">> => #{type => integer, description => <<"Total number of subscriptions">>},
|
||||||
|
<<"subscriptions_by_plan">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of subscriptions grouped by plan">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"subscriptions_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of subscriptions grouped by status">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"trial_subscriptions">> => #{type => integer, description => <<"Number of subscriptions in trial period (trial_used = false)">>},
|
||||||
|
<<"ending_paid_subscriptions">> => #{
|
||||||
|
type => array,
|
||||||
|
description => <<"Paid subscriptions expiring within 30 days">>,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"id">> => #{type => string},
|
||||||
|
<<"user_id">> => #{type => string},
|
||||||
|
<<"plan">> => #{type => string},
|
||||||
|
<<"expires_at">> => #{type => string, format => <<"date-time">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
%% @doc GET /v1/admin/subscriptions/stats – статистика по подпискам.
|
||||||
|
-spec get_subscription_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
|
get_subscription_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_subscription:count_subscriptions(),
|
||||||
|
ByPlan = core_subscription:count_subscriptions_by_plan(),
|
||||||
|
ByStatus = core_subscription:count_subscriptions_by_status(),
|
||||||
|
Trial = core_subscription:count_trial_subscriptions(),
|
||||||
|
EndingPaid = core_subscription:get_ending_paid_subscriptions(30),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_subscriptions">> => Total,
|
||||||
|
<<"subscriptions_by_plan">> => maps:from_list([{atom_to_binary(Plan, utf8), Count} || {Plan, Count} <- ByPlan]),
|
||||||
|
<<"subscriptions_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]),
|
||||||
|
<<"trial_subscriptions">> => Trial,
|
||||||
|
<<"ending_paid_subscriptions">> => lists:map(fun(Sub) ->
|
||||||
|
#{
|
||||||
|
<<"id">> => Sub#subscription.id,
|
||||||
|
<<"user_id">> => Sub#subscription.user_id,
|
||||||
|
<<"plan">> => atom_to_binary(Sub#subscription.plan, utf8),
|
||||||
|
<<"expires_at">> => handler_utils:datetime_to_iso8601(Sub#subscription.expires_at)
|
||||||
|
}
|
||||||
|
end, EndingPaid)
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Административный обработчик для получения статистики по тикетам.
|
%%% @doc Административный обработчик статистики по тикетам.
|
||||||
%%% GET – возвращает агрегированную статистику тикетов
|
%%% GET – возвращает агрегированные данные по тикетам.
|
||||||
%%% (количество по статусам: open, in_progress, resolved, closed).
|
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(admin_handler_ticket_stats).
|
-module(admin_handler_ticket_stats).
|
||||||
-behaviour(cowboy_handler).
|
-behaviour(cowboy_handler).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
-export([trails/0]).
|
-export([trails/0]).
|
||||||
|
|
||||||
@@ -16,7 +14,7 @@
|
|||||||
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"GET">> -> get_stats(Req);
|
<<"GET">> -> get_ticket_stats(Req);
|
||||||
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@@ -27,36 +25,39 @@ trails() ->
|
|||||||
#{
|
#{
|
||||||
path => <<"/v1/admin/tickets/stats">>,
|
path => <<"/v1/admin/tickets/stats">>,
|
||||||
method => <<"GET">>,
|
method => <<"GET">>,
|
||||||
description => <<"Get ticket statistics (admin)">>,
|
description => <<"Get ticket statistics">>,
|
||||||
tags => [<<"Tickets">>],
|
tags => [<<"Tickets">>],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => #{
|
200 => #{
|
||||||
description => <<"Ticket statistics">>,
|
description => <<"Ticket statistics">>,
|
||||||
content => #{<<"application/json">> => #{schema => #{
|
content => #{<<"application/json">> => #{schema => ticket_stats_schema()}}
|
||||||
type => object,
|
|
||||||
properties => #{
|
|
||||||
open => #{type => integer, description => <<"Number of open tickets">>},
|
|
||||||
in_progress => #{type => integer, description => <<"Number of tickets in progress">>},
|
|
||||||
resolved => #{type => integer, description => <<"Number of resolved tickets">>},
|
|
||||||
closed => #{type => integer, description => <<"Number of closed tickets">>},
|
|
||||||
total => #{type => integer, description => <<"Total number of tickets">>}
|
|
||||||
}
|
}
|
||||||
}}}
|
|
||||||
},
|
|
||||||
403 => #{description => <<"Admin access required">>}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
ticket_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
open => #{type => integer, description => <<"Number of open tickets">>},
|
||||||
|
in_progress => #{type => integer, description => <<"Number of in-progress tickets">>},
|
||||||
|
resolved => #{type => integer, description => <<"Number of resolved tickets">>},
|
||||||
|
closed => #{type => integer, description => <<"Number of closed tickets">>},
|
||||||
|
total_tickets => #{type => integer, description => <<"Total number of tickets">>},
|
||||||
|
total_errors => #{type => integer, description => <<"Total error occurrences">>}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
%%% Internal functions
|
%%% Internal functions
|
||||||
|
|
||||||
%% @doc Получить статистику тикетов. Доступно только администраторам.
|
%% @doc GET /v1/admin/tickets/stats – получить статистику по тикетам.
|
||||||
-spec get_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
-spec get_ticket_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
get_stats(Req) ->
|
get_ticket_stats(Req) ->
|
||||||
case handler_utils:auth_admin(Req) of
|
case handler_utils:auth_admin(Req) of
|
||||||
{ok, _AdminId, Req1} ->
|
{ok, AdminId, Req1} ->
|
||||||
Stats = core_ticket:stats(),
|
Stats = logic_ticket:get_statistics(AdminId),
|
||||||
handler_utils:send_json(Req1, 200, Stats);
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Msg, Req1} ->
|
||||||
handler_utils:send_error(Req1, Code, Message)
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
end.
|
end.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Административный обработчик статистики по пользователям.
|
||||||
|
%%% GET /v1/admin/users/stats – возвращает общее количество,
|
||||||
|
%%% распределение по ролям, статусам и число неподтверждённых.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(admin_handler_user_stats).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%%% cowboy_handler callback
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> get_user_stats(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%% Swagger metadata
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/admin/users/stats">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Get detailed user statistics (total, by role, by status, pending)">>,
|
||||||
|
tags => [<<"Statistics">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"User statistics object">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => user_stats_schema()}}
|
||||||
|
},
|
||||||
|
403 => #{description => <<"Admin access required">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
user_stats_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"total_users">> => #{type => integer, description => <<"Total number of users">>},
|
||||||
|
<<"users_by_role">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of users grouped by role">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"users_by_status">> => #{
|
||||||
|
type => object,
|
||||||
|
description => <<"Number of users grouped by status">>,
|
||||||
|
additionalProperties => #{type => integer}
|
||||||
|
},
|
||||||
|
<<"pending_users">> => #{type => integer, description => <<"Number of unverified users">>}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%% Internal functions
|
||||||
|
|
||||||
|
%% @doc GET /v1/admin/users/stats – статистика по пользователям.
|
||||||
|
-spec get_user_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
|
get_user_stats(Req) ->
|
||||||
|
case handler_utils:auth_admin(Req) of
|
||||||
|
{ok, _AdminId, Req1} ->
|
||||||
|
Total = core_user:count_users(),
|
||||||
|
ByRole = core_user:count_users_by_role(),
|
||||||
|
ByStatus = core_user:count_users_by_status(),
|
||||||
|
Pending = core_user:count_pending_users(),
|
||||||
|
Stats = #{
|
||||||
|
<<"total_users">> => Total,
|
||||||
|
<<"users_by_role">> => maps:from_list([{atom_to_binary(Role, utf8), Count} || {Role, Count} <- ByRole]),
|
||||||
|
<<"users_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]),
|
||||||
|
<<"pending_users">> => Pending
|
||||||
|
},
|
||||||
|
handler_utils:send_json(Req1, 200, Stats);
|
||||||
|
{error, Code, Msg, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Msg)
|
||||||
|
end.
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @doc Модуль сбора статистики для административной панели.
|
|
||||||
%%%
|
|
||||||
%%% Предоставляет функции `get_stats/2` и `get_stats/4`, возвращающие
|
|
||||||
%%% агрегированные показатели в зависимости от роли администратора.
|
|
||||||
%%% @end
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(logic_stats).
|
-module(logic_stats).
|
||||||
-export([get_stats/2, get_stats/4]).
|
-export([get_stats/2, get_stats/4]).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
@@ -53,9 +46,14 @@ build_superadmin_stats(From, To) ->
|
|||||||
events_total => core_event:count_events(),
|
events_total => core_event:count_events(),
|
||||||
calendars_total => core_calendar:count_calendars(),
|
calendars_total => core_calendar:count_calendars(),
|
||||||
reviews_total => core_review:count_reviews(),
|
reviews_total => core_review:count_reviews(),
|
||||||
reports_total => core_report:count_reports_by_status(pending),
|
reports_total => core_report:count_reports(), % все жалобы
|
||||||
|
reports_pending => core_report:count_reports_by_status(pending),
|
||||||
|
reports_reviewed => core_report:count_reports_by_status(reviewed),
|
||||||
|
reports_dismissed => core_report:count_reports_by_status(dismissed),
|
||||||
tickets_open => core_ticket:count_tickets_by_status(open),
|
tickets_open => core_ticket:count_tickets_by_status(open),
|
||||||
tickets_total => length(core_ticket:list_all()),
|
tickets_total => length(core_ticket:list_all()),
|
||||||
|
subscriptions_total => core_subscription:count_subscriptions(),
|
||||||
|
trial_subscriptions => core_subscription:count_trial_subscriptions(),
|
||||||
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time()),
|
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time()),
|
||||||
registrations_by_day => date_list_to_json(core_user:count_users_by_date(From, To)),
|
registrations_by_day => date_list_to_json(core_user:count_users_by_date(From, To)),
|
||||||
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
|
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
|
||||||
@@ -80,9 +78,14 @@ build_admin_stats(From, To) ->
|
|||||||
events_total => core_event:count_events(),
|
events_total => core_event:count_events(),
|
||||||
calendars_total => core_calendar:count_calendars(),
|
calendars_total => core_calendar:count_calendars(),
|
||||||
reviews_total => core_review:count_reviews(),
|
reviews_total => core_review:count_reviews(),
|
||||||
reports_total => core_report:count_reports_by_status(pending),
|
reports_total => core_report:count_reports(),
|
||||||
|
reports_pending => core_report:count_reports_by_status(pending),
|
||||||
|
reports_reviewed => core_report:count_reports_by_status(reviewed),
|
||||||
|
reports_dismissed => core_report:count_reports_by_status(dismissed),
|
||||||
tickets_open => core_ticket:count_tickets_by_status(open),
|
tickets_open => core_ticket:count_tickets_by_status(open),
|
||||||
tickets_total => length(core_ticket:list_all()),
|
tickets_total => length(core_ticket:list_all()),
|
||||||
|
subscriptions_total => core_subscription:count_subscriptions(),
|
||||||
|
trial_subscriptions => core_subscription:count_trial_subscriptions(),
|
||||||
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time()),
|
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time()),
|
||||||
registrations_by_day => date_list_to_json(core_user:count_users_by_date(From, To)),
|
registrations_by_day => date_list_to_json(core_user:count_users_by_date(From, To)),
|
||||||
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
|
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
|
||||||
|
|||||||
@@ -12,22 +12,27 @@ admin() ->
|
|||||||
admin_handler_health,
|
admin_handler_health,
|
||||||
admin_handler_stats,
|
admin_handler_stats,
|
||||||
admin_handler_login,
|
admin_handler_login,
|
||||||
admin_handler_refresh, % ← добавлен
|
admin_handler_refresh,
|
||||||
% ================== ПОЛЬЗОВАТЕЛИ ==================
|
% ================== ПОЛЬЗОВАТЕЛИ ==================
|
||||||
admin_handler_users,
|
admin_handler_users,
|
||||||
admin_handler_user_by_id,
|
admin_handler_user_by_id,
|
||||||
admin_handler_user_verification_token, % ← добавлен
|
admin_handler_user_verification_token,
|
||||||
|
admin_handler_user_stats,
|
||||||
% ================== КАЛЕНДАРИ ==================
|
% ================== КАЛЕНДАРИ ==================
|
||||||
admin_handler_calendars, % ← добавлен
|
admin_handler_calendars,
|
||||||
admin_handler_calendar_by_id, % ← добавлен
|
admin_handler_calendar_stats,
|
||||||
|
admin_handler_calendar_by_id,
|
||||||
% ================== СОБЫТИЯ ==================
|
% ================== СОБЫТИЯ ==================
|
||||||
admin_handler_events,
|
admin_handler_events,
|
||||||
|
admin_handler_event_stats,
|
||||||
admin_handler_event_by_id,
|
admin_handler_event_by_id,
|
||||||
% ================== ОТЧЁТЫ ==================
|
% ================== ОТЧЁТЫ ==================
|
||||||
admin_handler_reports,
|
admin_handler_reports,
|
||||||
|
admin_handler_report_stats,
|
||||||
admin_handler_report_by_id,
|
admin_handler_report_by_id,
|
||||||
% ================== ОТЗЫВЫ ==================
|
% ================== ОТЗЫВЫ ==================
|
||||||
admin_handler_reviews,
|
admin_handler_reviews,
|
||||||
|
admin_handler_reviews_stats,
|
||||||
admin_handler_reviews_by_id,
|
admin_handler_reviews_by_id,
|
||||||
% ================== БАН-СЛОВА ==================
|
% ================== БАН-СЛОВА ==================
|
||||||
admin_handler_banned_words,
|
admin_handler_banned_words,
|
||||||
@@ -37,13 +42,14 @@ admin() ->
|
|||||||
admin_handler_tickets,
|
admin_handler_tickets,
|
||||||
% ================== ПОДПИСКИ ==================
|
% ================== ПОДПИСКИ ==================
|
||||||
admin_handler_subscriptions,
|
admin_handler_subscriptions,
|
||||||
|
admin_handler_subscription_stats,
|
||||||
admin_handler_subscriptions_by_id,
|
admin_handler_subscriptions_by_id,
|
||||||
% ================== МОДЕРАЦИЯ (общий маршрут) ==================
|
% ================== МОДЕРАЦИЯ (общий маршрут) ==================
|
||||||
admin_handler_moderation,
|
admin_handler_moderation,
|
||||||
% ================== Управление ролями (только для superadmin) ==================
|
% ================== Управление ролями (только для superadmin) ==================
|
||||||
admin_handler_me,
|
admin_handler_me,
|
||||||
admin_handler_admins,
|
admin_handler_admins,
|
||||||
admin_handler_admins_by_id, % ← добавлен
|
admin_handler_admins_by_id,
|
||||||
admin_handler_audit
|
admin_handler_audit
|
||||||
],
|
],
|
||||||
lists:flatmap(fun trails_from_module/1, Modules).
|
lists:flatmap(fun trails_from_module/1, Modules).
|
||||||
@@ -59,7 +65,7 @@ user() ->
|
|||||||
handler_register,
|
handler_register,
|
||||||
handler_login,
|
handler_login,
|
||||||
handler_refresh,
|
handler_refresh,
|
||||||
handler_verify, % ← добавлен
|
handler_verify,
|
||||||
handler_booking_by_id,
|
handler_booking_by_id,
|
||||||
handler_bookings,
|
handler_bookings,
|
||||||
handler_calendar_by_id,
|
handler_calendar_by_id,
|
||||||
|
|||||||
@@ -3,13 +3,24 @@
|
|||||||
%%%
|
%%%
|
||||||
%%% Покрывает эндпоинты:
|
%%% Покрывает эндпоинты:
|
||||||
%%% GET /v1/admin/stats
|
%%% GET /v1/admin/stats
|
||||||
|
%%% GET /v1/admin/users/stats
|
||||||
|
%%% GET /v1/admin/events/stats
|
||||||
|
%%% GET /v1/admin/reports/stats
|
||||||
|
%%% GET /v1/admin/reviews/stats
|
||||||
|
%%% GET /v1/admin/subscriptions/stats
|
||||||
|
%%% GET /v1/admin/calendars/stats
|
||||||
%%%
|
%%%
|
||||||
%%% Проверяет:
|
%%% Проверяет:
|
||||||
%%% - получение статистики для всех четырёх ролей администраторов
|
%%% - получение общей статистики для всех ролей администраторов
|
||||||
%%% - для superadmin и admin – наличие ключевых метрик, включая
|
%%% - для superadmin и admin – наличие ключевых метрик, включая pending_users_total, *_by_day
|
||||||
%%% pending_users_total, *_by_day
|
|
||||||
%%% - для moderator и support – ответ непустой
|
%%% - для moderator и support – ответ непустой
|
||||||
%%% - работу с фильтром по датам (from, to)
|
%%% - работу с фильтром по датам (from, to)
|
||||||
|
%%% - статистику по пользователям (users/stats)
|
||||||
|
%%% - статистику по событиям (events/stats)
|
||||||
|
%%% - статистику по жалобам (reports/stats)
|
||||||
|
%%% - статистику по отзывам (reviews/stats)
|
||||||
|
%%% - статистику по подпискам (subscriptions/stats)
|
||||||
|
%%% - статистику по календарям (calendars/stats)
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(admin_stats_tests).
|
-module(admin_stats_tests).
|
||||||
@@ -27,6 +38,31 @@ test() ->
|
|||||||
ModerToken = api_test_runner:get_moderator_token(),
|
ModerToken = api_test_runner:get_moderator_token(),
|
||||||
SupportToken = api_test_runner:get_support_token(),
|
SupportToken = api_test_runner:get_support_token(),
|
||||||
|
|
||||||
|
% Создаём тестовые данные для ненулевой статистики
|
||||||
|
UserToken = api_test_runner:get_user_token(),
|
||||||
|
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"StatsCal">>}),
|
||||||
|
EventId = api_test_runner:create_event(UserToken, CalId, #{
|
||||||
|
title => <<"Stats Event">>,
|
||||||
|
start_time => api_test_runner:future_date(),
|
||||||
|
duration => 60
|
||||||
|
}),
|
||||||
|
% Бронируем и подтверждаем, чтобы можно было оставить отзыв
|
||||||
|
#{<<"id">> := BookingId} = api_test_runner:client_post(
|
||||||
|
<<"/v1/events/", EventId/binary, "/bookings">>, UserToken, #{}),
|
||||||
|
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, UserToken,
|
||||||
|
#{action => <<"confirm">>}),
|
||||||
|
% Оставляем отзыв
|
||||||
|
api_test_runner:client_post(<<"/v1/reviews">>, UserToken,
|
||||||
|
#{target_type => <<"event">>, target_id => EventId, rating => 5, comment => <<"Great!">>}),
|
||||||
|
% Жалоба
|
||||||
|
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
|
||||||
|
#{<<"target_type">> => <<"event">>, <<"target_id">> => EventId, <<"reason">> => <<"Test">>}),
|
||||||
|
% Подписка
|
||||||
|
SubUserToken = api_test_runner:get_user_token(),
|
||||||
|
api_test_runner:client_post(<<"/v1/subscription">>, SubUserToken,
|
||||||
|
#{<<"action">> => <<"start_trial">>}),
|
||||||
|
|
||||||
|
% Общая статистика
|
||||||
test_stats_for_role("Superadmin", SuperToken, strict),
|
test_stats_for_role("Superadmin", SuperToken, strict),
|
||||||
test_stats_for_role("Admin", AdminToken, strict),
|
test_stats_for_role("Admin", AdminToken, strict),
|
||||||
test_stats_for_role("Moderator", ModerToken, loose),
|
test_stats_for_role("Moderator", ModerToken, loose),
|
||||||
@@ -34,6 +70,14 @@ test() ->
|
|||||||
|
|
||||||
test_stats_with_dates(SuperToken),
|
test_stats_with_dates(SuperToken),
|
||||||
|
|
||||||
|
% Детальная статистика
|
||||||
|
test_user_stats(SuperToken),
|
||||||
|
test_event_stats(SuperToken),
|
||||||
|
test_report_stats(SuperToken),
|
||||||
|
test_review_stats(SuperToken),
|
||||||
|
test_subscription_stats(SuperToken),
|
||||||
|
test_calendar_stats(SuperToken),
|
||||||
|
|
||||||
ct:pal("=== All admin stats tests passed ==="),
|
ct:pal("=== All admin stats tests passed ==="),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
@@ -41,9 +85,6 @@ test() ->
|
|||||||
%%% Тестовые функции
|
%%% Тестовые функции
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
%% @doc Проверяет получение статистики для конкретной роли.
|
|
||||||
%% strict – ожидаем ключи users_total/events_total/pending_users_total и *_by_day
|
|
||||||
%% loose – просто убеждаемся, что ответ непустой
|
|
||||||
-spec test_stats_for_role(string(), binary(), strict | loose) -> ok.
|
-spec test_stats_for_role(string(), binary(), strict | loose) -> ok.
|
||||||
test_stats_for_role(RoleName, Token, Strictness) ->
|
test_stats_for_role(RoleName, Token, Strictness) ->
|
||||||
ct:pal(" TEST: Get stats for role ~s", [RoleName]),
|
ct:pal(" TEST: Get stats for role ~s", [RoleName]),
|
||||||
@@ -51,18 +92,32 @@ test_stats_for_role(RoleName, Token, Strictness) ->
|
|||||||
?assert(is_map(Stats)),
|
?assert(is_map(Stats)),
|
||||||
case Strictness of
|
case Strictness of
|
||||||
strict ->
|
strict ->
|
||||||
HasUsers = maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats),
|
% Тотальные значения
|
||||||
HasEvents = maps:is_key(<<"events_total">>, Stats) orelse maps:is_key(<<"events">>, Stats),
|
?assert(maps:is_key(<<"users_total">>, Stats)),
|
||||||
?assert(HasUsers orelse HasEvents),
|
|
||||||
% Проверяем наличие новых ключей
|
|
||||||
?assert(maps:is_key(<<"pending_users_total">>, Stats)),
|
?assert(maps:is_key(<<"pending_users_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"events_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"calendars_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reviews_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_pending">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_reviewed">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_dismissed">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"tickets_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"tickets_open">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"subscriptions_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"trial_subscriptions">>, Stats)),
|
||||||
|
% Массивы по дням
|
||||||
|
?assert(maps:is_key(<<"registrations_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"events_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"reviews_by_day">>, Stats)),
|
?assert(maps:is_key(<<"reviews_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"calendars_by_day">>, Stats)),
|
?assert(maps:is_key(<<"calendars_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"reports_by_day">>, Stats)),
|
?assert(maps:is_key(<<"reports_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"tickets_by_day">>, Stats)),
|
?assert(maps:is_key(<<"tickets_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"subscriptions_by_day">>, Stats)),
|
?assert(maps:is_key(<<"subscriptions_by_day">>, Stats)),
|
||||||
?assert(maps:is_key(<<"pending_users_by_day">>, Stats)),
|
?assert(maps:is_key(<<"pending_users_by_day">>, Stats)),
|
||||||
% Проверяем, что *_by_day являются списками
|
% Проверка, что они действительно списки
|
||||||
|
?assert(is_list(maps:get(<<"registrations_by_day">>, Stats))),
|
||||||
|
?assert(is_list(maps:get(<<"events_by_day">>, Stats))),
|
||||||
?assert(is_list(maps:get(<<"reviews_by_day">>, Stats))),
|
?assert(is_list(maps:get(<<"reviews_by_day">>, Stats))),
|
||||||
?assert(is_list(maps:get(<<"calendars_by_day">>, Stats))),
|
?assert(is_list(maps:get(<<"calendars_by_day">>, Stats))),
|
||||||
?assert(is_list(maps:get(<<"reports_by_day">>, Stats))),
|
?assert(is_list(maps:get(<<"reports_by_day">>, Stats))),
|
||||||
@@ -74,7 +129,6 @@ test_stats_for_role(RoleName, Token, Strictness) ->
|
|||||||
end,
|
end,
|
||||||
ct:pal(" OK: ~p keys", [length(maps:keys(Stats))]).
|
ct:pal(" OK: ~p keys", [length(maps:keys(Stats))]).
|
||||||
|
|
||||||
%% @doc GET /v1/admin/stats?from=...&to=... – проверяет фильтрацию по датам.
|
|
||||||
-spec test_stats_with_dates(binary()) -> ok.
|
-spec test_stats_with_dates(binary()) -> ok.
|
||||||
test_stats_with_dates(Token) ->
|
test_stats_with_dates(Token) ->
|
||||||
ct:pal(" TEST: Get stats with date range"),
|
ct:pal(" TEST: Get stats with date range"),
|
||||||
@@ -85,3 +139,84 @@ test_stats_with_dates(Token) ->
|
|||||||
?assert(is_map(Stats)),
|
?assert(is_map(Stats)),
|
||||||
?assert(maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats)),
|
?assert(maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats)),
|
||||||
ct:pal(" OK").
|
ct:pal(" OK").
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/users/stats.
|
||||||
|
-spec test_user_stats(binary()) -> ok.
|
||||||
|
test_user_stats(Token) ->
|
||||||
|
ct:pal(" TEST: User statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/users/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_users">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"users_by_role">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"users_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"pending_users">>, Stats)),
|
||||||
|
ct:pal(" OK: total=~p, pending=~p", [maps:get(<<"total_users">>, Stats), maps:get(<<"pending_users">>, Stats)]).
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/events/stats.
|
||||||
|
-spec test_event_stats(binary()) -> ok.
|
||||||
|
test_event_stats(Token) ->
|
||||||
|
ct:pal(" TEST: Event statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/events/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_events">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"events_by_type">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"events_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_events_by_rating">>, Stats)),
|
||||||
|
?assert(is_list(maps:get(<<"top_events_by_rating">>, Stats))),
|
||||||
|
ct:pal(" OK: total=~p", [maps:get(<<"total_events">>, Stats)]).
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/reports/stats.
|
||||||
|
-spec test_report_stats(binary()) -> ok.
|
||||||
|
test_report_stats(Token) ->
|
||||||
|
ct:pal(" TEST: Report statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/reports/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_reports">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_by_target_type">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_targets_by_reports">>, Stats)),
|
||||||
|
?assert(is_list(maps:get(<<"top_targets_by_reports">>, Stats))),
|
||||||
|
ct:pal(" OK: total=~p", [maps:get(<<"total_reports">>, Stats)]).
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/reviews/stats.
|
||||||
|
-spec test_review_stats(binary()) -> ok.
|
||||||
|
test_review_stats(Token) ->
|
||||||
|
ct:pal(" TEST: Review statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/reviews/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reviews_by_target_type">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reviews_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_targets_by_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_targets_by_positive_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_targets_by_negative_reviews">>, Stats)),
|
||||||
|
ct:pal(" OK: total=~p", [maps:get(<<"total_reviews">>, Stats)]).
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/subscriptions/stats.
|
||||||
|
-spec test_subscription_stats(binary()) -> ok.
|
||||||
|
test_subscription_stats(Token) ->
|
||||||
|
ct:pal(" TEST: Subscription statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/subscriptions/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_subscriptions">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"subscriptions_by_plan">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"subscriptions_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"trial_subscriptions">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"ending_paid_subscriptions">>, Stats)),
|
||||||
|
?assert(is_list(maps:get(<<"ending_paid_subscriptions">>, Stats))),
|
||||||
|
ct:pal(" OK: total=~p, trial=~p", [maps:get(<<"total_subscriptions">>, Stats), maps:get(<<"trial_subscriptions">>, Stats)]).
|
||||||
|
|
||||||
|
%% @doc Тестирует эндпоинт GET /v1/admin/calendars/stats.
|
||||||
|
-spec test_calendar_stats(binary()) -> ok.
|
||||||
|
test_calendar_stats(Token) ->
|
||||||
|
ct:pal(" TEST: Calendar statistics"),
|
||||||
|
Stats = api_test_runner:admin_get(<<"/v1/admin/calendars/stats">>, Token),
|
||||||
|
?assert(is_map(Stats)),
|
||||||
|
?assert(maps:is_key(<<"total_calendars">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"calendars_by_type">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"calendars_by_status">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_calendars_by_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_calendars_by_positive_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_calendars_by_negative_reviews">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"top_calendars_by_rating">>, Stats)),
|
||||||
|
ct:pal(" OK: total=~p", [maps:get(<<"total_calendars">>, Stats)]).
|
||||||
@@ -14,9 +14,6 @@
|
|||||||
-module(user_verification_tests).
|
-module(user_verification_tests).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-export([test/0]).
|
-export([test/0]).
|
||||||
-export([verification_test/0]).
|
|
||||||
|
|
||||||
verification_test() -> test().
|
|
||||||
|
|
||||||
-spec test() -> ok.
|
-spec test() -> ok.
|
||||||
test() ->
|
test() ->
|
||||||
|
|||||||
Reference in New Issue
Block a user