V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user