Расширена общая статистика по дням: Отзывы, Календари, Жалобы, Тикеты, Подписки #22
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
-export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
-export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
||||||
-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]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -152,6 +153,27 @@ freeze(Id, Reason) ->
|
|||||||
unfreeze(Id, Reason) ->
|
unfreeze(Id, Reason) ->
|
||||||
update(Id, [{status, active}, {reason, Reason}]).
|
update(Id, [{status, active}, {reason, Reason}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт календарей, созданных в заданном временном диапазоне.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_calendars_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_calendars_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#calendar{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(C) -> C#calendar.created_at >= From andalso
|
||||||
|
C#calendar.created_at =< To end, All),
|
||||||
|
Counts = lists:foldl(fun(C, Acc) ->
|
||||||
|
Day = date_part(C#calendar.created_at),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, Cnt} -> lists:keyreplace(Day, 1, Acc, {Day, Cnt+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
@@ -3,6 +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]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -156,4 +157,25 @@ avg_resolution_time(Status) ->
|
|||||||
calendar:datetime_to_gregorian_seconds(R#report.created_at)
|
calendar:datetime_to_gregorian_seconds(R#report.created_at)
|
||||||
|| R <- Reports, R#report.resolved_at /= undefined]),
|
|| R <- Reports, R#report.resolved_at /= undefined]),
|
||||||
Total / length(Reports) / 3600.0
|
Total / length(Reports) / 3600.0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт жалоб, созданных в заданном временном диапазоне.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reports_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_reports_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#report{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(R) -> R#report.created_at >= From andalso
|
||||||
|
R#report.created_at =< To end, All),
|
||||||
|
Counts = lists:foldl(fun(R, Acc) ->
|
||||||
|
Day = date_part(R#report.created_at),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
hide/2, unhide/2]).
|
hide/2, unhide/2]).
|
||||||
-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]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -182,6 +183,27 @@ count_reviews() ->
|
|||||||
list_all() ->
|
list_all() ->
|
||||||
mnesia:dirty_match_object(#review{_ = '_'}).
|
mnesia:dirty_match_object(#review{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт отзывов, созданных в заданном временном диапазоне.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reviews_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_reviews_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#review{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(R) -> R#review.created_at >= From andalso
|
||||||
|
R#review.created_at =< To end, All),
|
||||||
|
Counts = lists:foldl(fun(R, Acc) ->
|
||||||
|
Day = date_part(R#review.created_at),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
-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_subscription/0]).
|
||||||
|
-export([count_subscriptions_by_date/2]).
|
||||||
|
|
||||||
-define(TRIAL_DAYS, 30).
|
-define(TRIAL_DAYS, 30).
|
||||||
|
|
||||||
@@ -292,4 +293,25 @@ parse_iso_datetime(Other) -> Other.
|
|||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-spec count_subscription() -> non_neg_integer().
|
-spec count_subscription() -> non_neg_integer().
|
||||||
count_subscription() ->
|
count_subscription() ->
|
||||||
mnesia:table_info(subscription, size).
|
mnesia:table_info(subscription, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт подписок, созданных в заданном временном диапазоне.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_subscriptions_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_subscriptions_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#subscription{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(S) -> S#subscription.created_at >= From andalso
|
||||||
|
S#subscription.created_at =< To end, All),
|
||||||
|
Counts = lists:foldl(fun(S, Acc) ->
|
||||||
|
Day = date_part(S#subscription.created_at),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
-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]).
|
||||||
-export([update_ticket/2]).
|
-export([update_ticket/2]).
|
||||||
-export([delete_ticket/1]). % ← для admin_handler_ticket_by_id
|
-export([delete_ticket/1]).
|
||||||
|
-export([count_tickets_by_date/2]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
%% Значения по умолчанию для необязательных полей
|
%% Значения по умолчанию для необязательных полей
|
||||||
@@ -248,6 +249,27 @@ count_tickets_by_admin(AdminId, Status) ->
|
|||||||
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт тикетов, созданных в заданном временном диапазоне.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_tickets_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_tickets_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#ticket{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(T) -> T#ticket.first_seen >= From andalso
|
||||||
|
T#ticket.first_seen =< To end, All),
|
||||||
|
Counts = lists:foldl(fun(T, Acc) ->
|
||||||
|
Day = date_part(T#ticket.first_seen),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
+30
-1
@@ -5,7 +5,7 @@
|
|||||||
-export([email_exists/1]).
|
-export([email_exists/1]).
|
||||||
-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, 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([create_bot/2, delete_bot/1]).
|
-export([create_bot/2, delete_bot/1]).
|
||||||
|
|
||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
@@ -277,6 +277,35 @@ count_users_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
lists:sort(Counts).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество неподтверждённых пользователей (status = pending).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_pending_users() -> non_neg_integer().
|
||||||
|
count_pending_users() ->
|
||||||
|
Match = #user{status = pending, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт неподтверждённых пользователей (status=pending) по дням.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_pending_users_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
|
count_pending_users_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
|
Pending = lists:filter(fun(U) -> U#user.status =:= pending end, All),
|
||||||
|
Filtered = lists:filter(fun(U) -> U#user.created_at >= From andalso
|
||||||
|
U#user.created_at =< To end, Pending),
|
||||||
|
Counts = lists:foldl(fun(U, Acc) ->
|
||||||
|
Day = date_part(U#user.created_at),
|
||||||
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
|
false -> [{Day, 1} | Acc];
|
||||||
|
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
|
||||||
|
end
|
||||||
|
end, [], Filtered),
|
||||||
|
lists:sort(Counts).
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
||||||
%%% @end
|
%%% @end
|
||||||
|
|||||||
@@ -35,10 +35,7 @@ trails() ->
|
|||||||
responses => #{
|
responses => #{
|
||||||
200 => #{
|
200 => #{
|
||||||
description => <<"Statistics object">>,
|
description => <<"Statistics object">>,
|
||||||
content => #{<<"application/json">> => #{schema => #{
|
content => #{<<"application/json">> => #{schema => stats_schema()}}
|
||||||
type => object,
|
|
||||||
properties => stats_schema()
|
|
||||||
}}}
|
|
||||||
},
|
},
|
||||||
403 => #{description => <<"Admin access required">>}
|
403 => #{description => <<"Admin access required">>}
|
||||||
}
|
}
|
||||||
@@ -47,14 +44,65 @@ trails() ->
|
|||||||
|
|
||||||
stats_schema() ->
|
stats_schema() ->
|
||||||
#{
|
#{
|
||||||
<<"users">> => #{type => integer, description => <<"Total number of users">>},
|
type => object,
|
||||||
<<"events">> => #{type => integer},
|
properties => #{
|
||||||
<<"reviews">> => #{type => integer},
|
<<"users_total">> => #{type => integer, description => <<"Total number of users">>},
|
||||||
<<"calendars">> => #{type => integer},
|
<<"events_total">> => #{type => integer, description => <<"Total number of events">>},
|
||||||
<<"reports">> => #{type => integer},
|
<<"reviews_total">> => #{type => integer, description => <<"Total number of reviews">>},
|
||||||
<<"tickets">> => #{type => integer},
|
<<"calendars_total">> => #{type => integer, description => <<"Total number of calendars">>},
|
||||||
<<"subscriptions">> => #{type => integer},
|
<<"reports_total">> => #{type => integer, description => <<"Total number of reports">>},
|
||||||
<<"active_subscriptions">> => #{type => integer}
|
<<"tickets_total">> => #{type => integer, description => <<"Total number of tickets">>},
|
||||||
|
<<"tickets_open">> => #{type => integer, description => <<"Number of open tickets">>},
|
||||||
|
<<"subscriptions_total">> => #{type => integer, description => <<"Total number of subscriptions">>},
|
||||||
|
<<"active_subscriptions">> => #{type => integer, description => <<"Number of active 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">>},
|
||||||
|
<<"registrations_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily user registrations">>
|
||||||
|
},
|
||||||
|
<<"events_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily event creations">>
|
||||||
|
},
|
||||||
|
<<"reviews_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily review creations">>
|
||||||
|
},
|
||||||
|
<<"calendars_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily calendar creations">>
|
||||||
|
},
|
||||||
|
<<"reports_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily report creations">>
|
||||||
|
},
|
||||||
|
<<"tickets_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily ticket creations">>
|
||||||
|
},
|
||||||
|
<<"subscriptions_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily subscription creations">>
|
||||||
|
},
|
||||||
|
<<"pending_users_by_day">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => #{<<"date">> => #{type => string}, <<"count">> => #{type => integer}}},
|
||||||
|
description => <<"Daily pending user registrations">>
|
||||||
|
},
|
||||||
|
<<"admin_activity">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object},
|
||||||
|
description => <<"Recent admin actions (superadmin only)">>
|
||||||
|
}
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%% Internal functions
|
%%% Internal functions
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ get_stats(_, _, _, _) ->
|
|||||||
build_superadmin_stats(From, To) ->
|
build_superadmin_stats(From, To) ->
|
||||||
#{
|
#{
|
||||||
users_total => core_user:count_users(),
|
users_total => core_user:count_users(),
|
||||||
|
pending_users_total => core_user:count_pending_users(),
|
||||||
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(),
|
||||||
@@ -58,6 +59,12 @@ build_superadmin_stats(From, To) ->
|
|||||||
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)),
|
||||||
|
reviews_by_day => date_list_to_json(core_review:count_reviews_by_date(From, To)),
|
||||||
|
calendars_by_day => date_list_to_json(core_calendar:count_calendars_by_date(From, To)),
|
||||||
|
reports_by_day => date_list_to_json(core_report:count_reports_by_date(From, To)),
|
||||||
|
tickets_by_day => date_list_to_json(core_ticket:count_tickets_by_date(From, To)),
|
||||||
|
subscriptions_by_day => date_list_to_json(core_subscription:count_subscriptions_by_date(From, To)),
|
||||||
|
pending_users_by_day => date_list_to_json(core_user:count_pending_users_by_date(From, To)),
|
||||||
admin_activity => collect_admin_activity()
|
admin_activity => collect_admin_activity()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
@@ -69,6 +76,7 @@ build_superadmin_stats(From, To) ->
|
|||||||
build_admin_stats(From, To) ->
|
build_admin_stats(From, To) ->
|
||||||
#{
|
#{
|
||||||
users_total => core_user:count_users(),
|
users_total => core_user:count_users(),
|
||||||
|
pending_users_total => core_user:count_pending_users(),
|
||||||
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(),
|
||||||
@@ -77,7 +85,13 @@ build_admin_stats(From, To) ->
|
|||||||
tickets_total => length(core_ticket:list_all()),
|
tickets_total => length(core_ticket:list_all()),
|
||||||
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)),
|
||||||
|
reviews_by_day => date_list_to_json(core_review:count_reviews_by_date(From, To)),
|
||||||
|
calendars_by_day => date_list_to_json(core_calendar:count_calendars_by_date(From, To)),
|
||||||
|
reports_by_day => date_list_to_json(core_report:count_reports_by_date(From, To)),
|
||||||
|
tickets_by_day => date_list_to_json(core_ticket:count_tickets_by_date(From, To)),
|
||||||
|
subscriptions_by_day => date_list_to_json(core_subscription:count_subscriptions_by_date(From, To)),
|
||||||
|
pending_users_by_day => date_list_to_json(core_user:count_pending_users_by_date(From, To))
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,28 +2,26 @@
|
|||||||
%%% @doc Тесты административного API для получения статистики.
|
%%% @doc Тесты административного API для получения статистики.
|
||||||
%%%
|
%%%
|
||||||
%%% Покрывает эндпоинты:
|
%%% Покрывает эндпоинты:
|
||||||
%%% GET /v1/admin/stats
|
%%% GET /v1/admin/stats
|
||||||
%%%
|
%%%
|
||||||
%%% Проверяет:
|
%%% Проверяет:
|
||||||
%%% - получение статистики для всех четырёх ролей администраторов
|
%%% - получение статистики для всех четырёх ролей администраторов
|
||||||
%%% - для superadmin и admin – наличие ключевых метрик
|
%%% - для superadmin и admin – наличие ключевых метрик, включая
|
||||||
%%% - для moderator и support – ответ непустой
|
%%% pending_users_total, *_by_day
|
||||||
%%% - работу с фильтром по датам (from, to)
|
%%% - для moderator и support – ответ непустой
|
||||||
|
%%% - работу с фильтром по датам (from, to)
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(admin_stats_tests).
|
-module(admin_stats_tests).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
-export([test/0]).
|
-export([test/0]).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Главная тестовая функция
|
%%% Главная тестовая функция
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
-spec test() -> ok.
|
-spec test() -> ok.
|
||||||
test() ->
|
test() ->
|
||||||
ct:pal("=== Admin Stats Tests ==="),
|
ct:pal("=== Admin Stats Tests ==="),
|
||||||
|
|
||||||
SuperToken = api_test_runner:get_superadmin_token(),
|
SuperToken = api_test_runner:get_superadmin_token(),
|
||||||
AdminToken = api_test_runner:get_admin_token(),
|
AdminToken = api_test_runner:get_admin_token(),
|
||||||
ModerToken = api_test_runner:get_moderator_token(),
|
ModerToken = api_test_runner:get_moderator_token(),
|
||||||
@@ -44,34 +42,46 @@ test() ->
|
|||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
%% @doc Проверяет получение статистики для конкретной роли.
|
%% @doc Проверяет получение статистики для конкретной роли.
|
||||||
%% strict – ожидаем ключи users_total/users и events_total/events
|
%% strict – ожидаем ключи users_total/events_total/pending_users_total и *_by_day
|
||||||
%% loose – просто убеждаемся, что ответ непустой
|
%% 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]),
|
||||||
Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token),
|
Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token),
|
||||||
?assert(is_map(Stats)),
|
?assert(is_map(Stats)),
|
||||||
case Strictness of
|
case Strictness of
|
||||||
strict ->
|
strict ->
|
||||||
HasUsers = maps:is_key(<<"users_total">>, Stats) orelse
|
HasUsers = maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats),
|
||||||
maps:is_key(<<"users">>, Stats),
|
HasEvents = maps:is_key(<<"events_total">>, Stats) orelse maps:is_key(<<"events">>, Stats),
|
||||||
HasEvents = maps:is_key(<<"events_total">>, Stats) orelse
|
?assert(HasUsers orelse HasEvents),
|
||||||
maps:is_key(<<"events">>, Stats),
|
% Проверяем наличие новых ключей
|
||||||
?assert(HasUsers orelse HasEvents);
|
?assert(maps:is_key(<<"pending_users_total">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reviews_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"calendars_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"reports_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"tickets_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"subscriptions_by_day">>, Stats)),
|
||||||
|
?assert(maps:is_key(<<"pending_users_by_day">>, Stats)),
|
||||||
|
% Проверяем, что *_by_day являются списками
|
||||||
|
?assert(is_list(maps:get(<<"reviews_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(<<"tickets_by_day">>, Stats))),
|
||||||
|
?assert(is_list(maps:get(<<"subscriptions_by_day">>, Stats))),
|
||||||
|
?assert(is_list(maps:get(<<"pending_users_by_day">>, Stats)));
|
||||||
loose ->
|
loose ->
|
||||||
?assert(map_size(Stats) > 0)
|
?assert(map_size(Stats) > 0)
|
||||||
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=... – проверяет фильтрацию по датам.
|
%% @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"),
|
||||||
From = <<"2026-01-01T00:00:00Z">>,
|
From = <<"2026-01-01T00:00:00Z">>,
|
||||||
To = <<"2026-12-31T23:59:59Z">>,
|
To = <<"2026-12-31T23:59:59Z">>,
|
||||||
Path = <<"/v1/admin/stats?from=", From/binary, "&to=", To/binary>>,
|
Path = <<"/v1/admin/stats?from=", From/binary, "&to=", To/binary>>,
|
||||||
Stats = api_test_runner:admin_get(Path, Token),
|
Stats = api_test_runner:admin_get(Path, Token),
|
||||||
?assert(is_map(Stats)),
|
?assert(is_map(Stats)),
|
||||||
?assert(maps:is_key(<<"users_total">>, Stats) orelse
|
?assert(maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats)),
|
||||||
maps:is_key(<<"users">>, Stats)),
|
ct:pal(" OK").
|
||||||
ct:pal(" OK").
|
|
||||||
Reference in New Issue
Block a user