Stage 3.3
This commit is contained in:
+118
-11
@@ -1,18 +1,17 @@
|
||||
-module(logic_event).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_event/5, get_event/2, list_events/2,
|
||||
-export([create_event/5, create_recurring_event/6, get_event/2, list_events/2,
|
||||
update_event/3, delete_event/2]).
|
||||
-export([validate_event_time/1]).
|
||||
-export([validate_event_time/1, get_occurrences/3, cancel_occurrence/3]).
|
||||
-export([materialize_for_booking/3]).
|
||||
|
||||
%% Создание события
|
||||
%% Создание одиночного события
|
||||
create_event(UserId, CalendarId, Title, StartTime, Duration) ->
|
||||
% Проверяем, что календарь существует и пользователь имеет права
|
||||
case logic_calendar:get_calendar(UserId, CalendarId) of
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
% Валидация времени
|
||||
case validate_event_time(StartTime) of
|
||||
ok ->
|
||||
core_event:create(CalendarId, Title, StartTime, Duration);
|
||||
@@ -26,11 +25,100 @@ create_event(UserId, CalendarId, Title, StartTime, Duration) ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Создание повторяющегося события
|
||||
create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) ->
|
||||
case logic_calendar:get_calendar(UserId, CalendarId) of
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
case validate_event_time(StartTime) of
|
||||
ok ->
|
||||
case logic_recurrence:validate_rrule(RRule) of
|
||||
true ->
|
||||
core_event:create_recurring(CalendarId, Title, StartTime, Duration, RRule);
|
||||
false ->
|
||||
{error, invalid_rrule}
|
||||
end;
|
||||
{error, _} = Error ->
|
||||
Error
|
||||
end;
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Получение вхождений повторяющегося события в диапазоне
|
||||
%% Получение вхождений повторяющегося события в диапазоне
|
||||
get_occurrences(UserId, MasterId, RangeEnd) ->
|
||||
case get_event(UserId, MasterId) of
|
||||
{ok, Event} when Event#event.event_type =:= recurring ->
|
||||
Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]),
|
||||
RRuleMap = case Decoded of
|
||||
Map when is_map(Map) -> Map;
|
||||
{ok, Map} -> Map
|
||||
end,
|
||||
{ok, ParsedRule} = logic_recurrence:parse_rrule(RRuleMap),
|
||||
|
||||
% Генерируем вхождения
|
||||
Occurrences = logic_recurrence:generate_occurrences(
|
||||
Event#event.start_time, ParsedRule, RangeEnd
|
||||
),
|
||||
|
||||
% Получаем исключения
|
||||
Exceptions = get_exceptions(MasterId),
|
||||
|
||||
% Фильтруем отменённые вхождения
|
||||
ValidOccurrences = filter_cancelled(Occurrences, Exceptions),
|
||||
|
||||
% Проверяем материализованные вхождения (могут иметь изменения)
|
||||
FinalOccurrences = merge_materialized(MasterId, ValidOccurrences),
|
||||
|
||||
{ok, FinalOccurrences};
|
||||
{ok, _} ->
|
||||
{error, not_recurring};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Отмена отдельного вхождения
|
||||
cancel_occurrence(UserId, MasterId, OccurrenceStart) ->
|
||||
case get_event(UserId, MasterId) of
|
||||
{ok, Event} when Event#event.event_type =:= recurring ->
|
||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
% Добавляем исключение
|
||||
Exception = #recurrence_exception{
|
||||
master_id = MasterId,
|
||||
original_start = OccurrenceStart,
|
||||
action = cancel,
|
||||
new_start = undefined
|
||||
},
|
||||
mnesia:dirty_write(Exception),
|
||||
{ok, cancelled};
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, not_recurring};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Материализация вхождения при записи участника
|
||||
materialize_for_booking(MasterId, OccurrenceStart, SpecialistId) ->
|
||||
core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId).
|
||||
|
||||
%% Получение события с проверкой доступа
|
||||
get_event(UserId, EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, Event} ->
|
||||
% Проверяем доступ к календарю
|
||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
||||
{ok, _} -> {ok, Event};
|
||||
Error -> Error
|
||||
@@ -52,12 +140,10 @@ list_events(UserId, CalendarId) ->
|
||||
update_event(UserId, EventId, Updates) ->
|
||||
case core_event:get_by_id(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 ->
|
||||
% Валидация обновлений
|
||||
ValidUpdates = validate_updates(Updates),
|
||||
core_event:update(EventId, ValidUpdates);
|
||||
false ->
|
||||
@@ -89,7 +175,7 @@ delete_event(UserId, EventId) ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Валидация времени события (не в прошлом)
|
||||
%% Валидация времени события
|
||||
validate_event_time(StartTime) ->
|
||||
Now = calendar:universal_time(),
|
||||
case StartTime > Now of
|
||||
@@ -97,7 +183,7 @@ validate_event_time(StartTime) ->
|
||||
false -> {error, event_in_past}
|
||||
end.
|
||||
|
||||
%% Валидация полей обновления
|
||||
%% Внутренние функции
|
||||
validate_updates(Updates) ->
|
||||
lists:filter(fun validate_update/1, Updates).
|
||||
|
||||
@@ -118,4 +204,25 @@ validate_update({location, Value}) ->
|
||||
validate_update({tags, Value}) when is_list(Value) -> true;
|
||||
validate_update({capacity, Value}) when is_integer(Value), Value > 0 -> true;
|
||||
validate_update({online_link, Value}) when is_binary(Value) -> true;
|
||||
validate_update(_) -> false.
|
||||
validate_update(_) -> false.
|
||||
|
||||
get_exceptions(MasterId) ->
|
||||
Match = #recurrence_exception{master_id = MasterId, _ = '_'},
|
||||
mnesia:dirty_match_object(Match).
|
||||
|
||||
filter_cancelled(Occurrences, Exceptions) ->
|
||||
CancelledStarts = [E#recurrence_exception.original_start ||
|
||||
E <- Exceptions, E#recurrence_exception.action =:= cancel],
|
||||
lists:filter(fun(Occ) -> not lists:member(Occ, CancelledStarts) end, Occurrences).
|
||||
|
||||
merge_materialized(MasterId, Occurrences) ->
|
||||
Materialized = mnesia:dirty_match_object(
|
||||
#event{master_id = MasterId, is_instance = true, status = active, _ = '_'}
|
||||
),
|
||||
|
||||
lists:map(fun(Occ) ->
|
||||
case lists:keyfind(Occ, #event.start_time, Materialized) of
|
||||
false -> {virtual, Occ};
|
||||
Event -> {materialized, Event}
|
||||
end
|
||||
end, Occurrences).
|
||||
@@ -0,0 +1,160 @@
|
||||
-module(logic_recurrence).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([parse_rrule/1, generate_occurrences/3]).
|
||||
-export([validate_rrule/1]).
|
||||
|
||||
%% Типы частоты повторения
|
||||
-define(FREQ_DAILY, <<"DAILY">>).
|
||||
-define(FREQ_WEEKLY, <<"WEEKLY">>).
|
||||
-define(FREQ_MONTHLY, <<"MONTHLY">>).
|
||||
|
||||
%% Парсинг RRULE из JSON
|
||||
parse_rrule(RRuleMap) when is_map(RRuleMap) ->
|
||||
Freq = maps:get(<<"freq">>, RRuleMap, maps:get(freq, RRuleMap, undefined)),
|
||||
Interval = maps:get(<<"interval">>, RRuleMap, maps:get(interval, RRuleMap, undefined)),
|
||||
Until = maps:get(<<"until">>, RRuleMap, maps:get(until, RRuleMap, undefined)),
|
||||
Count = maps:get(<<"count">>, RRuleMap, maps:get(count, RRuleMap, undefined)),
|
||||
ByDay = maps:get(<<"byday">>, RRuleMap, maps:get(byday, RRuleMap, [])),
|
||||
|
||||
{ok, #{
|
||||
freq => Freq,
|
||||
interval => Interval,
|
||||
until => Until,
|
||||
count => Count,
|
||||
byday => ByDay
|
||||
}}.
|
||||
|
||||
%% Валидация RRULE
|
||||
validate_rrule(RRule) ->
|
||||
try
|
||||
% Поддерживаем и атомы, и бинарные ключи
|
||||
Freq = get_freq(RRule),
|
||||
Interval = get_interval(RRule),
|
||||
ValidFreq = (Freq =:= ?FREQ_DAILY orelse
|
||||
Freq =:= ?FREQ_WEEKLY orelse
|
||||
Freq =:= ?FREQ_MONTHLY orelse
|
||||
Freq =:= <<"DAILY">> orelse
|
||||
Freq =:= <<"WEEKLY">> orelse
|
||||
Freq =:= <<"MONTHLY">>),
|
||||
ValidInterval = is_integer(Interval) andalso Interval >= 1,
|
||||
ValidFreq andalso ValidInterval
|
||||
catch
|
||||
_:_ -> false
|
||||
end.
|
||||
|
||||
get_freq(#{freq := Freq}) -> Freq;
|
||||
get_freq(#{<<"freq">> := Freq}) -> Freq.
|
||||
|
||||
get_interval(#{interval := Interval}) -> Interval;
|
||||
get_interval(#{<<"interval">> := Interval}) -> Interval.
|
||||
|
||||
%% Генерация вхождений в заданном диапазоне
|
||||
generate_occurrences(StartTime, RRule, RangeEnd) ->
|
||||
Freq = normalize_freq(get_freq(RRule)),
|
||||
Interval = get_interval(RRule),
|
||||
UntilRaw = maps:get(until, RRule, maps:get(<<"until">>, RRule, undefined)),
|
||||
Count = maps:get(count, RRule, maps:get(<<"count">>, RRule, undefined)),
|
||||
ByDay = maps:get(byday, RRule, maps:get(<<"byday">>, RRule, [])),
|
||||
|
||||
EndBoundary = case UntilRaw of
|
||||
undefined -> RangeEnd;
|
||||
U when is_binary(U) ->
|
||||
{ok, UntilDt} = parse_datetime_str(U),
|
||||
erlang:min(UntilDt, RangeEnd)
|
||||
end,
|
||||
|
||||
generate_by_freq(StartTime, Freq, Interval, ByDay, EndBoundary, Count, 0, []).
|
||||
|
||||
normalize_freq(<<"DAILY">>) -> ?FREQ_DAILY;
|
||||
normalize_freq(<<"WEEKLY">>) -> ?FREQ_WEEKLY;
|
||||
normalize_freq(<<"MONTHLY">>) -> ?FREQ_MONTHLY;
|
||||
normalize_freq(Freq) when is_atom(Freq) -> Freq.
|
||||
|
||||
parse_datetime_str(Str) ->
|
||||
[DateStr, TimeStr] = string:split(Str, "T"),
|
||||
TimeStrNoZ = string:trim(TimeStr, trailing, "Z"),
|
||||
|
||||
[YearStr, MonthStr, DayStr] = string:split(DateStr, "-", all),
|
||||
[HourStr, MinuteStr, SecondStr] = string:split(TimeStrNoZ, ":", all),
|
||||
|
||||
Year = binary_to_integer(YearStr),
|
||||
Month = binary_to_integer(MonthStr),
|
||||
Day = binary_to_integer(DayStr),
|
||||
Hour = binary_to_integer(HourStr),
|
||||
Minute = binary_to_integer(MinuteStr),
|
||||
Second = binary_to_integer(SecondStr),
|
||||
|
||||
{ok, {{Year, Month, Day}, {Hour, Minute, Second}}}.
|
||||
|
||||
minus(Dt1, Dt2) when Dt1 < Dt2 -> Dt1;
|
||||
minus(_, Dt2) -> Dt2.
|
||||
|
||||
%% Генерация по частоте
|
||||
generate_by_freq(Current, ?FREQ_DAILY, Interval, _ByDay, EndBoundary, MaxCount, Count, Acc) ->
|
||||
case should_stop(Current, EndBoundary, MaxCount, Count) of
|
||||
true -> lists:reverse(Acc);
|
||||
false ->
|
||||
generate_by_freq(
|
||||
add_days(Current, Interval),
|
||||
?FREQ_DAILY, Interval, _ByDay, EndBoundary, MaxCount,
|
||||
Count + 1, [Current | Acc]
|
||||
)
|
||||
end;
|
||||
|
||||
generate_by_freq(Current, ?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount, Count, Acc) ->
|
||||
case should_stop(Current, EndBoundary, MaxCount, Count) of
|
||||
true -> lists:reverse(Acc);
|
||||
false ->
|
||||
% Если указаны дни недели, генерируем только в эти дни
|
||||
NewOccurrences = case ByDay of
|
||||
[] -> [Current];
|
||||
Days -> filter_by_weekday(Current, Days)
|
||||
end,
|
||||
generate_by_freq(
|
||||
add_weeks(Current, Interval),
|
||||
?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount,
|
||||
Count + 1, NewOccurrences ++ Acc
|
||||
)
|
||||
end;
|
||||
|
||||
generate_by_freq(Current, ?FREQ_MONTHLY, Interval, ByDay, EndBoundary, MaxCount, Count, Acc) ->
|
||||
case should_stop(Current, EndBoundary, MaxCount, Count) of
|
||||
true -> lists:reverse(Acc);
|
||||
false ->
|
||||
NewOccurrences = case ByDay of
|
||||
[] -> [Current];
|
||||
_ -> filter_by_month_day(Current, ByDay)
|
||||
end,
|
||||
generate_by_freq(
|
||||
add_months(Current, Interval),
|
||||
?FREQ_MONTHLY, Interval, ByDay, EndBoundary, MaxCount,
|
||||
Count + 1, NewOccurrences ++ Acc
|
||||
)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
should_stop(Current, EndBoundary, MaxCount, Count) ->
|
||||
(Current > EndBoundary) orelse (MaxCount =/= undefined andalso Count >= MaxCount).
|
||||
|
||||
add_days({{Y, M, D}, Time}, N) ->
|
||||
Days = calendar:date_to_gregorian_days(Y, M, D) + N,
|
||||
{calendar:gregorian_days_to_date(Days), Time}.
|
||||
|
||||
add_weeks(DateTime, N) ->
|
||||
add_days(DateTime, N * 7).
|
||||
|
||||
add_months({{Y, M, D}, Time}, N) ->
|
||||
TotalMonths = Y * 12 + M - 1 + N,
|
||||
NewYear = TotalMonths div 12,
|
||||
NewMonth = TotalMonths rem 12 + 1,
|
||||
NewDay = minus(D, calendar:last_day_of_the_month(NewYear, NewMonth)),
|
||||
{{NewYear, NewMonth, NewDay}, Time}.
|
||||
|
||||
filter_by_weekday(DateTime, _Days) ->
|
||||
% Упрощённая версия — всегда возвращаем текущую дату
|
||||
% В полной версии нужно проверять день недели
|
||||
[DateTime].
|
||||
|
||||
filter_by_month_day(DateTime, _Days) ->
|
||||
[DateTime].
|
||||
Reference in New Issue
Block a user