This commit is contained in:
2026-04-20 21:04:16 +03:00
parent b24cbc97f3
commit 19f82768e4
18 changed files with 1851 additions and 131 deletions

View File

@@ -24,9 +24,16 @@ create_calendar(Req) ->
#{<<"title">> := Title} ->
Description = maps:get(<<"description">>, Decoded, <<"">>),
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
Tags = maps:get(<<"tags">>, Decoded, []),
Type = parse_type(maps:get(<<"type">>, Decoded, <<"personal">>)),
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation) of
{ok, Calendar} ->
Response = calendar_to_json(Calendar),
% Обновляем теги и тип
Updates = [{tags, Tags}, {type, Type}],
core_calendar:update(Calendar#calendar.id, Updates),
{ok, Updated} = core_calendar:get_by_id(Calendar#calendar.id),
Response = calendar_to_json(Updated),
send_json(Req2, 201, Response);
{error, user_inactive} ->
send_error(Req2, 403, <<"User account is not active">>);
@@ -51,6 +58,10 @@ parse_confirmation(<<"manual">>) -> manual;
parse_confirmation(#{<<"timeout">> := N}) when is_integer(N), N > 0 -> {timeout, N};
parse_confirmation(_) -> manual.
parse_type(<<"personal">>) -> personal;
parse_type(<<"commercial">>) -> commercial;
parse_type(_) -> personal.
%% GET /v1/calendars - список календарей
list_calendars(Req) ->
case handler_auth:authenticate(Req) of

View File

@@ -43,9 +43,8 @@ update_event(Req) ->
try jsx:decode(Body, [return_maps]) of
UpdatesMap when is_map(UpdatesMap) ->
Updates = maps:to_list(UpdatesMap),
% Преобразуем строку времени в datetime если есть
UpdatesWithTime = convert_time_field(Updates),
case logic_event:update_event(UserId, EventId, UpdatesWithTime) of
UpdatesWithTypes = convert_fields(Updates),
case logic_event:update_event(UserId, EventId, UpdatesWithTypes) of
{ok, Event} ->
Response = event_to_json(Event),
send_json(Req2, 200, Response);
@@ -88,13 +87,22 @@ delete_event(Req) ->
end.
%% Вспомогательные функции
convert_time_field(Updates) ->
convert_fields(Updates) ->
lists:map(fun
({start_time, Value}) when is_binary(Value) ->
case parse_datetime(Value) of
{ok, DateTime} -> {start_time, DateTime};
_ -> {start_time, Value}
end;
({location, Value}) when is_map(Value) ->
case Value of
#{<<"lat">> := Lat, <<"lon">> := Lon} ->
Address = maps:get(<<"address">>, Value, <<"">>),
{location, #location{address = Address, lat = Lat, lon = Lon}};
_ -> {location, undefined}
end;
({tags, Value}) when is_list(Value) ->
{tags, Value};
(Other) -> Other
end, Updates).
@@ -125,6 +133,16 @@ event_to_json(Event) ->
#{address => Addr, lat => Lat, lon => Lon}
end,
RecurrenceJson = case Event#event.recurrence_rule of
undefined -> null;
Rule ->
Decoded = jsx:decode(Rule, [return_maps]),
case Decoded of
Map when is_map(Map) -> Map;
{ok, Map} -> Map
end
end,
#{
id => Event#event.id,
calendar_id => Event#event.calendar_id,
@@ -133,6 +151,9 @@ event_to_json(Event) ->
event_type => Event#event.event_type,
start_time => datetime_to_iso8601(Event#event.start_time),
duration => Event#event.duration,
recurrence => RecurrenceJson,
master_id => Event#event.master_id,
is_instance => Event#event.is_instance,
specialist_id => Event#event.specialist_id,
location => LocationJson,
tags => Event#event.tags,

View File

@@ -27,13 +27,19 @@ create_event(Req) ->
<<"duration">> := Duration} ->
case parse_datetime(StartTimeStr) of
{ok, StartTime} ->
% Парсим location если есть
Location = parse_location(maps:get(<<"location">>, Decoded, undefined)),
% Проверяем, есть ли правило повторения
case maps:get(<<"recurrence">>, Decoded, undefined) of
undefined ->
% Одиночное событие
case logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration) of
{ok, Event} ->
Response = event_to_json(Event),
% Обновляем location и capacity если нужно
update_event_fields(Event#event.id, Location, Decoded),
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
Response = event_to_json(UpdatedEvent),
send_json(Req2, 201, Response);
{error, access_denied} ->
send_error(Req2, 403, <<"Access denied">>);
@@ -48,7 +54,9 @@ create_event(Req) ->
% Повторяющееся событие
case logic_event:create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) of
{ok, Event} ->
Response = event_to_json(Event),
update_event_fields(Event#event.id, Location, Decoded),
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
Response = event_to_json(UpdatedEvent),
send_json(Req2, 201, Response);
{error, invalid_rrule} ->
send_error(Req2, 400, <<"Invalid recurrence rule">>);
@@ -83,14 +91,12 @@ list_events(Req) ->
case handler_auth:authenticate(Req) of
{ok, UserId, Req1} ->
CalendarId = cowboy_req:binding(calendar_id, Req1),
% Проверяем параметры запроса для диапазона дат
Qs = cowboy_req:parse_qs(Req1),
From = proplists:get_value(<<"from">>, Qs, undefined),
To = proplists:get_value(<<"to">>, Qs, undefined),
case logic_event:list_events(UserId, CalendarId) of
{ok, Events} ->
% Если указан диапазон, разворачиваем повторяющиеся события
Response = case {From, To} of
{undefined, undefined} ->
[event_to_json(E) || E <- Events];
@@ -111,7 +117,43 @@ list_events(Req) ->
send_error(Req1, Code, Message)
end.
%% Разворачивание повторяющихся событий в диапазоне
%% Вспомогательные функции
update_event_fields(EventId, Location, Decoded) ->
Updates = [],
Updates1 = case Location of
undefined -> Updates;
_ -> [{location, Location} | Updates]
end,
Updates2 = case maps:get(<<"capacity">>, Decoded, undefined) of
undefined -> Updates1;
Cap -> [{capacity, Cap} | Updates1]
end,
Updates3 = case maps:get(<<"tags">>, Decoded, undefined) of
undefined -> Updates2;
Tags -> [{tags, Tags} | Updates2]
end,
Updates4 = case maps:get(<<"description">>, Decoded, undefined) of
undefined -> Updates3;
Desc -> [{description, Desc} | Updates3]
end,
Updates5 = case maps:get(<<"online_link">>, Decoded, undefined) of
undefined -> Updates4;
Link -> [{online_link, Link} | Updates4]
end,
if Updates5 /= [] -> core_event:update(EventId, Updates5);
true -> ok
end.
parse_location(undefined) -> undefined;
parse_location(LocationMap) when is_map(LocationMap) ->
case LocationMap of
#{<<"lat">> := Lat, <<"lon">> := Lon} ->
Address = maps:get(<<"address">>, LocationMap, <<"">>),
#location{address = Address, lat = Lat, lon = Lon};
_ -> undefined
end;
parse_location(_) -> undefined.
expand_recurring_events(UserId, Events, From, To) ->
lists:flatmap(fun(Event) ->
case Event#event.event_type of
@@ -148,7 +190,6 @@ parse_datetime_binary(Str) ->
{ok, Dt} = parse_datetime(Str),
Dt.
%% Вспомогательные функции
parse_datetime(Str) ->
try
[DateStr, TimeStr] = string:split(Str, "T"),
@@ -178,7 +219,12 @@ event_to_json(Event) ->
RecurrenceJson = case Event#event.recurrence_rule of
undefined -> null;
Rule -> jsx:decode(Rule, [return_maps])
Rule ->
Decoded = jsx:decode(Rule, [return_maps]),
case Decoded of
Map when is_map(Map) -> Map;
{ok, Map} -> Map
end
end,
#{

View File

@@ -0,0 +1,104 @@
-module(handler_search).
-include("records.hrl").
-export([init/2]).
init(Req, Opts) ->
handle(Req, Opts).
handle(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> search(Req);
_ -> send_error(Req, 405, <<"Method not allowed">>)
end.
search(Req) ->
case handler_auth:authenticate(Req) of
{ok, UserId, Req1} ->
Qs = cowboy_req:parse_qs(Req1),
Type = proplists:get_value(<<"type">>, Qs, undefined),
Query = proplists:get_value(<<"q">>, Qs, undefined),
Params = parse_params(Qs),
case logic_search:search(Type, Query, UserId, Params) of
{ok, Total, Results} ->
Response = #{
total => Total,
limit => maps:get(limit, Params, 20),
offset => maps:get(offset, Params, 0),
results => Results
},
send_json(Req1, 200, Response);
{error, _} ->
send_error(Req1, 500, <<"Search failed">>)
end;
{error, Code, Message, Req1} ->
send_error(Req1, Code, Message)
end.
parse_params(Qs) ->
Params = #{
limit => parse_int_param(Qs, <<"limit">>, 20),
offset => parse_int_param(Qs, <<"offset">>, 0),
tags => proplists:get_value(<<"tags">>, Qs),
sort => proplists:get_value(<<"sort">>, Qs, <<"start_time">>),
order => proplists:get_value(<<"order">>, Qs, <<"asc">>)
},
Params1 = case {parse_float_param(Qs, <<"lat">>), parse_float_param(Qs, <<"lon">>)} of
{{ok, Lat}, {ok, Lon}} ->
Radius = parse_int_param(Qs, <<"radius">>, 10),
Params#{lat => Lat, lon => Lon, radius => Radius};
_ -> Params
end,
Params2 = case {parse_datetime_param(Qs, <<"from">>), parse_datetime_param(Qs, <<"to">>)} of
{{ok, From}, {ok, To}} ->
Params1#{from => From, to => To};
{{ok, From}, error} ->
Params1#{from => From};
{error, {ok, To}} ->
Params1#{to => To};
_ -> Params1
end,
Params2.
parse_int_param(Qs, Key, Default) ->
case proplists:get_value(Key, Qs) of
undefined -> Default;
Val -> binary_to_integer(Val)
end.
parse_float_param(Qs, Key) ->
case proplists:get_value(Key, Qs) of
undefined -> error;
Val -> {ok, binary_to_float(Val)}
end.
parse_datetime_param(Qs, Key) ->
case proplists:get_value(Key, Qs) of
undefined -> error;
Val ->
try
[DateStr, TimeStr] = string:split(Val, "T"),
TimeStrNoZ = string:trim(TimeStr, trailing, "Z"),
[Y, M, D] = [binary_to_integer(X) || X <- string:split(DateStr, "-", all)],
[H, Min, S] = [binary_to_integer(X) || X <- string:split(TimeStrNoZ, ":", all)],
{ok, {{Y, M, D}, {H, Min, S}}}
catch
_:_ -> error
end
end.
send_json(Req, Status, Data) ->
Body = jsx:encode(Data),
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
send_error(Req, Status, Message) ->
Body = jsx:encode(#{error => Message}),
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).