Избавление от undefined, всем необязательным полям присваиваются дефолтные значения #22

This commit is contained in:
2026-05-27 16:18:33 +03:00
parent ae6fe21ce6
commit b78df927dd
21 changed files with 2037 additions and 1088 deletions
+153 -156
View File
@@ -1,191 +1,188 @@
-module(logic_booking).
-include("records.hrl").
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
cancel_booking/2, cancel_booking/3, get_booking/2,
list_bookings/2, list_user_bookings/1, delete_booking/2,
list_bookings_admin/0, get_booking_admin/1, list_event_bookings/1]).
-export([create_booking/2, confirm_booking/3, cancel_booking/2]).
-export([get_booking/2, list_event_bookings/2, list_user_bookings/1]).
-export([auto_confirm/1, check_timeout_confirmations/0]).
%% Создание бронирования (запись на событие)
%%%-------------------------------------------------------------------
%%% @doc Создание бронирования со статусом `pending`.
%%% @end
%%%-------------------------------------------------------------------
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
{ok, #booking{}} | {error, full | already_booked | not_found}.
create_booking(UserId, EventId) ->
% Получаем событие напрямую, без проверки доступа к календарю
case core_event:get_by_id(EventId) of
{ok, Event} ->
% Проверяем, что событие активно
case Event#event.status of
active ->
% Проверяем календарь для политики подтверждения
case core_calendar:get_by_id(Event#event.calendar_id) of
{ok, Calendar} ->
case Calendar#calendar.status of
active ->
% Проверяем, что есть места
case check_capacity(EventId, Event#event.capacity) of
{ok, _} ->
% Проверяем, не записан ли уже пользователь
case core_booking:get_by_event_and_user(EventId, UserId) of
{error, not_found} ->
ActualEventId = get_actual_event_id(Event, UserId),
case core_booking:create(ActualEventId, UserId) of
{ok, Booking} ->
handle_confirmation_policy(Booking, Event, Calendar),
logic_notification:notify_booking(UserId, Booking), % ← Уведомление
{ok, Booking};
Error ->
Error
end;
{ok, _} ->
{error, already_booked}
end;
{error, full} ->
{error, event_full}
end;
_ ->
{error, calendar_not_active}
end;
_ ->
{error, calendar_not_found}
case check_capacity(EventId, Event#event.capacity) of
{ok, _} ->
case core_booking:get_by_event_and_user(EventId, UserId) of
{error, not_found} ->
core_booking:create(EventId, UserId, pending);
{ok, _} ->
{error, already_booked}
end;
_ ->
{error, event_not_active}
{error, full} ->
{error, full}
end;
Error ->
Error
{error, not_found} ->
{error, not_found}
end.
%% Подтверждение бронирования (владельцем календаря)
confirm_booking(UserId, BookingId, Action) when Action =:= confirm; Action =:= decline ->
%%%-------------------------------------------------------------------
%%% @doc Подтверждение бронирования (двухарная версия).
%%% @end
%%%-------------------------------------------------------------------
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
confirm_booking(BookingId, _UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
% Проверяем права на событие
case logic_event:get_event(UserId, Booking#booking.event_id) of
{ok, Event} ->
% Проверяем, что пользователь может редактировать календарь
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
NewStatus = case Action of
confirm -> confirmed;
decline -> cancelled
end,
case core_booking:update_status(BookingId, NewStatus) of
{ok, Updated} ->
logic_notification:notify_booking(Updated#booking.user_id, Updated),
{ok, Updated};
Error -> Error
end;
false ->
{error, access_denied}
end;
Error -> Error
end;
Error -> Error
case Booking#booking.status of
pending ->
Now = calendar:universal_time(),
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
_ ->
{error, access_denied}
end;
Error -> Error
end.
%% Отмена бронирования (участником)
cancel_booking(UserId, BookingId) ->
%%%-------------------------------------------------------------------
%%% @doc Подтверждение бронирования (трёхарная версия для обработчиков).
%%% @end
%%%-------------------------------------------------------------------
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm) ->
{ok, #booking{}} | {error, not_found | access_denied}.
confirm_booking(UserId, BookingId, confirm) ->
confirm_booking(BookingId, UserId).
%%%-------------------------------------------------------------------
%%% @doc Отмена бронирования (двухарная версия).
%%% @end
%%%-------------------------------------------------------------------
-spec cancel_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
cancel_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case Booking#booking.status of
cancelled ->
{ok, Booking};
_ ->
case Booking#booking.user_id =:= UserId of
true -> core_booking:update(BookingId, [{status, cancelled}]);
false -> {error, access_denied}
end
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Отмена бронирования (трёхарная версия для обработчиков).
%%% @end
%%%-------------------------------------------------------------------
-spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) ->
{ok, #booking{}} | {error, not_found | access_denied}.
cancel_booking(UserId, BookingId, cancel) ->
cancel_booking(BookingId, UserId).
%%%-------------------------------------------------------------------
%%% @doc Получение бронирования по ID.
%%% @end
%%%-------------------------------------------------------------------
-spec get_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
get_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
% Проверяем, что это бронирование пользователя
case Booking#booking.user_id =:= UserId of
true ->
core_booking:update_status(BookingId, cancelled);
false ->
{error, access_denied}
true -> {ok, Booking};
false -> {error, access_denied}
end;
Error ->
Error
Error -> Error
end.
%% Получение бронирования
get_booking(UserId, BookingId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
% Проверяем доступ к событию
case logic_event:get_event(UserId, Booking#booking.event_id) of
{ok, _} -> {ok, Booking};
Error -> Error
end;
Error ->
Error
end.
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события.
%%% @end
%%%-------------------------------------------------------------------
-spec list_bookings(EventId :: binary(), UserId :: binary()) ->
{ok, [#booking{}]}.
list_bookings(EventId, UserId) ->
{ok, Bookings} = core_booking:list_by_event(EventId),
Filtered = case admin_utils:is_admin(UserId) of
true -> Bookings;
false -> [B || B <- Bookings, B#booking.user_id =:= UserId]
end,
{ok, Filtered}.
%% Список бронирований события (для владельца)
list_event_bookings(UserId, EventId) ->
case logic_event:get_event(UserId, EventId) of
{ok, Event} ->
% Проверяем права на календарь
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
core_booking:list_by_event(EventId);
false ->
{error, access_denied}
end;
Error ->
Error
end;
Error ->
Error
end.
%% Список бронирований пользователя
%%%-------------------------------------------------------------------
%%% @doc Список бронирований пользователя.
%%% @end
%%%-------------------------------------------------------------------
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
list_user_bookings(UserId) ->
core_booking:list_by_user(UserId).
%% Автоматическое подтверждение (для политики auto)
auto_confirm(BookingId) ->
core_booking:update_status(BookingId, confirmed).
%%%-------------------------------------------------------------------
%%% @doc Удаление бронирования (только владелец).
%%% @end
%%%-------------------------------------------------------------------
-spec delete_booking(BookingId :: binary(), UserId :: binary()) ->
ok | {error, not_found | access_denied}.
delete_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case Booking#booking.user_id =:= UserId of
true -> core_booking:delete(BookingId);
false -> {error, access_denied}
end;
Error -> Error
end.
%% Проверка истечения timeout подтверждений
check_timeout_confirmations() ->
% Получаем все pending бронирования для календарей с timeout
% В реальной реализации нужно периодически вызывать эту функцию
ok.
%%%-------------------------------------------------------------------
%%% @doc Административное получение бронирования.
%%% @end
%%%-------------------------------------------------------------------
-spec get_booking_admin(BookingId :: binary()) ->
{ok, #booking{}} | {error, not_found}.
get_booking_admin(BookingId) ->
core_booking:get_by_id(BookingId).
%% Внутренние функции
check_capacity(_EventId, undefined) ->
{ok, unlimited};
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события (административный).
%%% @end
%%%-------------------------------------------------------------------
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
list_event_bookings(EventId) ->
core_booking:list_by_event(EventId).
%%%-------------------------------------------------------------------
%%% @doc Список всех бронирований (административный).
%%% @end
%%%-------------------------------------------------------------------
-spec list_bookings_admin() -> {ok, [#booking{}]}.
list_bookings_admin() ->
{ok, core_booking:list_all()}.
%%%===================================================================
%%% ВНУТРЕННИЕ ФУНКЦИИ
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Проверка вместимости события.
%%% `undefined` и `0` означают неограниченную вместимость.
%%% @end
%%%-------------------------------------------------------------------
-spec check_capacity(EventId :: binary(), Capacity :: integer() | undefined) ->
{ok, integer() | unlimited} | {error, full}.
check_capacity(_EventId, undefined) -> {ok, unlimited};
check_capacity(_EventId, 0) -> {ok, unlimited};
check_capacity(EventId, Capacity) ->
{ok, Bookings} = core_booking:list_by_event(EventId),
ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]),
case ConfirmedCount < Capacity of
true -> {ok, Capacity - ConfirmedCount};
false -> {error, full}
end.
get_actual_event_id(Event, _UserId) ->
case Event#event.event_type of
recurring ->
% Для повторяющихся событий нужно материализовать вхождение
% Здесь предполагается, что start_time передаётся в запросе
% В полной реализации нужно получать occurrence_start из параметров
Event#event.id;
single ->
Event#event.id
end.
handle_confirmation_policy(Booking, _Event, Calendar) ->
io:format("Confirmation policy: ~p~n", [Calendar#calendar.confirmation]),
case Calendar#calendar.confirmation of
auto ->
io:format("Auto-confirming booking ~p~n", [Booking#booking.id]),
auto_confirm(Booking#booking.id);
manual ->
io:format("Manual confirmation, leaving pending~n"),
ok;
{timeout, Seconds} ->
io:format("Timeout confirmation: ~p seconds~n", [Seconds]),
spawn(fun() ->
timer:sleep(Seconds * 1000),
case core_booking:get_by_id(Booking#booking.id) of
{ok, B} when B#booking.status =:= pending ->
auto_confirm(Booking#booking.id);
_ ->
ok
end
end)
end.
+46 -36
View File
@@ -1,47 +1,57 @@
-module(logic_report).
-include("records.hrl").
-export([create_report/4, get_report/2, list_reports/1, update_report_status/3]).
-export([list_reports/1, get_report/2, update_report_status/3, delete_report/2]).
%%%-------------------------------------------------------------------
%%% @doc Создаёт жалобу.
%%% @end
%%%-------------------------------------------------------------------
-spec create_report(UserId :: binary(), TargetType :: calendar | event | review,
TargetId :: binary(), Reason :: binary()) ->
{ok, #report{}} | {error, term()}.
create_report(UserId, TargetType, TargetId, Reason) ->
core_report:create(UserId, TargetType, TargetId, Reason).
%% Получить список всех жалоб (только для админов)
-spec list_reports(binary()) -> {ok, [#report{}]} | {error, access_denied}.
%%%-------------------------------------------------------------------
%%% @doc Получить жалобу по ID. Только для админа или автора.
%%% @end
%%%-------------------------------------------------------------------
-spec get_report(UserId :: binary(), ReportId :: binary()) ->
{ok, #report{}} | {error, not_found | access_denied}.
get_report(UserId, ReportId) ->
case core_report:get_by_id(ReportId) of
{ok, Report} ->
case UserId =:= Report#report.reporter_id orelse admin_utils:is_admin(UserId) of
true -> {ok, Report};
false -> {error, access_denied}
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Список всех жалоб (только для админов).
%%% @end
%%%-------------------------------------------------------------------
-spec list_reports(AdminId :: binary()) -> {ok, [#report{}]} | {error, access_denied}.
list_reports(AdminId) ->
case admin_utils:is_admin(AdminId) of
true ->
{ok, Reports} = core_report:list_all(),
{ok, Reports};
false -> {error, access_denied}
true ->
Reports = core_report:list_all(), % возвращает список
{ok, Reports}; % упаковываем в кортеж
false ->
{error, access_denied}
end.
%% Получить конкретную жалобу по ID (только для админов)
-spec get_report(binary(), binary()) -> {ok, #report{}} | {error, not_found | access_denied}.
get_report(AdminId, ReportId) ->
case admin_utils:is_admin(AdminId) of
true -> core_report:get_by_id(ReportId);
false -> {error, access_denied}
end.
%% Обновить статус жалобы (только для админов)
-spec update_report_status(binary(), binary(), binary()) -> {ok, #report{}} | {error, not_found | access_denied | invalid_status}.
%%%-------------------------------------------------------------------
%%% @doc Обновить статус жалобы.
%%% @end
%%%-------------------------------------------------------------------
-spec update_report_status(AdminId :: binary(), ReportId :: binary(), NewStatus :: atom()) ->
{ok, #report{}} | {error, not_found | access_denied}.
update_report_status(AdminId, ReportId, NewStatus) ->
case admin_utils:is_admin(AdminId) of
true ->
StatusAtom = case NewStatus of
<<"reviewed">> -> reviewed;
<<"dismissed">> -> dismissed;
_ -> undefined
end,
case StatusAtom of
undefined -> {error, invalid_status};
_ -> core_report:update_status(ReportId, StatusAtom, AdminId)
end;
false -> {error, access_denied}
end.
%% Удалить жалобу (только для админов)
-spec delete_report(binary(), binary()) -> {ok, deleted} | {error, not_found | access_denied}.
delete_report(AdminId, ReportId) ->
case admin_utils:is_admin(AdminId) of
true -> core_report:delete(ReportId);
false -> {error, access_denied}
true ->
core_report:update_status(ReportId, NewStatus, AdminId);
false ->
{error, access_denied}
end.
+104 -44
View File
@@ -1,62 +1,103 @@
%%%-------------------------------------------------------------------
%%% @doc Модуль поиска событий и календарей.
%%%
%%% Предоставляет публичную функцию `search/4`, которая выполняет
%%% фильтрацию, сортировку и пагинацию доступных пользователю сущностей.
%%% Поддерживается полнотекстовый поиск, фильтры по тегам, датам и
%%% географическому положению.
%%% @end
%%%-------------------------------------------------------------------
-module(logic_search).
-include("records.hrl").
-export([search/4]).
%% ─────────────────────────────────────────────────────────────────
%% Константы
%% ─────────────────────────────────────────────────────────────────
-define(DEFAULT_LIMIT, 20).
-define(MAX_LIMIT, 100).
-define(EARTH_RADIUS_KM, 6371.0).
%% Поиск событий и календарей
%%%-------------------------------------------------------------------
%%% @doc Выполняет поиск по событиям и календарям.
%%%
%%% `Type` может принимать значения `<<"event">>`, `<<"calendar">>`
%%% или `undefined` (оба типа).
%%%
%%% Возвращает кортеж `{ok, TotalCount, ResultMap}`, где ResultMap
%%% содержит ключи `<<"events">>` и/или `<<"calendars">>`.
%%% @end
%%%-------------------------------------------------------------------
-spec search(Type :: binary() | undefined,
Query :: binary() | undefined,
UserId :: binary(),
Params :: map()) ->
{ok, non_neg_integer(), map()}.
search(Type, Query, UserId, Params) ->
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
Offset = maps:get(offset, Params, 0),
case Type of
<<"event">> ->
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"events">> => Events}};
<<"calendar">> ->
{ok, Total, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"calendars">> => Calendars}};
_ ->
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, EventsTotal + CalendarsTotal, #{
<<"events">> => Events,
<<"calendars">> => Calendars
}}
end.
<<"event">> ->
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"events">> => Events}};
<<"calendar">> ->
{ok, Total, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"calendars">> => Calendars}};
_ ->
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, EventsTotal + CalendarsTotal, #{
<<"events">> => Events,
<<"calendars">> => Calendars
}}
end.
%% ============ Поиск событий ============
-spec search_events(Query :: binary() | undefined,
UserId :: binary(),
Params :: map(),
Limit :: non_neg_integer(),
Offset :: non_neg_integer()) ->
{ok, non_neg_integer(), [map()]}.
search_events(Query, UserId, Params, Limit, Offset) ->
AllEvents = get_all_events(),
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
Sorted = sort_events(Filtered, Params),
Paginated = paginate(Sorted, Limit, Offset),
{ok, length(Filtered), format_events(Paginated)}.
%% ============ Поиск календарей ============
-spec search_calendars(Query :: binary() | undefined,
UserId :: binary(),
Params :: map(),
Limit :: non_neg_integer(),
Offset :: non_neg_integer()) ->
{ok, non_neg_integer(), [map()]}.
search_calendars(Query, UserId, Params, Limit, Offset) ->
AllCalendars = get_all_calendars(),
AccessibleCalendars = filter_accessible_calendars(AllCalendars, UserId),
Filtered = apply_calendar_filters(AccessibleCalendars, Query, Params),
Paginated = paginate(Filtered, Limit, Offset),
{ok, length(Filtered), format_calendars(Paginated)}.
%% ============ Получение данных ============
-spec get_all_events() -> [#event{}].
get_all_events() ->
Match = #event{status = active, is_instance = false, _ = '_'},
mnesia:dirty_match_object(Match).
-spec get_all_calendars() -> [#calendar{}].
get_all_calendars() ->
Match = #calendar{status = active, _ = '_'},
mnesia:dirty_match_object(Match).
%% ============ Фильтрация по доступности ============
-spec filter_accessible_events([#event{}], binary()) -> [#event{}].
filter_accessible_events(Events, UserId) ->
lists:filter(fun(Event) ->
case core_calendar:get_by_id(Event#event.calendar_id) of
@@ -74,33 +115,39 @@ filter_accessible_events(Events, UserId) ->
end
end, Events).
-spec filter_accessible_calendars([#calendar{}], binary()) -> [#calendar{}].
filter_accessible_calendars(Calendars, UserId) ->
lists:filter(fun(Calendar) ->
logic_calendar:can_access(UserId, Calendar)
end, Calendars).
lists:filter(fun(Calendar) -> logic_calendar:can_access(UserId, Calendar) end, Calendars).
%% ============ Применение фильтров ============
-spec apply_event_filters([#event{}], binary() | undefined, map()) -> [#event{}].
apply_event_filters(Events, Query, Params) ->
Events1 = filter_by_text(Events, Query),
Events2 = filter_by_tags(Events1, Params),
Events3 = filter_by_date_range(Events2, Params),
filter_by_location(Events3, Params).
-spec apply_calendar_filters([#calendar{}], binary() | undefined, map()) -> [#calendar{}].
apply_calendar_filters(Calendars, Query, Params) ->
Calendars1 = filter_by_text(Calendars, Query),
filter_by_tags(Calendars1, Params).
%% --- Текстовый поиск ---
-spec filter_by_text([#event{} | #calendar{}], binary() | undefined) -> [#event{} | #calendar{}].
filter_by_text(Items, undefined) -> Items;
filter_by_text(Items, <<>>) -> Items;
filter_by_text(Items, Query) ->
QueryLower = string:lowercase(Query),
QueryLower = string:lowercase(binary_to_list(Query)),
lists:filter(fun(Item) ->
Title = get_title(Item),
Description = get_description(Item),
Title = binary_to_list(get_title(Item)),
Description = binary_to_list(get_description(Item)),
string:find(string:lowercase(Title), QueryLower) =/= nomatch orelse
string:find(string:lowercase(Description), QueryLower) =/= nomatch
end, Items).
%% --- Фильтр по тегам ---
-spec filter_by_tags([#event{} | #calendar{}], map()) -> [#event{} | #calendar{}].
filter_by_tags(Items, Params) ->
case maps:get(tags, Params, undefined) of
undefined -> Items;
@@ -112,10 +159,11 @@ filter_by_tags(Items, Params) ->
end, Items)
end.
%% --- Фильтр по дате ---
-spec filter_by_date_range([#event{}], map()) -> [#event{}].
filter_by_date_range(Events, Params) ->
From = maps:get(from, Params, undefined),
To = maps:get(to, Params, undefined),
case {From, To} of
{undefined, undefined} -> Events;
_ ->
@@ -126,6 +174,8 @@ filter_by_date_range(Events, Params) ->
end, Events)
end.
%% --- Гео-фильтр ---
-spec filter_by_location([#event{}], map()) -> [#event{}].
filter_by_location(Events, Params) ->
case {maps:get(lat, Params, undefined), maps:get(lon, Params, undefined)} of
{undefined, _} -> Events;
@@ -134,76 +184,84 @@ filter_by_location(Events, Params) ->
Radius = maps:get(radius, Params, 10),
lists:filter(fun(Event) ->
case Event#event.location of
undefined -> false;
#location{lat = EventLat, lon = EventLon} ->
distance(Lat, Lon, EventLat, EventLon) =< Radius
#location{lat = EventLat, lon = EventLon}
when is_number(EventLat), is_number(EventLon) ->
distance(Lat, Lon, EventLat, EventLon) =< Radius;
_ -> false
end
end, Events)
end.
%% ============ Вспомогательные функции ============
-spec get_title(#event{} | #calendar{}) -> binary().
get_title(#event{title = Title}) -> Title;
get_title(#calendar{title = Title}) -> Title.
-spec get_description(#event{} | #calendar{}) -> binary().
get_description(#event{description = Desc}) -> Desc;
get_description(#calendar{description = Desc}) -> Desc.
-spec get_tags(#event{} | #calendar{}) -> [binary()].
get_tags(#event{tags = Tags}) -> Tags;
get_tags(#calendar{tags = Tags}) -> Tags.
-spec has_any_tag([binary()], [string()]) -> boolean().
has_any_tag(ItemTags, SearchTags) ->
lists:any(fun(Tag) -> lists:member(Tag, ItemTags) end, SearchTags).
%% ============ Гео-вычисления ============
%% @doc Вычисляет расстояние между двумя географическими точками по формуле Хаверсина.
-spec distance(number(), number(), number(), number()) -> float().
distance(Lat1, Lon1, Lat2, Lon2) ->
DLat = deg_to_rad(Lat2 - Lat1),
DLon = deg_to_rad(Lon2 - Lon1),
A = math:sin(DLat / 2) * math:sin(DLat / 2) +
math:cos(deg_to_rad(Lat1)) * math:cos(deg_to_rad(Lat2)) *
math:sin(DLon / 2) * math:sin(DLon / 2),
C = 2 * math:atan2(math:sqrt(A), math:sqrt(1 - A)),
?EARTH_RADIUS_KM * C.
-spec deg_to_rad(number()) -> float().
deg_to_rad(Deg) -> Deg * math:pi() / 180.
%% ============ Сортировка ============
-spec sort_events([#event{}], map()) -> [#event{}].
sort_events(Events, Params) ->
SortBy = maps:get(sort, Params, <<"start_time">>),
Order = maps:get(order, Params, <<"asc">>),
Sorted = case SortBy of
<<"start_time">> ->
lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
<<"rating">> ->
lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
<<"created_at">> ->
lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
<<"start_time">> -> lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
<<"rating">> -> lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
<<"created_at">> -> lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
_ -> Events
end,
case Order of
<<"desc">> -> lists:reverse(Sorted);
_ -> Sorted
end.
%% ============ Пагинация ============
-spec paginate([term()], non_neg_integer(), non_neg_integer()) -> [term()].
paginate(List, Limit, Offset) ->
lists:sublist(List, Offset + 1, Limit).
%% ============ Форматирование ============
%% ============ Форматирование ответа ============
-spec format_events([#event{}]) -> [map()].
format_events(Events) ->
lists:map(fun format_event/1, Events).
-spec format_event(#event{}) -> map().
format_event(Event) ->
Location = case Event#event.location of
undefined -> null;
#location{address = Addr, lat = Lat, lon = Lon} ->
#{address => Addr, lat => Lat, lon => Lon}
end,
#{
id => Event#event.id,
calendar_id => Event#event.calendar_id,
@@ -220,9 +278,11 @@ format_event(Event) ->
status => Event#event.status
}.
-spec format_calendars([#calendar{}]) -> [map()].
format_calendars(Calendars) ->
lists:map(fun format_calendar/1, Calendars).
-spec format_calendar(#calendar{}) -> map().
format_calendar(Calendar) ->
#{
id => Calendar#calendar.id,
@@ -236,6 +296,6 @@ format_calendar(Calendar) ->
status => Calendar#calendar.status
}.
-spec datetime_to_iso8601(calendar:datetime()) -> binary().
datetime_to_iso8601({{Y, M, D}, {H, Min, S}}) ->
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
[Y, M, D, H, Min, S])).
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Y, M, D, H, Min, S])).
+97 -60
View File
@@ -1,17 +1,29 @@
%%%-------------------------------------------------------------------
%%% @doc Модуль сбора статистики для административной панели.
%%%
%%% Предоставляет функции `get_stats/2` и `get_stats/4`, возвращающие
%%% агрегированные показатели в зависимости от роли администратора.
%%% @end
%%%-------------------------------------------------------------------
-module(logic_stats).
-export([get_stats/2, get_stats/4]).
-include("records.hrl").
%% ========== Точка входа (без дат) =============================
%%%-------------------------------------------------------------------
%%% @doc Возвращает статистику за текущий год.
%%% @end
%%%-------------------------------------------------------------------
-spec get_stats(Role :: atom(), AdminId :: binary()) -> map().
get_stats(Role, AdminId) ->
{{Y, _, _}, _} = calendar:universal_time(),
From = {{Y, 1, 1}, {0, 0, 0}}, % начало текущего года
To = calendar:universal_time(),
From = {{Y, 1, 1}, {0, 0, 0}},
To = calendar:universal_time(),
get_stats(Role, AdminId, From, To).
%% ========== Точка входа (с фильтром по датам) =================
%%%-------------------------------------------------------------------
%%% @doc Возвращает статистику за указанный период.
%%% @end
%%%-------------------------------------------------------------------
-spec get_stats(Role :: atom(), AdminId :: binary(),
From :: calendar:datetime(), To :: calendar:datetime()) -> map().
get_stats(superadmin, _AdminId, From, To) ->
@@ -25,78 +37,103 @@ get_stats(support, AdminId, From, To) ->
get_stats(_, _, _, _) ->
#{}.
%% ========== Суперадмин =========================================
%%%===================================================================
%%% Внутренние функции построения статистики
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Статистика для суперадмина.
%%% @end
%%%-------------------------------------------------------------------
-spec build_superadmin_stats(From :: calendar:datetime(), To :: calendar:datetime()) -> map().
build_superadmin_stats(From, To) ->
#{
users_total => core_user:count_users(),
events_total => core_event:count_events(),
calendars_total => core_calendar:count_calendars(),
reviews_total => core_review:count_reviews(),
reports_total => core_report:count_reports_by_status(pending),
tickets_open => core_ticket:count_tickets_by_status(open),
tickets_total => length(core_ticket:list_all()),
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)),
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
admin_activity => collect_admin_activity()
users_total => core_user:count_users(),
events_total => core_event:count_events(),
calendars_total => core_calendar:count_calendars(),
reviews_total => core_review:count_reviews(),
reports_total => core_report:count_reports_by_status(pending),
tickets_open => core_ticket:count_tickets_by_status(open),
tickets_total => length(core_ticket:list_all()),
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)),
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To)),
admin_activity => collect_admin_activity()
}.
%% ========== Админ =========================================
%%%-------------------------------------------------------------------
%%% @doc Статистика для администратора.
%%% @end
%%%-------------------------------------------------------------------
-spec build_admin_stats(From :: calendar:datetime(), To :: calendar:datetime()) -> map().
build_admin_stats(From, To) ->
#{
users_total => core_user:count_users(),
events_total => core_event:count_events(),
calendars_total => core_calendar:count_calendars(),
reviews_total => core_review:count_reviews(),
reports_total => core_report:count_reports_by_status(pending),
tickets_open => core_ticket:count_tickets_by_status(open),
tickets_total => length(core_ticket:list_all()),
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)),
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To))
users_total => core_user:count_users(),
events_total => core_event:count_events(),
calendars_total => core_calendar:count_calendars(),
reviews_total => core_review:count_reviews(),
reports_total => core_report:count_reports_by_status(pending),
tickets_open => core_ticket:count_tickets_by_status(open),
tickets_total => length(core_ticket:list_all()),
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)),
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To))
}.
%% ========== Модератор ==========================================
%%%-------------------------------------------------------------------
%%% @doc Статистика для модератора.
%%% @end
%%%-------------------------------------------------------------------
-spec build_moderator_stats(AdminId :: binary(), From :: calendar:datetime(), To :: calendar:datetime()) -> map().
build_moderator_stats(AdminId, _From, _To) ->
#{
reports_reviewed => core_report:count_reports_resolved_by_admin(AdminId, reviewed),
reports_dismissed => core_report:count_reports_resolved_by_admin(AdminId, dismissed),
avg_report_resolution_h => trunc_hours(core_report:avg_resolution_time(reviewed)),
events_moderated => 0 % заглушка, можно доработать
reports_reviewed => core_report:count_reports_resolved_by_admin(AdminId, reviewed),
reports_dismissed => core_report:count_reports_resolved_by_admin(AdminId, dismissed),
avg_report_resolution_h => trunc_hours(core_report:avg_resolution_time(reviewed)),
events_moderated => 0 % заглушка, можно доработать
}.
%% ========== Поддержка ==========================================
%%%-------------------------------------------------------------------
%%% @doc Статистика для поддержки.
%%% @end
%%%-------------------------------------------------------------------
-spec build_support_stats(AdminId :: binary(), From :: calendar:datetime(), To :: calendar:datetime()) -> map().
build_support_stats(AdminId, _From, _To) ->
#{
tickets_assigned_open => core_ticket:count_tickets_by_admin(AdminId, open),
tickets_assigned_total => core_ticket:count_tickets_by_admin(AdminId, closed) +
core_ticket:count_tickets_by_admin(AdminId, in_progress),
reports_pending => core_report:count_reports_by_status(pending)
tickets_assigned_open => core_ticket:count_tickets_by_admin(AdminId, open),
tickets_assigned_total => core_ticket:count_tickets_by_admin(AdminId, all),
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time())
}.
%% ========== Вспомогательные функции ============================
%%%===================================================================
%%% Вспомогательные функции
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Преобразует список дат в JSON-подобный формат.
%%% @end
%%%-------------------------------------------------------------------
-spec date_list_to_json([{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}]) -> [map()].
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}])),
lists:map(fun({{Y, M, D}, Count}) ->
#{
admin_id => A#admin.id,
nickname => A#admin.nickname,
email => A#admin.email,
role => A#admin.role,
last_login => A#admin.last_login,
actions => Actions
<<"date">> => iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D])),
<<"count">> => Count
}
end, Admins).
end, List).
%%%-------------------------------------------------------------------
%%% @doc Округляет часы до двух знаков после запятой.
%%% @end
%%%-------------------------------------------------------------------
-spec trunc_hours(float()) -> float().
trunc_hours(Hours) ->
round(Hours * 100) / 100.
%%%-------------------------------------------------------------------
%%% @doc Сбор активности администраторов (заглушка).
%%% @end
%%%-------------------------------------------------------------------
-spec collect_admin_activity() -> [map()].
collect_admin_activity() ->
[].