fix ticket stats
This commit is contained in:
@@ -196,7 +196,8 @@
|
|||||||
last_seen :: calendar:datetime(),
|
last_seen :: calendar:datetime(),
|
||||||
status :: open | in_progress | resolved | closed,
|
status :: open | in_progress | resolved | closed,
|
||||||
assigned_to :: binary(),
|
assigned_to :: binary(),
|
||||||
resolution_note :: binary()
|
resolution_note :: binary(),
|
||||||
|
closed_at :: calendar:datetime() | undefined
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%% ------------------- Подписки ----------------------------------------
|
%% ------------------- Подписки ----------------------------------------
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ update_ticket(Id, Updates) ->
|
|||||||
|
|
||||||
delete_ticket(Id) ->
|
delete_ticket(Id) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, _Ticket} -> % переменная не используется
|
{ok, _Ticket} ->
|
||||||
mnesia:dirty_delete({ticket, Id}),
|
mnesia:dirty_delete({ticket, Id}),
|
||||||
{ok, deleted};
|
{ok, deleted};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc Статистика по тикетам (используется в admin_handler_ticket_stats)
|
||||||
stats() ->
|
stats() ->
|
||||||
Tickets = list_all(),
|
Tickets = list_all(),
|
||||||
#{
|
#{
|
||||||
@@ -50,6 +51,8 @@ stats() ->
|
|||||||
create_ticket(Data) ->
|
create_ticket(Data) ->
|
||||||
Id = infra_utils:generate_id(9),
|
Id = infra_utils:generate_id(9),
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
|
Status0 = maps:get(<<"status">>, Data, open), %% <-- ИСПРАВЛЕНО: извлекаем сырое значение
|
||||||
|
Status = normalize_status(Status0), %% <-- ИСПРАВЛЕНО: приводим к атому
|
||||||
Ticket = #ticket{
|
Ticket = #ticket{
|
||||||
id = Id,
|
id = Id,
|
||||||
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
|
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
|
||||||
@@ -60,9 +63,10 @@ create_ticket(Data) ->
|
|||||||
count = 1,
|
count = 1,
|
||||||
first_seen = Now,
|
first_seen = Now,
|
||||||
last_seen = Now,
|
last_seen = Now,
|
||||||
status = maps:get(<<"status">>, Data, open),
|
status = Status, %% <-- ИСПРАВЛЕНО: сохраняем атом
|
||||||
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
|
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
|
||||||
resolution_note = maps:get(<<"resolution_note">>, Data, undefined)
|
resolution_note = maps:get(<<"resolution_note">>, Data, undefined),
|
||||||
|
closed_at = undefined
|
||||||
},
|
},
|
||||||
mnesia:dirty_write(Ticket),
|
mnesia:dirty_write(Ticket),
|
||||||
{ok, Ticket}.
|
{ok, Ticket}.
|
||||||
@@ -70,32 +74,52 @@ create_ticket(Data) ->
|
|||||||
list_by_user(UserId) ->
|
list_by_user(UserId) ->
|
||||||
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
||||||
|
|
||||||
|
%% ── функции подсчёта с нормализацией статуса ─────────────
|
||||||
|
|
||||||
|
%% @private Подсчитывает тикеты с заданным статусом (атом или бинарный)
|
||||||
count_by_status(Status, Tickets) ->
|
count_by_status(Status, Tickets) ->
|
||||||
length([T || T <- Tickets, T#ticket.status =:= Status]).
|
length([T || T <- Tickets, normalize_status(T#ticket.status) =:= Status]).
|
||||||
|
|
||||||
|
%% @doc Количество тикетов по статусу (атом или бинарный)
|
||||||
count_tickets_by_status(Status) ->
|
count_tickets_by_status(Status) ->
|
||||||
Match = #ticket{status = Status, _ = '_'},
|
Tickets = list_all(),
|
||||||
length(mnesia:dirty_match_object(Match)).
|
count_by_status(Status, Tickets).
|
||||||
|
|
||||||
|
%% @doc Количество тикетов, назначенных администратору, с заданным статусом
|
||||||
count_tickets_by_admin(AdminId, Status) ->
|
count_tickets_by_admin(AdminId, Status) ->
|
||||||
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
Tickets = list_all(),
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length([T || T <- Tickets,
|
||||||
|
T#ticket.assigned_to =:= AdminId andalso
|
||||||
|
normalize_status(T#ticket.status) =:= Status]).
|
||||||
|
|
||||||
avg_resolution_time() ->
|
avg_resolution_time() ->
|
||||||
Tickets = mnesia:dirty_match_object(#ticket{status = closed, _ = '_'}),
|
% Загружаем все тикеты (или можно только закрытые, если их мало – решите по нагрузке)
|
||||||
case Tickets of
|
Tickets = mnesia:dirty_match_object(#ticket{_ = '_'}),
|
||||||
|
% Фильтруем закрытые с учётом нормализации статуса
|
||||||
|
ClosedTickets = [T || T <- Tickets,
|
||||||
|
normalize_status(T#ticket.status) =:= closed,
|
||||||
|
T#ticket.closed_at =/= undefined],
|
||||||
|
case ClosedTickets of
|
||||||
[] -> 0;
|
[] -> 0;
|
||||||
_ ->
|
_ ->
|
||||||
TotalSeconds = lists:sum([calendar:datetime_to_gregorian_seconds(T#ticket.last_seen) -
|
TotalSeconds = lists:sum([
|
||||||
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen) || T <- Tickets]),
|
calendar:datetime_to_gregorian_seconds(T#ticket.closed_at) -
|
||||||
TotalSeconds / length(Tickets) / 3600.0
|
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen)
|
||||||
|
|| T <- ClosedTickets]),
|
||||||
|
TotalSeconds / length(ClosedTickets) / 3600.0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ── внутренние ─────────────────────────────────────────
|
%% ── внутренние ─────────────────────────────────────────
|
||||||
apply_updates(Ticket, Updates) ->
|
apply_updates(Ticket, Updates) ->
|
||||||
lists:foldl(fun({Key, Value}, Acc) ->
|
lists:foldl(fun({Key, Value}, Acc) ->
|
||||||
case Key of
|
case Key of
|
||||||
<<"status">> -> Acc#ticket{status = binary_to_atom(Value, utf8)};
|
<<"status">> ->
|
||||||
|
NewStatus = normalize_status(Value),
|
||||||
|
Acc1 = Acc#ticket{status = NewStatus},
|
||||||
|
case NewStatus of
|
||||||
|
closed -> Acc1#ticket{closed_at = calendar:universal_time()};
|
||||||
|
_ -> Acc1
|
||||||
|
end;
|
||||||
<<"assigned_to">> -> Acc#ticket{assigned_to = Value};
|
<<"assigned_to">> -> Acc#ticket{assigned_to = Value};
|
||||||
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
|
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
|
||||||
<<"error_message">> -> Acc#ticket{error_message = Value};
|
<<"error_message">> -> Acc#ticket{error_message = Value};
|
||||||
@@ -103,4 +127,12 @@ apply_updates(Ticket, Updates) ->
|
|||||||
<<"context">> -> Acc#ticket{context = Value};
|
<<"context">> -> Acc#ticket{context = Value};
|
||||||
_ -> Acc
|
_ -> Acc
|
||||||
end
|
end
|
||||||
end, Ticket, maps:to_list(Updates)).
|
end, Ticket, maps:to_list(Updates)).
|
||||||
|
|
||||||
|
%% @private Преобразует бинарный статус в атом, если нужно.
|
||||||
|
%% Атомы возвращает без изменений.
|
||||||
|
normalize_status(Status) when is_atom(Status) -> Status;
|
||||||
|
normalize_status(Status) when is_binary(Status) ->
|
||||||
|
try binary_to_existing_atom(Status, utf8)
|
||||||
|
catch error:badarg -> Status
|
||||||
|
end.
|
||||||
@@ -105,18 +105,24 @@ parse_ticket_filters(Req) ->
|
|||||||
q => proplists:get_value(<<"q">>, Qs)
|
q => proplists:get_value(<<"q">>, Qs)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% @private Дополнительная фильтрация (assigned_to, q).
|
%% @private Дополнительная фильтрация (status, assigned_to, q).
|
||||||
-spec apply_ticket_filters([#ticket{}], map()) -> [#ticket{}].
|
-spec apply_ticket_filters([#ticket{}], map()) -> [#ticket{}].
|
||||||
apply_ticket_filters(Tickets, Filters) ->
|
apply_ticket_filters(Tickets, Filters) ->
|
||||||
|
Status = maps:get(status, Filters, undefined),
|
||||||
Assigned = maps:get(assigned_to, Filters, undefined),
|
Assigned = maps:get(assigned_to, Filters, undefined),
|
||||||
Q = maps:get(q, Filters, undefined),
|
Q = maps:get(q, Filters, undefined),
|
||||||
F1 = case Assigned of
|
F1 = case Status of
|
||||||
undefined -> Tickets;
|
undefined -> Tickets;
|
||||||
_ -> [T || T <- Tickets, T#ticket.assigned_to =:= Assigned]
|
_ -> [T || T <- Tickets,
|
||||||
|
normalize_status(T#ticket.status) =:= normalize_status(Status)]
|
||||||
|
end,
|
||||||
|
F2 = case Assigned of
|
||||||
|
undefined -> F1;
|
||||||
|
_ -> [T || T <- F1, T#ticket.assigned_to =:= Assigned]
|
||||||
end,
|
end,
|
||||||
case Q of
|
case Q of
|
||||||
undefined -> F1;
|
undefined -> F2;
|
||||||
_ -> [T || T <- F1,
|
_ -> [T || T <- F2,
|
||||||
string:str(binary_to_list(T#ticket.error_message), binary_to_list(Q)) > 0]
|
string:str(binary_to_list(T#ticket.error_message), binary_to_list(Q)) > 0]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@@ -147,4 +153,12 @@ pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
|
|||||||
<<"content-range">> => iolist_to_binary(io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])),
|
<<"content-range">> => iolist_to_binary(io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])),
|
||||||
<<"x-total-count">> => integer_to_binary(Total),
|
<<"x-total-count">> => integer_to_binary(Total),
|
||||||
<<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">>
|
<<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">>
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
%% @private Нормализует статус: атом → атом, binary → атом.
|
||||||
|
-spec normalize_status(atom() | binary()) -> atom().
|
||||||
|
normalize_status(Status) when is_atom(Status) -> Status;
|
||||||
|
normalize_status(Status) when is_binary(Status) ->
|
||||||
|
try binary_to_existing_atom(Status, utf8)
|
||||||
|
catch error:badarg -> Status
|
||||||
|
end.
|
||||||
@@ -118,10 +118,10 @@ get_statistics(AdminId) ->
|
|||||||
case admin_utils:is_admin(AdminId) of
|
case admin_utils:is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
All = core_ticket:list_all(),
|
All = core_ticket:list_all(),
|
||||||
Open = length([T || T <- All, T#ticket.status =:= open]),
|
Open = count_by_status(All, open),
|
||||||
InProgress = length([T || T <- All, T#ticket.status =:= in_progress]),
|
InProgress = count_by_status(All, in_progress),
|
||||||
Resolved = length([T || T <- All, T#ticket.status =:= resolved]),
|
Resolved = count_by_status(All, resolved),
|
||||||
Closed = length([T || T <- All, T#ticket.status =:= closed]),
|
Closed = count_by_status(All, closed),
|
||||||
TotalErrors = lists:sum([T#ticket.count || T <- All]),
|
TotalErrors = lists:sum([T#ticket.count || T <- All]),
|
||||||
#{
|
#{
|
||||||
total_tickets => length(All),
|
total_tickets => length(All),
|
||||||
@@ -134,6 +134,15 @@ get_statistics(AdminId) ->
|
|||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
count_by_status(Tickets, Status) ->
|
||||||
|
length([T || T <- Tickets, normalize_status(T#ticket.status) =:= Status]).
|
||||||
|
|
||||||
|
normalize_status(Status) when is_atom(Status) -> Status;
|
||||||
|
normalize_status(Status) when is_binary(Status) ->
|
||||||
|
try binary_to_existing_atom(Status, utf8)
|
||||||
|
catch error:badarg -> Status
|
||||||
|
end.
|
||||||
|
|
||||||
%% ============ Вспомогательные функции ============
|
%% ============ Вспомогательные функции ============
|
||||||
|
|
||||||
notify_admins(_Ticket) ->
|
notify_admins(_Ticket) ->
|
||||||
|
|||||||
Reference in New Issue
Block a user