360 lines
16 KiB
Erlang
360 lines
16 KiB
Erlang
-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. |