food-market/src/food-market.api/Controllers/Catalog/CountriesController.cs
nns 5a06e15924 feat(tables): server-side sort by column header click
Во всех таблицах можно сортировать по клику на заголовок столбца:
первый клик — по возрастанию (↑), второй — по убыванию (↓),
смена колонки сбрасывает предыдущую. Без активной сортировки —
серверный 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>
2026-04-24 12:24:05 +05:00

103 lines
4.2 KiB
C#

using foodmarket.Application.Catalog;
using foodmarket.Application.Common;
using foodmarket.Domain.Catalog;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers.Catalog;
[ApiController]
[Authorize]
[Route("api/catalog/countries")]
public class CountriesController : ControllerBase
{
private readonly AppDbContext _db;
public CountriesController(AppDbContext db) => _db = db;
[HttpGet]
public async Task<ActionResult<PagedResult<CountryDto>>> List([FromQuery] PagedRequest req, CancellationToken ct)
{
var q = _db.Countries.Include(c => c.DefaultCurrency).AsNoTracking().AsQueryable();
if (!string.IsNullOrWhiteSpace(req.Search))
{
var s = req.Search.Trim().ToLower();
q = q.Where(c => c.Name.ToLower().Contains(s) || c.Code.ToLower().Contains(s));
}
var total = await q.CountAsync(ct);
q = (req.Sort, req.Desc) switch
{
("code", false) => q.OrderBy(c => c.Code),
("code", true) => q.OrderByDescending(c => c.Code),
("currency", false) => q.OrderBy(c => c.DefaultCurrency != null ? c.DefaultCurrency.Code : null).ThenBy(c => c.Name),
("currency", true) => q.OrderByDescending(c => c.DefaultCurrency != null ? c.DefaultCurrency.Code : null).ThenBy(c => c.Name),
("vatRate", false) => q.OrderBy(c => c.VatRate).ThenBy(c => c.Name),
("vatRate", true) => q.OrderByDescending(c => c.VatRate).ThenBy(c => c.Name),
("name", true) => q.OrderByDescending(c => c.Name),
_ => q.OrderBy(c => c.Name),
};
var items = await q
.Skip(req.Skip).Take(req.Take)
.Select(c => new CountryDto(
c.Id, c.Code, c.Name,
c.DefaultCurrencyId,
c.DefaultCurrency != null ? c.DefaultCurrency.Code : null,
c.DefaultCurrency != null ? c.DefaultCurrency.Symbol : null,
c.VatRate))
.ToListAsync(ct);
return new PagedResult<CountryDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<CountryDto>> Get(Guid id, CancellationToken ct)
{
var c = await _db.Countries.Include(x => x.DefaultCurrency).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
return c is null ? NotFound() : Project(c);
}
[HttpPost, Authorize(Roles = "SuperAdmin,Admin")]
public async Task<ActionResult<CountryDto>> Create([FromBody] CountryInput input, CancellationToken ct)
{
var e = new Country
{
Code = input.Code.Trim().ToUpper(),
Name = input.Name,
DefaultCurrencyId = input.DefaultCurrencyId,
VatRate = input.VatRate,
};
_db.Countries.Add(e);
await _db.SaveChangesAsync(ct);
await _db.Entry(e).Reference(x => x.DefaultCurrency).LoadAsync(ct);
return CreatedAtAction(nameof(Get), new { id = e.Id }, Project(e));
}
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] CountryInput input, CancellationToken ct)
{
var e = await _db.Countries.FirstOrDefaultAsync(x => x.Id == id, ct);
if (e is null) return NotFound();
e.Code = input.Code.Trim().ToUpper();
e.Name = input.Name;
e.DefaultCurrencyId = input.DefaultCurrencyId;
e.VatRate = input.VatRate;
await _db.SaveChangesAsync(ct);
return NoContent();
}
[HttpDelete("{id:guid}"), Authorize(Roles = "SuperAdmin")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var e = await _db.Countries.FirstOrDefaultAsync(x => x.Id == id, ct);
if (e is null) return NotFound();
_db.Countries.Remove(e);
await _db.SaveChangesAsync(ct);
return NoContent();
}
private static CountryDto Project(Country c) => new(
c.Id, c.Code, c.Name,
c.DefaultCurrencyId, c.DefaultCurrency?.Code, c.DefaultCurrency?.Symbol, c.VatRate);
}