Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/backend/Modules/Common/Operations/Search/Endpoint.cs
T

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);
}
}