V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,47 @@
using Dapper;
using FastEndpoints;
using Npgsql;
namespace Modules.ERP.Inventory.Search;
public sealed record SearchInventoryRequest(string? Keyword, int Page = 1, int PageSize = 200);
public sealed record InventoryItemRow(Guid ItemId, string ItemCode, string ItemName, string Specification, decimal TotalQty, decimal AvailableQty, decimal AllocatedQty, decimal HoldQty);
public sealed record SearchInventoryResponse(IReadOnlyList<InventoryItemRow> Items, int TotalCount);
public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint<SearchInventoryRequest, SearchInventoryResponse>
{
public override void Configure()
{
Get("/api/erp/inventory");
Permissions("erp.inventory.read");
}
public override async Task HandleAsync(SearchInventoryRequest req, CancellationToken ct)
{
var page = Math.Max(req.Page, 1);
var pageSize = Math.Clamp(req.PageSize, 1, 500);
var keyword = string.IsNullOrWhiteSpace(req.Keyword) ? null : req.Keyword.Trim();
var offset = (page - 1) * pageSize;
const string sql = """
select item_id as ItemId, item_code as ItemCode, item_name as ItemName, specification,
sum(on_hand_qty) as TotalQty,
sum(available_qty) as AvailableQty,
sum(allocated_qty) as AllocatedQty,
sum(hold_qty) as HoldQty
from erp_inventory_snapshot_projection
where (@Keyword is null or search_text ilike '%' || @Keyword || '%')
group by item_id, item_code, item_name, specification
order by item_code
limit @PageSize offset @Offset;
select count(distinct item_id)::int
from erp_inventory_snapshot_projection
where (@Keyword is null or search_text ilike '%' || @Keyword || '%');
""";
await using var connection = await dataSource.OpenConnectionAsync(ct);
using var multi = await connection.QueryMultipleAsync(new CommandDefinition(sql, new { Keyword = keyword, PageSize = pageSize, Offset = offset }, cancellationToken: ct));
var items = (await multi.ReadAsync<InventoryItemRow>()).AsList();
var totalCount = await multi.ReadSingleAsync<int>();
await Send.OkAsync(new SearchInventoryResponse(items, totalCount), ct);
}
}
@@ -0,0 +1,32 @@
using Dapper;
using FastEndpoints;
using Npgsql;
namespace Modules.ERP.Inventory.Search;
public sealed class InventoryHistoryRequest { public Guid ItemId { get; init; } public int PageSize { get; init; } = 100; }
public sealed record InventoryHistoryRow(Guid EntryId, DateTimeOffset OccurredAt, string BusinessType, string ReferenceNo, string WarehouseName, string LocationCode, decimal InboundQty, decimal OutboundQty, decimal BalanceQty, string Actor);
public sealed record InventoryHistoryResponse(IReadOnlyList<InventoryHistoryRow> Items);
public sealed class HistoryEndpoint(NpgsqlDataSource dataSource) : Endpoint<InventoryHistoryRequest, InventoryHistoryResponse>
{
public override void Configure() { Get("/api/erp/inventory/{itemId}/history"); Permissions("erp.inventory.read"); }
public override async Task HandleAsync(InventoryHistoryRequest req, CancellationToken ct)
{
var pageSize=Math.Clamp(req.PageSize,1,500);
const string sql="""
select entry_id as EntryId, occurred_at as OccurredAt, business_type as BusinessType,
reference_no as ReferenceNo, warehouse_name as WarehouseName,
coalesce(location_code,'-') as LocationCode, inbound_qty as InboundQty,
outbound_qty as OutboundQty, balance_qty as BalanceQty, actor as Actor
from erp_inventory_ledger_projection
where item_id=@ItemId
order by occurred_at desc, entry_id
limit @PageSize;
""";
await using var connection=await dataSource.OpenConnectionAsync(ct);
var items=(await connection.QueryAsync<InventoryHistoryRow>(new CommandDefinition(sql,new{req.ItemId,PageSize=pageSize},cancellationToken:ct))).AsList();
await Send.OkAsync(new InventoryHistoryResponse(items),ct);
}
}
@@ -0,0 +1,37 @@
using Dapper;
using FastEndpoints;
using Npgsql;
namespace Modules.ERP.Inventory.Search;
public sealed class LocationsRequest { public Guid ItemId { get; init; } }
public sealed record InventoryLocationRow(string Key, string WarehouseName, string LocationCode, decimal OnHandQty, decimal AllocatedQty, decimal AvailableQty, decimal HoldQty);
public sealed record InventoryLocationsResponse(IReadOnlyList<InventoryLocationRow> Items);
public sealed class LocationsEndpoint(NpgsqlDataSource dataSource) : Endpoint<LocationsRequest, InventoryLocationsResponse>
{
public override void Configure()
{
Get("/api/erp/inventory/{itemId}/locations");
Permissions("erp.inventory.read");
}
public override async Task HandleAsync(LocationsRequest req, CancellationToken ct)
{
const string sql = """
select snapshot_key as Key,
warehouse_name as WarehouseName,
coalesce(location_code, '-') as LocationCode,
on_hand_qty as OnHandQty,
allocated_qty as AllocatedQty,
available_qty as AvailableQty,
hold_qty as HoldQty
from erp_inventory_snapshot_projection
where item_id = @ItemId
order by warehouse_name, location_code nulls first;
""";
await using var connection = await dataSource.OpenConnectionAsync(ct);
var items = (await connection.QueryAsync<InventoryLocationRow>(new CommandDefinition(sql, new { req.ItemId }, cancellationToken: ct))).AsList();
await Send.OkAsync(new InventoryLocationsResponse(items), ct);
}
}