fix ticket stats
This commit is contained in:
@@ -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
|
||||
}).
|
||||
|
||||
%% ------------------- Подписки ----------------------------------------
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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) ->
|
||||
|
||||
Reference in New Issue
Block a user