Во всех таблицах можно сортировать по клику на заголовок столбца:
первый клик — по возрастанию (↑), второй — по убыванию (↓),
смена колонки сбрасывает предыдущую. Без активной сортировки —
серверный default (обычно по Name ASC).
Реализация:
- PagedRequest: добавлены Sort (ключ колонки) и Order ("asc"/"desc"),
плюс удобное свойство Desc.
- DataTable: Column.sortKey + props sortKey/sortOrder/onSortChange,
в заголовке появляется иконка (ArrowUpDown/ArrowUp/ArrowDown).
- useCatalogList: хранит sortKey/sortOrder, отдаёт setSort, шлёт
?sort=&order= в query-string.
- Все 10 List-эндпоинтов (Countries, Currencies, UnitsOfMeasure,
PriceTypes, Stores, RetailPoints, Counterparties, ProductGroups,
Products, Supplies, RetailSales + Stock/Movements) принимают
параметры и применяют switch-based OrderBy по whitelisted ключам.
- Все страницы со списками прокидывают sort state и sortKey на
колонках, где сортировка имеет смысл (тексты/числа/даты).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
6.8 KiB
C#
142 lines
6.8 KiB
C#
using foodmarket.Application.Common;
|
|
using foodmarket.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace foodmarket.Api.Controllers.Inventory;
|
|
|
|
[ApiController]
|
|
[Authorize]
|
|
[Route("api/inventory")]
|
|
public class StockController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _db;
|
|
public StockController(AppDbContext db) => _db = db;
|
|
|
|
public record StockRow(
|
|
Guid ProductId, string ProductName, string? Article, string UnitSymbol,
|
|
Guid StoreId, string StoreName,
|
|
decimal Quantity, decimal ReservedQuantity, decimal Available);
|
|
|
|
[HttpGet("stock")]
|
|
public async Task<ActionResult<PagedResult<StockRow>>> GetStock(
|
|
[FromQuery] Guid? storeId,
|
|
[FromQuery] Guid? productId,
|
|
[FromQuery] string? search,
|
|
[FromQuery] bool includeZero = false,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 50,
|
|
[FromQuery] string? sort = null,
|
|
[FromQuery] string? order = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var q = from s in _db.Stocks
|
|
join p in _db.Products on s.ProductId equals p.Id
|
|
join u in _db.UnitsOfMeasure on p.UnitOfMeasureId equals u.Id
|
|
join st in _db.Stores on s.StoreId equals st.Id
|
|
select new { s, p, u, st };
|
|
|
|
if (storeId.HasValue) q = q.Where(x => x.s.StoreId == storeId.Value);
|
|
if (productId.HasValue) q = q.Where(x => x.s.ProductId == productId.Value);
|
|
if (!includeZero) q = q.Where(x => x.s.Quantity != 0);
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var term = $"%{search.Trim()}%";
|
|
q = q.Where(x => EF.Functions.ILike(x.p.Name, term)
|
|
|| (x.p.Article != null && EF.Functions.ILike(x.p.Article, term)));
|
|
}
|
|
|
|
var total = await q.CountAsync(ct);
|
|
var desc = string.Equals(order, "desc", StringComparison.OrdinalIgnoreCase);
|
|
q = (sort, desc) switch
|
|
{
|
|
("article", false) => q.OrderBy(x => x.p.Article).ThenBy(x => x.p.Name),
|
|
("article", true) => q.OrderByDescending(x => x.p.Article).ThenBy(x => x.p.Name),
|
|
("unit", false) => q.OrderBy(x => x.u.Name).ThenBy(x => x.p.Name),
|
|
("unit", true) => q.OrderByDescending(x => x.u.Name).ThenBy(x => x.p.Name),
|
|
("store", false) => q.OrderBy(x => x.st.Name).ThenBy(x => x.p.Name),
|
|
("store", true) => q.OrderByDescending(x => x.st.Name).ThenBy(x => x.p.Name),
|
|
("quantity", false) => q.OrderBy(x => x.s.Quantity).ThenBy(x => x.p.Name),
|
|
("quantity", true) => q.OrderByDescending(x => x.s.Quantity).ThenBy(x => x.p.Name),
|
|
("reserved", false) => q.OrderBy(x => x.s.ReservedQuantity).ThenBy(x => x.p.Name),
|
|
("reserved", true) => q.OrderByDescending(x => x.s.ReservedQuantity).ThenBy(x => x.p.Name),
|
|
("available", false) => q.OrderBy(x => x.s.Quantity - x.s.ReservedQuantity).ThenBy(x => x.p.Name),
|
|
("available", true) => q.OrderByDescending(x => x.s.Quantity - x.s.ReservedQuantity).ThenBy(x => x.p.Name),
|
|
("name", true) => q.OrderByDescending(x => x.p.Name),
|
|
_ => q.OrderBy(x => x.p.Name),
|
|
};
|
|
var items = await q
|
|
.Skip((page - 1) * pageSize).Take(pageSize)
|
|
.Select(x => new StockRow(
|
|
x.p.Id, x.p.Name, x.p.Article, x.u.Name,
|
|
x.st.Id, x.st.Name,
|
|
x.s.Quantity, x.s.ReservedQuantity, x.s.Quantity - x.s.ReservedQuantity))
|
|
.ToListAsync(ct);
|
|
|
|
return new PagedResult<StockRow> { Items = items, Total = total, Page = page, PageSize = pageSize };
|
|
}
|
|
|
|
public record MovementRow(
|
|
Guid Id, DateTime OccurredAt,
|
|
Guid ProductId, string ProductName, string? Article,
|
|
Guid StoreId, string StoreName,
|
|
decimal Quantity, decimal? UnitCost,
|
|
string Type, string DocumentType, Guid? DocumentId, string? DocumentNumber,
|
|
string? Notes);
|
|
|
|
[HttpGet("movements")]
|
|
public async Task<ActionResult<PagedResult<MovementRow>>> GetMovements(
|
|
[FromQuery] Guid? storeId,
|
|
[FromQuery] Guid? productId,
|
|
[FromQuery] DateTime? from,
|
|
[FromQuery] DateTime? to,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 50,
|
|
[FromQuery] string? sort = null,
|
|
[FromQuery] string? order = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var q = from m in _db.StockMovements
|
|
join p in _db.Products on m.ProductId equals p.Id
|
|
join st in _db.Stores on m.StoreId equals st.Id
|
|
select new { m, p, st };
|
|
|
|
if (storeId.HasValue) q = q.Where(x => x.m.StoreId == storeId.Value);
|
|
if (productId.HasValue) q = q.Where(x => x.m.ProductId == productId.Value);
|
|
if (from.HasValue) q = q.Where(x => x.m.OccurredAt >= from.Value);
|
|
if (to.HasValue) q = q.Where(x => x.m.OccurredAt < to.Value);
|
|
|
|
var total = await q.CountAsync(ct);
|
|
var desc = string.Equals(order, "desc", StringComparison.OrdinalIgnoreCase);
|
|
q = (sort, desc) switch
|
|
{
|
|
("occurredAt", false) => q.OrderBy(x => x.m.OccurredAt),
|
|
("occurredAt", true) => q.OrderByDescending(x => x.m.OccurredAt),
|
|
("product", false) => q.OrderBy(x => x.p.Name),
|
|
("product", true) => q.OrderByDescending(x => x.p.Name),
|
|
("store", false) => q.OrderBy(x => x.st.Name).ThenByDescending(x => x.m.OccurredAt),
|
|
("store", true) => q.OrderByDescending(x => x.st.Name).ThenByDescending(x => x.m.OccurredAt),
|
|
("quantity", false) => q.OrderBy(x => x.m.Quantity).ThenByDescending(x => x.m.OccurredAt),
|
|
("quantity", true) => q.OrderByDescending(x => x.m.Quantity).ThenByDescending(x => x.m.OccurredAt),
|
|
("type", false) => q.OrderBy(x => x.m.Type).ThenByDescending(x => x.m.OccurredAt),
|
|
("type", true) => q.OrderByDescending(x => x.m.Type).ThenByDescending(x => x.m.OccurredAt),
|
|
("documentType", false) => q.OrderBy(x => x.m.DocumentType).ThenByDescending(x => x.m.OccurredAt),
|
|
("documentType", true) => q.OrderByDescending(x => x.m.DocumentType).ThenByDescending(x => x.m.OccurredAt),
|
|
_ => q.OrderByDescending(x => x.m.OccurredAt),
|
|
};
|
|
var items = await q
|
|
.Skip((page - 1) * pageSize).Take(pageSize)
|
|
.Select(x => new MovementRow(
|
|
x.m.Id, x.m.OccurredAt,
|
|
x.p.Id, x.p.Name, x.p.Article,
|
|
x.st.Id, x.st.Name,
|
|
x.m.Quantity, x.m.UnitCost,
|
|
x.m.Type.ToString(), x.m.DocumentType, x.m.DocumentId, x.m.DocumentNumber,
|
|
x.m.Notes))
|
|
.ToListAsync(ct);
|
|
|
|
return new PagedResult<MovementRow> { Items = items, Total = total, Page = page, PageSize = pageSize };
|
|
}
|
|
}
|