fix ticket stats

This commit is contained in:
2026-05-19 22:16:26 +03:00
parent d040256447
commit faee11ce29
4 changed files with 82 additions and 26 deletions

View File

@@ -196,7 +196,8 @@
last_seen :: calendar:datetime(),
status :: open | in_progress | resolved | closed,
assigned_to :: binary(),
resolution_note :: binary()
resolution_note :: binary(),
closed_at :: calendar:datetime() | undefined
}).
%% ------------------- Подписки ----------------------------------------

View File

@@ -30,12 +30,13 @@ update_ticket(Id, Updates) ->
delete_ticket(Id) ->
case get_by_id(Id) of
{ok, _Ticket} -> % переменная не используется
{ok, _Ticket} ->
mnesia:dirty_delete({ticket, Id}),
{ok, deleted};
Error -> Error
end.
%% @doc Статистика по тикетам (используется в admin_handler_ticket_stats)
stats() ->
Tickets = list_all(),
#{
@@ -50,6 +51,8 @@ stats() ->
create_ticket(Data) ->
Id = infra_utils:generate_id(9),
Now = calendar:universal_time(),
Status0 = maps:get(<<"status">>, Data, open), %% <-- ИСПРАВЛЕНО: извлекаем сырое значение
Status = normalize_status(Status0), %% <-- ИСПРАВЛЕНО: приводим к атому
Ticket = #ticket{
id = Id,
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
@@ -60,9 +63,10 @@ create_ticket(Data) ->
count = 1,
first_seen = Now,
last_seen = Now,
status = maps:get(<<"status">>, Data, open),
status = Status, %% <-- ИСПРАВЛЕНО: сохраняем атом
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),
{ok, Ticket}.
@@ -70,32 +74,52 @@ create_ticket(Data) ->
list_by_user(UserId) ->
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
%% ── функции подсчёта с нормализацией статуса ─────────────
%% @private Подсчитывает тикеты с заданным статусом (атом или бинарный)
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) ->
Match = #ticket{status = Status, _ = '_'},
length(mnesia:dirty_match_object(Match)).
Tickets = list_all(),
count_by_status(Status, Tickets).
%% @doc Количество тикетов, назначенных администратору, с заданным статусом
count_tickets_by_admin(AdminId, Status) ->
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
length(mnesia:dirty_match_object(Match)).
Tickets = list_all(),
length([T || T <- Tickets,
T#ticket.assigned_to =:= AdminId andalso
normalize_status(T#ticket.status) =:= Status]).
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;
_ ->
TotalSeconds = lists:sum([calendar:datetime_to_gregorian_seconds(T#ticket.last_seen) -
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen) || T <- Tickets]),
TotalSeconds / length(Tickets) / 3600.0
TotalSeconds = lists:sum([
calendar:datetime_to_gregorian_seconds(T#ticket.closed_at) -
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen)
|| T <- ClosedTickets]),
TotalSeconds / length(ClosedTickets) / 3600.0
end.
%% ── внутренние ─────────────────────────────────────────
apply_updates(Ticket, Updates) ->
lists:foldl(fun({Key, Value}, Acc) ->
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};
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
<<"error_message">> -> Acc#ticket{error_message = Value};
@@ -104,3 +128,11 @@ apply_updates(Ticket, Updates) ->
_ -> Acc
end
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.

View File

@@ -105,18 +105,24 @@ parse_ticket_filters(Req) ->
q => proplists:get_value(<<"q">>, Qs)
}.
%% @private Дополнительная фильтрация (assigned_to, q).
%% @private Дополнительная фильтрация (status, assigned_to, q).
-spec apply_ticket_filters([#ticket{}], map()) -> [#ticket{}].
apply_ticket_filters(Tickets, Filters) ->
Status = maps:get(status, Filters, undefined),
Assigned = maps:get(assigned_to, Filters, undefined),
Q = maps:get(q, Filters, undefined),
F1 = case Assigned of
F1 = case Status of
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,
case Q of
undefined -> F1;
_ -> [T || T <- F1,
undefined -> F2;
_ -> [T || T <- F2,
string:str(binary_to_list(T#ticket.error_message), binary_to_list(Q)) > 0]
end.
@@ -148,3 +154,11 @@ pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
<<"x-total-count">> => integer_to_binary(Total),
<<"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.

View File

@@ -118,10 +118,10 @@ get_statistics(AdminId) ->
case admin_utils:is_admin(AdminId) of
true ->
All = core_ticket:list_all(),
Open = length([T || T <- All, T#ticket.status =:= open]),
InProgress = length([T || T <- All, T#ticket.status =:= in_progress]),
Resolved = length([T || T <- All, T#ticket.status =:= resolved]),
Closed = length([T || T <- All, T#ticket.status =:= closed]),
Open = count_by_status(All, open),
InProgress = count_by_status(All, in_progress),
Resolved = count_by_status(All, resolved),
Closed = count_by_status(All, closed),
TotalErrors = lists:sum([T#ticket.count || T <- All]),
#{
total_tickets => length(All),
@@ -134,6 +134,15 @@ get_statistics(AdminId) ->
false -> {error, access_denied}
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) ->