Расширена статистика для разделов #22

This commit is contained in:
2026-05-28 13:49:08 +03:00
parent 2dac5e5102
commit 1986fd4694
20 changed files with 1189 additions and 71 deletions
@@ -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
}.
+7 -4
View File
@@ -47,15 +47,18 @@ stats_schema() ->
type => object,
properties => #{
<<"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">>},
<<"reviews_total">> => #{type => integer, description => <<"Total number of reviews">>},
<<"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_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">>},
<<"trial_subscriptions">> => #{type => integer, description => <<"Number of trial subscriptions">>},
<<"avg_ticket_resolution_h">> => #{type => number, format => float, description => <<"Average ticket resolution time in hours">>},
<<"registrations_by_day">> => #{
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 Административный обработчик для получения статистики по тикетам.
%%% GET – возвращает агрегированную статистику тикетов
%%% (количество по статусам: open, in_progress, resolved, closed).
%%% @doc Административный обработчик статистики по тикетам.
%%% GET – возвращает агрегированные данные по тикетам.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_ticket_stats).
-behaviour(cowboy_handler).
-export([init/2]).
-export([trails/0]).
@@ -16,8 +14,8 @@
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> get_stats(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
<<"GET">> -> get_ticket_stats(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
@@ -25,38 +23,41 @@ init(Req, _Opts) ->
trails() ->
[
#{
path => <<"/v1/admin/tickets/stats">>,
method => <<"GET">>,
description => <<"Get ticket statistics (admin)">>,
tags => [<<"Tickets">>],
responses => #{
path => <<"/v1/admin/tickets/stats">>,
method => <<"GET">>,
description => <<"Get ticket statistics">>,
tags => [<<"Tickets">>],
responses => #{
200 => #{
description => <<"Ticket statistics">>,
content => #{<<"application/json">> => #{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">>}
content => #{<<"application/json">> => #{schema => ticket_stats_schema()}}
}
}
}
].
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
%% @doc Получить статистику тикетов. Доступно только администраторам.
-spec get_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_stats(Req) ->
%% @doc GET /v1/admin/tickets/stats получить статистику по тикетам.
-spec get_ticket_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_ticket_stats(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
Stats = core_ticket:stats(),
{ok, AdminId, Req1} ->
Stats = logic_ticket:get_statistics(AdminId),
handler_utils:send_json(Req1, 200, Stats);
{error, Code, Message, Req1} ->
handler_utils:send_error(Req1, Code, Message)
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
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.