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