Files
EventHubBack/src/core/core_event.erl
T

360 lines
16 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-module(core_event).
-include("records.hrl").
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1, update/2, delete/1,
materialize_occurrence/3]).
-export([count_events/0, count_events_by_date/2]).
-export([freeze/2, unfreeze/2]).
-export([list_all/0]).
-export([count_events_by_type/0, count_events_by_status/0, get_top_events_by_rating/1]).
%% ─────────────────────────────────────────────────────────────────
%% Значения по умолчанию для необязательных полей
%% ─────────────────────────────────────────────────────────────────
-define(DEFAULT_RECURRENCE_RULE, <<>>).
-define(DEFAULT_MASTER_ID, <<>>).
-define(DEFAULT_SPECIALIST_ID, <<>>).
-define(DEFAULT_LOCATION, #location{address = <<>>, lat = 0.0, lon = 0.0}).
-define(DEFAULT_TAGS, []).
-define(DEFAULT_CAPACITY, 0).
-define(DEFAULT_ONLINE_LINK, <<>>).
-define(DEFAULT_REASON, <<>>).
-define(DEFAULT_ATTACHMENTS, []).
-define(DEFAULT_EDIT_HISTORY, []).
%%%-------------------------------------------------------------------
%%% @doc Создание одиночного события.
%%% Все необязательные поля инициализируются значениями по умолчанию.
%%% @end
%%%-------------------------------------------------------------------
-spec create(CalendarId :: binary(), Title :: binary(),
StartTime :: calendar:datetime(), Duration :: non_neg_integer()) ->
{ok, #event{}} | {error, term()}.
create(CalendarId, Title, StartTime, Duration) ->
Id = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Event = #event{
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = single,
start_time = StartTime,
duration = Duration,
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
master_id = ?DEFAULT_MASTER_ID,
is_instance = false,
specialist_id = ?DEFAULT_SPECIALIST_ID,
location = ?DEFAULT_LOCATION,
tags = ?DEFAULT_TAGS,
capacity = ?DEFAULT_CAPACITY,
online_link = ?DEFAULT_ONLINE_LINK,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() -> mnesia:write(Event), {ok, Event} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%%%-------------------------------------------------------------------
%%% @doc Создание повторяющегося события (мастер-запись).
%%% @end
%%%-------------------------------------------------------------------
-spec create_recurring(CalendarId :: binary(), Title :: binary(),
StartTime :: calendar:datetime(), Duration :: non_neg_integer(),
RRule :: map()) ->
{ok, #event{}} | {error, term()}.
create_recurring(CalendarId, Title, StartTime, Duration, RRule) ->
Id = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Event = #event{
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = recurring,
start_time = StartTime,
duration = Duration,
recurrence_rule = jsx:encode(RRule),
master_id = ?DEFAULT_MASTER_ID,
is_instance = false,
specialist_id = ?DEFAULT_SPECIALIST_ID,
location = ?DEFAULT_LOCATION,
tags = ?DEFAULT_TAGS,
capacity = ?DEFAULT_CAPACITY,
online_link = ?DEFAULT_ONLINE_LINK,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() -> mnesia:write(Event), {ok, Event} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%%%-------------------------------------------------------------------
%%% @doc Материализация вхождения повторяющегося события.
%%% @end
%%%-------------------------------------------------------------------
-spec materialize_occurrence(MasterId :: binary(),
OccurrenceStart :: calendar:datetime(),
SpecialistId :: binary()) ->
{ok, #event{}} | {error, term()}.
materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
case mnesia:dirty_read(event, MasterId) of
[] ->
{error, master_not_found};
[Master] when Master#event.event_type =:= recurring ->
Existing = mnesia:dirty_match_object(
#event{master_id = MasterId, start_time = OccurrenceStart,
is_instance = true, _ = '_'}
),
case Existing of
[] ->
InstanceId = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Instance = #event{
id = InstanceId,
calendar_id = Master#event.calendar_id,
title = Master#event.title,
description = Master#event.description,
event_type = single,
start_time = OccurrenceStart,
duration = Master#event.duration,
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
master_id = MasterId,
is_instance = true,
specialist_id = SpecialistId,
location = Master#event.location,
tags = Master#event.tags,
capacity = Master#event.capacity,
online_link = Master#event.online_link,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() -> mnesia:write(Instance), {ok, Instance} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end;
[Instance] ->
{ok, Instance}
end;
[_] ->
{error, not_recurring}
end.
%%%-------------------------------------------------------------------
%%% @doc Получить событие по идентификатору.
%%% @end
%%%-------------------------------------------------------------------
-spec get_by_id(Id :: binary()) -> {ok, #event{}} | {error, not_found}.
get_by_id(Id) ->
case mnesia:dirty_read(event, Id) of
[] -> {error, not_found};
[Event] -> {ok, Event}
end.
%%%-------------------------------------------------------------------
%%% @doc Список активных событий календаря.
%%% @end
%%%-------------------------------------------------------------------
-spec list_by_calendar(CalendarId :: binary()) -> {ok, [#event{}]}.
list_by_calendar(CalendarId) ->
Match = #event{calendar_id = CalendarId, status = active, is_instance = false, _ = '_'},
Events = mnesia:dirty_match_object(Match),
{ok, Events}.
%%%-------------------------------------------------------------------
%%% @doc Обновить поля события.
%%% `Updates` список пар `[{atom(), term()}]`.
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
%%% @end
%%%-------------------------------------------------------------------
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
{ok, #event{}} | {error, not_found | term()}.
update(Id, Updates) ->
F = fun() ->
case mnesia:read(event, Id) of
[] -> {error, not_found};
[Event] ->
UpdatedEvent = apply_updates(Event, Updates),
mnesia:write(UpdatedEvent),
{ok, UpdatedEvent}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%%%-------------------------------------------------------------------
%%% @doc Мягкое удаление события (установка статуса `deleted`).
%%% @end
%%%-------------------------------------------------------------------
-spec delete(Id :: binary()) -> {ok, #event{}} | {error, not_found | term()}.
delete(Id) ->
update(Id, [{status, deleted}]).
%%%-------------------------------------------------------------------
%%% @doc Количество записей в таблице `event`.
%%% @end
%%%-------------------------------------------------------------------
-spec count_events() -> non_neg_integer().
count_events() ->
mnesia:table_info(event, size).
%%%-------------------------------------------------------------------
%%% @doc Получить список всех активных мастер-событий.
%%% @end
%%%-------------------------------------------------------------------
-spec list_all() -> [#event{}].
list_all() ->
Match = #event{status = active, is_instance = false, _ = '_'},
mnesia:dirty_match_object(Match).
%%%-------------------------------------------------------------------
%%% @doc Подсчёт событий, созданных в заданном временном диапазоне.
%%% Возвращает список кортежей `{{Year,Month,Day}, Count}`, отсортированный по дате.
%%% @end
%%%-------------------------------------------------------------------
-spec count_events_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
count_events_by_date(From, To) ->
All = mnesia:dirty_match_object(#event{_ = '_'}),
Filtered = lists:filter(fun(E) -> E#event.created_at >= From andalso
E#event.created_at =< To end, All),
Counts = lists:foldl(fun(E, Acc) ->
Day = date_part(E#event.created_at),
case lists:keyfind(Day, 1, Acc) of
false -> [{Day, 1} | Acc];
{Day, C} -> lists:keyreplace(Day, 1, Acc, {Day, C+1})
end
end, [], Filtered),
lists:sort(Counts).
%%%-------------------------------------------------------------------
%%% @doc Подсчёт событий по типам (single, recurring).
%%% @end
%%%-------------------------------------------------------------------
-spec count_events_by_type() -> [{atom(), non_neg_integer()}].
count_events_by_type() ->
Events = mnesia:dirty_match_object(#event{_ = '_'}),
lists:foldl(fun(#event{event_type = Type}, Acc) ->
case lists:keyfind(Type, 1, Acc) of
false -> [{Type, 1} | Acc];
{Type, C} -> lists:keyreplace(Type, 1, Acc, {Type, C+1})
end
end, [], Events).
%%%-------------------------------------------------------------------
%%% @doc Подсчёт событий по статусам (active, cancelled, completed).
%%% @end
%%%-------------------------------------------------------------------
-spec count_events_by_status() -> [{atom(), non_neg_integer()}].
count_events_by_status() ->
Events = mnesia:dirty_match_object(#event{_ = '_'}),
lists:foldl(fun(#event{status = Status}, Acc) ->
case lists:keyfind(Status, 1, Acc) of
false -> [{Status, 1} | Acc];
{Status, C} -> lists:keyreplace(Status, 1, Acc, {Status, C+1})
end
end, [], Events).
%%%-------------------------------------------------------------------
%%% @doc Возвращает топ-N событий по среднему рейтингу (по убыванию).
%%% @end
%%%-------------------------------------------------------------------
-spec get_top_events_by_rating(N :: non_neg_integer()) -> [#event{}].
get_top_events_by_rating(N) ->
Events = mnesia:dirty_match_object(#event{_ = '_'}),
Sorted = lists:reverse(lists:sort(
fun(A, B) -> A#event.rating_avg =< B#event.rating_avg end,
Events)),
lists:sublist(Sorted, N).
%%%-------------------------------------------------------------------
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
%%% @end
%%%-------------------------------------------------------------------
-spec date_part(calendar:datetime()) -> {pos_integer(), pos_integer(), pos_integer()}.
date_part({{Y,M,D}, _}) -> {Y,M,D}.
%%%-------------------------------------------------------------------
%%% @doc Применить список обновлений к записи события.
%%% @end
%%%-------------------------------------------------------------------
-spec apply_updates(#event{}, [{atom(), term()}]) -> #event{}.
apply_updates(Event, Updates) ->
Updated = lists:foldl(fun({Field, Value}, E) -> set_field(Field, Value, E) end,
Event, Updates),
Updated#event{updated_at = calendar:universal_time()}.
%%%-------------------------------------------------------------------
%%% @doc Заморозить событие с указанием причины.
%%% @end
%%%-------------------------------------------------------------------
-spec freeze(Id :: binary(), Reason :: binary()) ->
{ok, #event{}} | {error, not_found | term()}.
freeze(Id, Reason) ->
update(Id, [{status, frozen}, {reason, Reason}]).
%%%-------------------------------------------------------------------
%%% @doc Разморозить событие с указанием причины.
%%% @end
%%%-------------------------------------------------------------------
-spec unfreeze(Id :: binary(), Reason :: binary()) ->
{ok, #event{}} | {error, not_found | term()}.
unfreeze(Id, Reason) ->
update(Id, [{status, active}, {reason, Reason}]).
%%%===================================================================
%%% ВНУТРЕННИЕ ФУНКЦИИ
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Установить одно поле записи события.
%%% Неизвестные поля игнорируются.
%%% @end
%%%-------------------------------------------------------------------
-spec set_field(atom(), term(), #event{}) -> #event{}.
set_field(title, Value, E) -> E#event{title = Value};
set_field(description, Value, E) -> E#event{description = Value};
set_field(start_time, Value, E) -> E#event{start_time = Value};
set_field(duration, Value, E) -> E#event{duration = Value};
set_field(specialist_id, Value, E) -> E#event{specialist_id = Value};
set_field(location, Value, E) -> E#event{location = Value};
set_field(tags, Value, E) -> E#event{tags = Value};
set_field(capacity, Value, E) -> E#event{capacity = Value};
set_field(online_link, Value, E) -> E#event{online_link = Value};
set_field(status, Value, E) -> E#event{status = Value};
set_field(reason, Value, E) -> E#event{reason = Value};
set_field(rating_avg, Value, E) -> E#event{rating_avg = Value};
set_field(rating_count, Value, E) -> E#event{rating_count = Value};
set_field(attachments, Value, E) -> E#event{attachments = Value};
set_field(edit_history, Value, E) -> E#event{edit_history = Value};
set_field(recurrence_rule, Value, E) -> E#event{recurrence_rule = Value};
set_field(master_id, Value, E) -> E#event{master_id = Value};
set_field(is_instance, Value, E) -> E#event{is_instance = Value};
set_field(event_type, Value, E) -> E#event{event_type = Value};
set_field(calendar_id, Value, E) -> E#event{calendar_id = Value};
set_field(_, _, E) -> E.