Избавление от undefined, всем необязательным полям присваиваются дефолтные значения #22
This commit is contained in:
+153
-156
@@ -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
@@ -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
@@ -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
@@ -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() ->
|
||||
[].
|
||||
Reference in New Issue
Block a user