V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
|
||||
namespace Modules.OMS.Orders.Search;
|
||||
|
||||
public sealed class SearchOrdersHandler(NpgsqlDataSource dataSource)
|
||||
{
|
||||
public async Task<SearchOrdersResponse> HandleAsync(SearchOrdersRequest request, CancellationToken ct)
|
||||
{
|
||||
var page = Math.Max(request.Page, 1);
|
||||
var pageSize = Math.Clamp(request.PageSize, 1, 500);
|
||||
var offset = (page - 1) * pageSize;
|
||||
|
||||
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||||
|
||||
// Read model query: UI-required projection in one server-side query boundary.
|
||||
const string rowsSql = """
|
||||
select
|
||||
o.id,
|
||||
o.order_no as OrderNo,
|
||||
ch.name as ChannelName,
|
||||
o.ordered_at as OrderedAt,
|
||||
o.customer_name as CustomerName,
|
||||
o.item_summary as ItemSummary,
|
||||
o.total_qty as TotalQty,
|
||||
o.amount as Amount,
|
||||
o.allocation_status as AllocationStatus,
|
||||
o.shipment_status as ShipmentStatus,
|
||||
o.exception_count as ExceptionCount
|
||||
from oms_order_search_projection o
|
||||
join sales_channel ch on ch.id = o.channel_id
|
||||
where o.ordered_at >= @From
|
||||
and o.ordered_at < @ToExclusive
|
||||
and (@ChannelId is null or o.channel_id = @ChannelId)
|
||||
and (@Status is null or o.shipment_status = @Status)
|
||||
and (@Keyword is null or o.search_text ilike '%' || @Keyword || '%')
|
||||
and (@ExceptionOnly = false or o.exception_count > 0)
|
||||
order by o.ordered_at desc, o.id desc
|
||||
limit @PageSize offset @Offset;
|
||||
""";
|
||||
|
||||
const string summarySql = """
|
||||
select
|
||||
count(*)::int as TotalCount,
|
||||
coalesce(sum(o.total_qty), 0) as TotalQty,
|
||||
coalesce(sum(o.amount), 0) as TotalAmount,
|
||||
count(*) filter (where o.order_status = 'NEW')::int as NewCount,
|
||||
count(*) filter (where o.shipment_status = 'READY')::int as ReadyToShipCount,
|
||||
count(*) filter (where o.exception_count > 0)::int as ExceptionCount
|
||||
from oms_order_search_projection o
|
||||
where o.ordered_at >= @From
|
||||
and o.ordered_at < @ToExclusive
|
||||
and (@ChannelId is null or o.channel_id = @ChannelId)
|
||||
and (@Status is null or o.shipment_status = @Status)
|
||||
and (@Keyword is null or o.search_text ilike '%' || @Keyword || '%')
|
||||
and (@ExceptionOnly = false or o.exception_count > 0);
|
||||
""";
|
||||
|
||||
var args = new
|
||||
{
|
||||
From = request.From.ToDateTime(TimeOnly.MinValue),
|
||||
ToExclusive = request.To.AddDays(1).ToDateTime(TimeOnly.MinValue),
|
||||
request.ChannelId,
|
||||
Status = string.IsNullOrWhiteSpace(request.Status) ? null : request.Status,
|
||||
Keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(),
|
||||
request.ExceptionOnly,
|
||||
PageSize = pageSize,
|
||||
Offset = offset,
|
||||
};
|
||||
|
||||
var rows = (await connection.QueryAsync<OrderSearchRow>(
|
||||
new CommandDefinition(rowsSql, args, cancellationToken: ct))).AsList();
|
||||
|
||||
var summary = await connection.QuerySingleAsync<SummaryRow>(
|
||||
new CommandDefinition(summarySql, args, cancellationToken: ct));
|
||||
|
||||
return new SearchOrdersResponse(
|
||||
rows,
|
||||
summary.TotalCount,
|
||||
summary.TotalQty,
|
||||
summary.TotalAmount,
|
||||
new OrderSearchCounters(
|
||||
summary.TotalCount,
|
||||
summary.NewCount,
|
||||
summary.ReadyToShipCount,
|
||||
summary.ExceptionCount));
|
||||
}
|
||||
|
||||
private sealed record SummaryRow(
|
||||
int TotalCount,
|
||||
decimal TotalQty,
|
||||
decimal TotalAmount,
|
||||
int NewCount,
|
||||
int ReadyToShipCount,
|
||||
int ExceptionCount);
|
||||
}
|
||||
Reference in New Issue
Block a user