122 lines
6.0 KiB
C#
122 lines
6.0 KiB
C#
using Dapper;
|
|
using FastEndpoints;
|
|
using Npgsql;
|
|
using Shared.Operations;
|
|
using System.Text.Json;
|
|
|
|
namespace Modules.Common.Operations.Search;
|
|
|
|
public sealed class SearchWorkItemsRequest
|
|
{
|
|
public string? Module { get; init; }
|
|
public string? Severity { get; init; }
|
|
public string? Status { get; init; }
|
|
public string? Owner { get; init; }
|
|
public string? Code { get; init; }
|
|
public string? Keyword { get; init; }
|
|
public int Page { get; init; } = 1;
|
|
public int PageSize { get; init; } = 200;
|
|
}
|
|
|
|
public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint<SearchWorkItemsRequest, WorkQueueResponse>
|
|
{
|
|
public override void Configure()
|
|
{
|
|
Get("/api/operations/work-items");
|
|
Permissions("common.operations.read");
|
|
}
|
|
|
|
public override async Task HandleAsync(SearchWorkItemsRequest req, CancellationToken ct)
|
|
{
|
|
var tenantId = OperationsIdentity.TenantId(User);
|
|
var userId = OperationsIdentity.UserId(User);
|
|
var page = Math.Max(1, req.Page);
|
|
var pageSize = Math.Clamp(req.PageSize, 1, 500);
|
|
var offset = (page - 1) * pageSize;
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
const string rowsSql = """
|
|
select id, source_module as SourceModule, source_type as SourceType, source_id as SourceId,
|
|
reference_no as ReferenceNo, source_screen_id as SourceScreenId, code, title, detail, severity, status, owner_id as OwnerId, owner_name as OwnerName,
|
|
occurred_at as OccurredAt, due_at as DueAt,
|
|
greatest(0, floor(extract(epoch from (now() - occurred_at)) / 60))::int as AgeMinutes,
|
|
version, context::text as ContextJson, retry_action_key as RetryActionKey,
|
|
allow_manual_resolution as AllowManualResolution
|
|
from kbx.work_items
|
|
where tenant_id = @TenantId
|
|
and (@Module is null or source_module = @Module)
|
|
and (@Severity is null or severity = @Severity)
|
|
and (@Status is null or status = @Status)
|
|
and (@Code is null or code = @Code)
|
|
and (@Keyword is null or title ilike '%' || @Keyword || '%' or detail ilike '%' || @Keyword || '%' or reference_no ilike '%' || @Keyword || '%' or source_id ilike '%' || @Keyword || '%')
|
|
and (@Owner is null
|
|
or (@Owner = 'mine' and owner_id = @UserId)
|
|
or (@Owner = 'unassigned' and owner_id is null))
|
|
order by
|
|
case severity when 'critical' then 0 when 'warning' then 1 else 2 end,
|
|
coalesce(due_at, occurred_at), occurred_at
|
|
limit @PageSize offset @Offset;
|
|
""";
|
|
|
|
const string countSql = """
|
|
select count(*)::int
|
|
from kbx.work_items
|
|
where tenant_id = @TenantId
|
|
and (@Module is null or source_module = @Module)
|
|
and (@Severity is null or severity = @Severity)
|
|
and (@Status is null or status = @Status)
|
|
and (@Code is null or code = @Code)
|
|
and (@Keyword is null or title ilike '%' || @Keyword || '%' or detail ilike '%' || @Keyword || '%' or reference_no ilike '%' || @Keyword || '%' or source_id ilike '%' || @Keyword || '%')
|
|
and (@Owner is null
|
|
or (@Owner = 'mine' and owner_id = @UserId)
|
|
or (@Owner = 'unassigned' and owner_id is null));
|
|
""";
|
|
|
|
const string countersSql = """
|
|
select code as Key, min(title) as Label, count(*)::int as Count,
|
|
case max(case severity when 'critical' then 3 when 'warning' then 2 else 1 end)
|
|
when 3 then 'critical' when 2 then 'warning' else 'info' end as Severity
|
|
from kbx.work_items
|
|
where tenant_id = @TenantId and status in ('open','claimed')
|
|
group by code
|
|
order by max(case severity when 'critical' then 3 when 'warning' then 2 else 1 end) desc, count(*) desc
|
|
limit 12;
|
|
""";
|
|
|
|
var args = new {
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
Module = EmptyToNull(req.Module),
|
|
Severity = EmptyToNull(req.Severity),
|
|
Status = EmptyToNull(req.Status),
|
|
Owner = EmptyToNull(req.Owner),
|
|
Code = EmptyToNull(req.Code),
|
|
Keyword = EmptyToNull(req.Keyword),
|
|
PageSize = pageSize,
|
|
Offset = offset,
|
|
};
|
|
|
|
var rows = (await connection.QueryAsync<WorkItemProjection>(new CommandDefinition(rowsSql, args, cancellationToken: ct))).AsList();
|
|
var total = await connection.ExecuteScalarAsync<int>(new CommandDefinition(countSql, args, cancellationToken: ct));
|
|
var counters = (await connection.QueryAsync<WorkQueueCounter>(new CommandDefinition(countersSql, new { TenantId = tenantId }, cancellationToken: ct))).AsList();
|
|
var items = rows.Select(ToDto).ToList();
|
|
await Send.OkAsync(new WorkQueueResponse(items, total, counters), ct);
|
|
}
|
|
|
|
private static string? EmptyToNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
|
|
private static WorkItemDto ToDto(WorkItemProjection row)
|
|
{
|
|
var actions = new List<WorkItemActionDto> {
|
|
new("navigate", "원 업무 보기", "navigate"),
|
|
};
|
|
if (WorkItemActionPolicy.CanClaim(row.Status)) actions.Add(new("claim", "내가 처리", "claim", "common.operations.claim"));
|
|
if (WorkItemActionPolicy.CanRetry(row.Status, row.RetryActionKey)) actions.Add(new("retry", "재처리", "retry", "common.operations.retry"));
|
|
if (WorkItemActionPolicy.CanManualResolve(row.Status, row.AllowManualResolution)) actions.Add(new("resolve", "해결 처리", "resolve", "common.operations.resolve"));
|
|
var context = JsonSerializer.Deserialize<JsonElement>(row.ContextJson);
|
|
return new(row.Id, row.SourceModule, row.SourceType, row.SourceId, row.ReferenceNo, row.SourceScreenId, row.Code, row.Title, row.Detail,
|
|
row.Severity, row.Status, row.OwnerId, row.OwnerName, row.OccurredAt, row.DueAt, row.AgeMinutes,
|
|
row.Version, context, actions);
|
|
}
|
|
}
|