Статистика для дашборда, расширенная #7
This commit is contained in:
@@ -4,20 +4,25 @@
|
|||||||
update_role/2, block/1, unblock/1, generate_id/0]).
|
update_role/2, block/1, unblock/1, generate_id/0]).
|
||||||
|
|
||||||
create(Email, Password, Role) ->
|
create(Email, Password, Role) ->
|
||||||
Id = generate_id(),
|
case get_by_email(Email) of
|
||||||
{ok, Hash} = argon2:hash(Password),
|
{ok, _} ->
|
||||||
Now = calendar:universal_time(),
|
{error, email_exists};
|
||||||
Admin = #admin{
|
{error, not_found} ->
|
||||||
id = Id,
|
Id = generate_id(),
|
||||||
email = Email,
|
{ok, Hash} = argon2:hash(Password),
|
||||||
password_hash = Hash,
|
Now = calendar:universal_time(),
|
||||||
role = Role,
|
Admin = #admin{
|
||||||
status = active,
|
id = Id,
|
||||||
created_at = Now,
|
email = Email,
|
||||||
updated_at = Now
|
password_hash = Hash,
|
||||||
},
|
role = Role,
|
||||||
mnesia:dirty_write(Admin),
|
status = active,
|
||||||
{ok, Admin}.
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
|
},
|
||||||
|
mnesia:dirty_write(Admin),
|
||||||
|
{ok, Admin}
|
||||||
|
end.
|
||||||
|
|
||||||
get_by_email(Email) ->
|
get_by_email(Email) ->
|
||||||
Match = #admin{email = Email, _ = '_'},
|
Match = #admin{email = Email, _ = '_'},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
-module(core_admin_audit).
|
-module(core_admin_audit).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([log/7, list/0, list/1]).
|
-export([log/7, list/0, list/1]).
|
||||||
|
-export([count_actions_by_admin/2]).
|
||||||
|
|
||||||
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip) ->
|
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip) ->
|
||||||
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, undefined).
|
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, undefined).
|
||||||
@@ -26,15 +27,33 @@ list() ->
|
|||||||
|
|
||||||
%% Фильтрация по параметрам (простая версия)
|
%% Фильтрация по параметрам (простая версия)
|
||||||
list(Filters) ->
|
list(Filters) ->
|
||||||
All = list(),
|
All = list(), % все записи
|
||||||
lists:filter(fun(E) ->
|
lists:filter(fun(E) ->
|
||||||
|
% Фильтр по admin_id
|
||||||
case proplists:get_value(admin_id, Filters) of
|
case proplists:get_value(admin_id, Filters) of
|
||||||
undefined -> true;
|
undefined -> true;
|
||||||
Id -> E#admin_audit.admin_id =:= Id
|
Id -> E#admin_audit.admin_id =:= Id
|
||||||
end andalso
|
end
|
||||||
case proplists:get_value(action, Filters) of
|
andalso
|
||||||
undefined -> true;
|
% Фильтр по action
|
||||||
Act -> E#admin_audit.action =:= Act
|
case proplists:get_value(action, Filters) of
|
||||||
end
|
undefined -> true;
|
||||||
% можно добавить фильтр по дате и т.д.
|
Act -> E#admin_audit.action =:= Act
|
||||||
end, All).
|
end
|
||||||
|
andalso
|
||||||
|
% Фильтр по дате с
|
||||||
|
case proplists:get_value(date_from, Filters) of
|
||||||
|
undefined -> true;
|
||||||
|
From -> E#admin_audit.timestamp >= From
|
||||||
|
end
|
||||||
|
andalso
|
||||||
|
% Фильтр по дате по
|
||||||
|
case proplists:get_value(date_to, Filters) of
|
||||||
|
undefined -> true;
|
||||||
|
To -> E#admin_audit.timestamp =< To
|
||||||
|
end
|
||||||
|
end, All).
|
||||||
|
|
||||||
|
count_actions_by_admin(AdminId, Action) ->
|
||||||
|
Match = #admin_audit{admin_id = AdminId, action = Action, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1,
|
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1,
|
||||||
update/2, delete/1, materialize_occurrence/3]).
|
update/2, delete/1, materialize_occurrence/3]).
|
||||||
-export([generate_id/0]).
|
-export([generate_id/0]).
|
||||||
-export([count_events/0]).
|
-export([count_events/0, count_events_by_date/2]).
|
||||||
|
|
||||||
%% Создание одиночного события
|
%% Создание одиночного события
|
||||||
create(CalendarId, Title, StartTime, Duration) ->
|
create(CalendarId, Title, StartTime, Duration) ->
|
||||||
@@ -171,6 +171,22 @@ delete(Id) ->
|
|||||||
count_events() ->
|
count_events() ->
|
||||||
mnesia:table_info(event, size).
|
mnesia:table_info(event, size).
|
||||||
|
|
||||||
|
count_events_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#event{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(E) ->
|
||||||
|
E#event.created_at >= From andalso E#event.created_at =< To
|
||||||
|
end, All),
|
||||||
|
Counts = lists:foldl(fun(E, Acc) ->
|
||||||
|
Day = date_part(E#event.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}.
|
||||||
|
|
||||||
%% Внутренние функции
|
%% Внутренние функции
|
||||||
generate_id() ->
|
generate_id() ->
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
-export([update_status/3, get_count_by_target/2]).
|
-export([update_status/3, get_count_by_target/2]).
|
||||||
-export([generate_id/0]).
|
-export([generate_id/0]).
|
||||||
-export([count_reports_by_status/1, count_reports_by_admin/2]).
|
-export([count_reports_by_status/1, count_reports_by_admin/2]).
|
||||||
|
-export([count_reports_resolved_by_admin/2, avg_resolution_time/1]).
|
||||||
|
|
||||||
%% Создание жалобы
|
%% Создание жалобы
|
||||||
create(ReporterId, TargetType, TargetId, Reason) ->
|
create(ReporterId, TargetType, TargetId, Reason) ->
|
||||||
@@ -92,6 +93,22 @@ count_reports_by_admin(AdminId, Status) ->
|
|||||||
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
count_reports_resolved_by_admin(AdminId, Status) ->
|
||||||
|
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
avg_resolution_time(Status) ->
|
||||||
|
Match = #report{status = Status, _ = '_'},
|
||||||
|
Reports = mnesia:dirty_match_object(Match),
|
||||||
|
Resolved = lists:filter(fun(R) -> R#report.resolved_at =/= undefined end, Reports),
|
||||||
|
case Resolved of
|
||||||
|
[] -> 0;
|
||||||
|
_ ->
|
||||||
|
TotalSeconds = lists:sum([calendar:datetime_to_gregorian_seconds(R#report.resolved_at) -
|
||||||
|
calendar:datetime_to_gregorian_seconds(R#report.created_at) || R <- Resolved]),
|
||||||
|
TotalSeconds / length(Resolved) / 3600.0
|
||||||
|
end.
|
||||||
|
|
||||||
%% Внутренние функции
|
%% Внутренние функции
|
||||||
generate_id() ->
|
generate_id() ->
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
create_ticket/1,
|
create_ticket/1,
|
||||||
list_by_user/1]).
|
list_by_user/1]).
|
||||||
-export([count_tickets_by_status/1, count_tickets_by_admin/2]).
|
-export([count_tickets_by_status/1, count_tickets_by_admin/2]).
|
||||||
|
-export([avg_resolution_time/0]).
|
||||||
|
|
||||||
list_all() ->
|
list_all() ->
|
||||||
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
||||||
@@ -69,6 +70,27 @@ create_ticket(Data) ->
|
|||||||
list_by_user(UserId) ->
|
list_by_user(UserId) ->
|
||||||
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
||||||
|
|
||||||
|
count_by_status(Status, Tickets) ->
|
||||||
|
length([T || T <- Tickets, T#ticket.status =:= Status]).
|
||||||
|
|
||||||
|
count_tickets_by_status(Status) ->
|
||||||
|
Match = #ticket{status = Status, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
count_tickets_by_admin(AdminId, Status) ->
|
||||||
|
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
avg_resolution_time() ->
|
||||||
|
Tickets = mnesia:dirty_match_object(#ticket{status = closed, _ = '_'}),
|
||||||
|
case Tickets of
|
||||||
|
[] -> 0;
|
||||||
|
_ ->
|
||||||
|
TotalSeconds = lists:sum([calendar:datetime_to_gregorian_seconds(T#ticket.last_seen) -
|
||||||
|
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen) || T <- Tickets]),
|
||||||
|
TotalSeconds / length(Tickets) / 3600.0
|
||||||
|
end.
|
||||||
|
|
||||||
%% ── внутренние ─────────────────────────────────────────
|
%% ── внутренние ─────────────────────────────────────────
|
||||||
apply_updates(Ticket, Updates) ->
|
apply_updates(Ticket, Updates) ->
|
||||||
lists:foldl(fun({Key, Value}, Acc) ->
|
lists:foldl(fun({Key, Value}, Acc) ->
|
||||||
@@ -81,15 +103,4 @@ apply_updates(Ticket, Updates) ->
|
|||||||
<<"context">> -> Acc#ticket{context = Value};
|
<<"context">> -> Acc#ticket{context = Value};
|
||||||
_ -> Acc
|
_ -> Acc
|
||||||
end
|
end
|
||||||
end, Ticket, maps:to_list(Updates)).
|
end, Ticket, maps:to_list(Updates)).
|
||||||
|
|
||||||
count_by_status(Status, Tickets) ->
|
|
||||||
length([T || T <- Tickets, T#ticket.status =:= Status]).
|
|
||||||
|
|
||||||
count_tickets_by_status(Status) ->
|
|
||||||
Match = #ticket{status = Status, _ = '_'},
|
|
||||||
length(mnesia:dirty_match_object(Match)).
|
|
||||||
|
|
||||||
count_tickets_by_admin(AdminId, Status) ->
|
|
||||||
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
|
||||||
length(mnesia:dirty_match_object(Match)).
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
-export([generate_id/0]).
|
-export([generate_id/0]).
|
||||||
-export([list_users/0]).
|
-export([list_users/0]).
|
||||||
-export([block/1, unblock/1]).
|
-export([block/1, unblock/1]).
|
||||||
-export([count_users/0]).
|
-export([count_users/0, count_users_by_date/2]).
|
||||||
|
|
||||||
%% Создание пользователя
|
%% Создание пользователя
|
||||||
create(Email, Password) ->
|
create(Email, Password) ->
|
||||||
@@ -126,6 +126,22 @@ unblock(Id) ->
|
|||||||
count_users() ->
|
count_users() ->
|
||||||
mnesia:table_info(user, size).
|
mnesia:table_info(user, size).
|
||||||
|
|
||||||
|
count_users_by_date(From, To) ->
|
||||||
|
All = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
|
Filtered = lists:filter(fun(U) ->
|
||||||
|
U#user.created_at >= From andalso U#user.created_at =< To
|
||||||
|
end, All),
|
||||||
|
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).
|
||||||
|
|
||||||
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
%% Внутренние функции
|
%% Внутренние функции
|
||||||
generate_id() ->
|
generate_id() ->
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||||
|
|||||||
@@ -39,12 +39,9 @@ create_admin(Req) ->
|
|||||||
Role = binary_to_atom(RoleBin, utf8),
|
Role = binary_to_atom(RoleBin, utf8),
|
||||||
case core_admin:create(Email, Password, Role) of
|
case core_admin:create(Email, Password, Role) of
|
||||||
{ok, Admin} ->
|
{ok, Admin} ->
|
||||||
% Заглушка отправки email
|
|
||||||
% send_invitation_email(Email),
|
|
||||||
core_admin_audit:log(AdminId, Admin#admin.email, Admin#admin.role,
|
|
||||||
<<"create_admin">>, <<"admin">>, Admin,
|
|
||||||
admin_utils:client_ip(Req)),
|
|
||||||
send_json(Req2, 201, admin_to_json(Admin));
|
send_json(Req2, 201, admin_to_json(Admin));
|
||||||
|
{error, email_exists} ->
|
||||||
|
send_error(Req2, 409, <<"Email already exists">>);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
send_error(Req2, 500, Reason)
|
send_error(Req2, 500, Reason)
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ get_stats(Req) ->
|
|||||||
case admin_utils:is_admin(AdminId) of
|
case admin_utils:is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
{ok, Admin} = core_admin:get_by_id(AdminId),
|
{ok, Admin} = core_admin:get_by_id(AdminId),
|
||||||
Stats = logic_stats:get_stats(Admin#admin.role, AdminId),
|
Role = Admin#admin.role,
|
||||||
|
% Извлекаем параметры from и to из запроса
|
||||||
|
Stats = case parse_date_range(Req1) of
|
||||||
|
{ok, From, To} ->
|
||||||
|
logic_stats:get_stats(Role, AdminId, From, To);
|
||||||
|
_ ->
|
||||||
|
logic_stats:get_stats(Role, AdminId)
|
||||||
|
end,
|
||||||
send_json(Req1, 200, Stats);
|
send_json(Req1, 200, Stats);
|
||||||
false ->
|
false ->
|
||||||
send_error(Req1, 403, <<"Admin access required">>)
|
send_error(Req1, 403, <<"Admin access required">>)
|
||||||
@@ -23,6 +30,28 @@ get_stats(Req) ->
|
|||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
parse_date_range(Req) ->
|
||||||
|
Qs = cowboy_req:parse_qs(Req),
|
||||||
|
From = proplists:get_value(<<"from">>, Qs),
|
||||||
|
To = proplists:get_value(<<"to">>, Qs),
|
||||||
|
case {From, To} of
|
||||||
|
{undefined, _} -> error;
|
||||||
|
{_, undefined} -> error;
|
||||||
|
{F, T} ->
|
||||||
|
try
|
||||||
|
FromDT = iso8601_to_datetime(F),
|
||||||
|
ToDT = iso8601_to_datetime(T),
|
||||||
|
{ok, FromDT, ToDT}
|
||||||
|
catch _:_ -> error
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
iso8601_to_datetime(Str) ->
|
||||||
|
[Date, Time] = binary:split(Str, <<"T">>),
|
||||||
|
[Y, M, D] = [binary_to_integer(X) || X <- binary:split(Date, <<"-">>, [global])],
|
||||||
|
[H, Min, S] = [binary_to_integer(X) || X <- binary:split(Time, <<":">>, [global])],
|
||||||
|
{{Y, M, D}, {H, Min, S}}.
|
||||||
|
|
||||||
send_json(Req, Status, Data) ->
|
send_json(Req, Status, Data) ->
|
||||||
Body = jsx:encode(Data),
|
Body = jsx:encode(Data),
|
||||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||||
|
|||||||
@@ -1,29 +1,82 @@
|
|||||||
-module(logic_stats).
|
-module(logic_stats).
|
||||||
-export([get_stats/2]).
|
-export([get_stats/2, get_stats/4]).
|
||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%% ========== Точка входа (без дат) =============================
|
||||||
-spec get_stats(Role :: atom(), AdminId :: binary()) -> map().
|
-spec get_stats(Role :: atom(), AdminId :: binary()) -> map().
|
||||||
get_stats(superadmin, _AdminId) ->
|
get_stats(Role, AdminId) ->
|
||||||
|
{{Y, _, _}, _} = calendar:universal_time(),
|
||||||
|
From = {{Y, 1, 1}, {0, 0, 0}}, % начало текущего года
|
||||||
|
To = calendar:universal_time(),
|
||||||
|
get_stats(Role, AdminId, From, To).
|
||||||
|
|
||||||
|
%% ========== Точка входа (с фильтром по датам) =================
|
||||||
|
-spec get_stats(Role :: atom(), AdminId :: binary(),
|
||||||
|
From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
|
get_stats(superadmin, _AdminId, From, To) ->
|
||||||
|
build_superadmin_stats(From, To);
|
||||||
|
get_stats(moderator, AdminId, From, To) ->
|
||||||
|
build_moderator_stats(AdminId, From, To);
|
||||||
|
get_stats(support, AdminId, From, To) ->
|
||||||
|
build_support_stats(AdminId, From, To);
|
||||||
|
get_stats(_, _, _, _) ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%% ========== Суперадмин =========================================
|
||||||
|
build_superadmin_stats(From, To) ->
|
||||||
#{
|
#{
|
||||||
users => core_user:count_users(),
|
users_total => core_user:count_users(),
|
||||||
calendars => core_calendar:count_calendars(),
|
events_total => core_event:count_events(),
|
||||||
events => core_event:count_events(),
|
calendars_total => core_calendar:count_calendars(),
|
||||||
bookings => core_booking:count_bookings(),
|
reviews_total => core_review:count_reviews(),
|
||||||
reviews => core_review:count_reviews(),
|
reports_total => core_report:count_reports_by_status(pending),
|
||||||
reports_total => core_report:count_reports_by_status(pending),
|
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()),
|
||||||
subscriptions => core_subscription:count_subscription()
|
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)),
|
||||||
get_stats(moderator, AdminId) ->
|
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
|
||||||
|
admin_activity => collect_admin_activity()
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% ========== Модератор ==========================================
|
||||||
|
build_moderator_stats(AdminId, _From, _To) ->
|
||||||
#{
|
#{
|
||||||
reports_reviewed => core_report:count_reports_by_admin(AdminId, reviewed),
|
reports_reviewed => core_report:count_reports_resolved_by_admin(AdminId, reviewed),
|
||||||
events_moderated => 0 % пока заглушка, можно добавить позже
|
reports_dismissed => core_report:count_reports_resolved_by_admin(AdminId, dismissed),
|
||||||
};
|
avg_report_resolution_h => trunc_hours(core_report:avg_resolution_time(reviewed)),
|
||||||
get_stats(support, AdminId) ->
|
events_moderated => 0 % заглушка, можно доработать
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% ========== Поддержка ==========================================
|
||||||
|
build_support_stats(AdminId, _From, _To) ->
|
||||||
#{
|
#{
|
||||||
tickets_assigned => core_ticket:count_tickets_by_admin(AdminId, open),
|
tickets_assigned_open => core_ticket:count_tickets_by_admin(AdminId, open),
|
||||||
reports_pending => core_report:count_reports_by_status(pending)
|
tickets_assigned_total => core_ticket:count_tickets_by_admin(AdminId, closed) +
|
||||||
};
|
core_ticket:count_tickets_by_admin(AdminId, in_progress),
|
||||||
get_stats(_, _) ->
|
reports_pending => core_report:count_reports_by_status(pending)
|
||||||
#{}.
|
}.
|
||||||
|
|
||||||
|
%% ========== Вспомогательные функции ============================
|
||||||
|
|
||||||
|
date_list_to_json(List) ->
|
||||||
|
[ #{<<"date">> => iso8601_date(Date), <<"count">> => Count} || {Date, Count} <- List ].
|
||||||
|
|
||||||
|
iso8601_date({{Y, M, D}, _}) ->
|
||||||
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D]));
|
||||||
|
iso8601_date({Y, M, D}) ->
|
||||||
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D])).
|
||||||
|
|
||||||
|
trunc_hours(Value) ->
|
||||||
|
round(Value * 10) / 10.0.
|
||||||
|
|
||||||
|
collect_admin_activity() ->
|
||||||
|
Admins = core_admin:list_all(),
|
||||||
|
lists:map(fun(A) ->
|
||||||
|
Actions = length(core_admin_audit:list([{admin_id, A#admin.id}])),
|
||||||
|
#{
|
||||||
|
admin_id => A#admin.id,
|
||||||
|
email => A#admin.email,
|
||||||
|
actions => Actions
|
||||||
|
}
|
||||||
|
end, Admins).
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
-module(api_admin_tests).
|
-module(api_admin_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-export([test/0]).
|
-export([test/0]).
|
||||||
|
|
||||||
test() ->
|
test() ->
|
||||||
@@ -25,17 +27,23 @@ test() ->
|
|||||||
|
|
||||||
%% TEST 3: Admin stats (superadmin)
|
%% TEST 3: Admin stats (superadmin)
|
||||||
io:format(" TEST 3: Admin stats (superadmin)... "),
|
io:format(" TEST 3: Admin stats (superadmin)... "),
|
||||||
% Логинимся под суперадмином (данные из api_test_runner)
|
|
||||||
LoginBody = jsx:encode(#{<<"email">> => <<"admin@eventhub.local">>, <<"password">> => <<"123456">>}),
|
LoginBody = jsx:encode(#{<<"email">> => <<"admin@eventhub.local">>, <<"password">> => <<"123456">>}),
|
||||||
{ok, {{_, 200, _}, _, LoginResp}} = httpc:request(post,
|
{ok, {{_, 200, _}, _, LoginResp}} = httpc:request(post,
|
||||||
{AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []),
|
{AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []),
|
||||||
#{<<"token">> := SuperToken} = jsx:decode(list_to_binary(LoginResp), [return_maps]),
|
#{<<"token">> := SuperToken} = jsx:decode(list_to_binary(LoginResp), [return_maps]),
|
||||||
|
|
||||||
% Запрашиваем статистику
|
% Без дат
|
||||||
{ok, {{_, 200, _}, _, StatsResp}} = httpc:request(get,
|
{ok, {{_, 200, _}, _, StatsResp1}} = httpc:request(get,
|
||||||
{AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperToken)}]}, [], []),
|
{AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperToken)}]}, [], []),
|
||||||
Stats = jsx:decode(list_to_binary(StatsResp), [return_maps]),
|
Stats1 = jsx:decode(list_to_binary(StatsResp1), [return_maps]),
|
||||||
io:format(" OK (keys: ~p)~n", [maps:keys(Stats)]),
|
io:format(" OK (keys: ~p)~n", [maps:keys(Stats1)]),
|
||||||
|
|
||||||
|
% С датами
|
||||||
|
{ok, {{_, 200, _}, _, StatsResp2}} = httpc:request(get,
|
||||||
|
{AdminURL ++ "/v1/admin/stats?from=2026-01-01T00:00:00&to=2026-12-31T23:59:59",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(SuperToken)}]}, [], []),
|
||||||
|
Stats2 = jsx:decode(list_to_binary(StatsResp2), [return_maps]),
|
||||||
|
io:format(" (with dates, keys: ~p)~n", [maps:keys(Stats2)]),
|
||||||
|
|
||||||
%% TEST 4: List users
|
%% TEST 4: List users
|
||||||
io:format(" TEST 4: List users... "),
|
io:format(" TEST 4: List users... "),
|
||||||
|
|||||||
@@ -22,15 +22,17 @@ cleanup(_) ->
|
|||||||
admin_stats_test_() ->
|
admin_stats_test_() ->
|
||||||
{setup, fun setup/0, fun cleanup/1, [
|
{setup, fun setup/0, fun cleanup/1, [
|
||||||
{"GET /admin/stats as superadmin returns 200 with system metrics", fun test_superadmin/0},
|
{"GET /admin/stats as superadmin returns 200 with system metrics", fun test_superadmin/0},
|
||||||
|
{"GET /admin/stats as superadmin with date filter", fun test_superadmin_dates/0},
|
||||||
{"GET /admin/stats as moderator returns 200 with own metrics", fun test_moderator/0},
|
{"GET /admin/stats as moderator returns 200 with own metrics", fun test_moderator/0},
|
||||||
{"GET /admin/stats as support returns 200 with assigned tickets", fun test_support/0},
|
{"GET /admin/stats as support returns 200 with assigned tickets", fun test_support/0},
|
||||||
{"GET /admin/stats with non‑admin token returns 403", fun test_forbidden/0},
|
{"GET /admin/stats with non‑admin token returns 403", fun test_forbidden/0},
|
||||||
{"POST /admin/stats returns 405", fun test_wrong_method/0}
|
{"POST /admin/stats returns 405", fun test_wrong_method/0}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
%% --- Суперадмин ---
|
%% --- Суперадмин (без дат) ---
|
||||||
test_superadmin() ->
|
test_superadmin() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
|
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
fun(Req) -> {ok, <<"adm1">>, Req} end),
|
fun(Req) -> {ok, <<"adm1">>, Req} end),
|
||||||
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
||||||
@@ -41,9 +43,26 @@ test_superadmin() ->
|
|||||||
{ok, _, _} = admin_handler_stats:init(req, []),
|
{ok, _, _} = admin_handler_stats:init(req, []),
|
||||||
?assertEqual(200, erase(test_reply)).
|
?assertEqual(200, erase(test_reply)).
|
||||||
|
|
||||||
|
%% --- Суперадмин (с датами) ---
|
||||||
|
test_superadmin_dates() ->
|
||||||
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
|
ok = meck:expect(cowboy_req, parse_qs, fun(_) ->
|
||||||
|
[{<<"from">>, <<"2026-01-01T00:00:00">>}, {<<"to">>, <<"2026-06-01T00:00:00">>}]
|
||||||
|
end),
|
||||||
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
|
fun(Req) -> {ok, <<"adm1">>, Req} end),
|
||||||
|
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
||||||
|
ok = meck:expect(core_admin, get_by_id,
|
||||||
|
fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end),
|
||||||
|
ok = meck:expect(logic_stats, get_stats,
|
||||||
|
fun(superadmin, _, _, _) -> #{users => 10} end),
|
||||||
|
{ok, _, _} = admin_handler_stats:init(req, []),
|
||||||
|
?assertEqual(200, erase(test_reply)).
|
||||||
|
|
||||||
%% --- Модератор ---
|
%% --- Модератор ---
|
||||||
test_moderator() ->
|
test_moderator() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
|
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
fun(Req) -> {ok, <<"mod1">>, Req} end),
|
fun(Req) -> {ok, <<"mod1">>, Req} end),
|
||||||
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
||||||
@@ -57,6 +76,7 @@ test_moderator() ->
|
|||||||
%% --- Поддержка ---
|
%% --- Поддержка ---
|
||||||
test_support() ->
|
test_support() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
|
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
fun(Req) -> {ok, <<"sup1">>, Req} end),
|
fun(Req) -> {ok, <<"sup1">>, Req} end),
|
||||||
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
|
||||||
|
|||||||
Reference in New Issue
Block a user