refactor(countries): drop SortOrder, sort by Name, auto-width columns
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 40s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 18s

- Country.SortOrder удалено из домена/DTO/API/seeder/web/UI.
- Миграция Phase5b_DropCountrySortOrder дропает колонку.
- Список стран сортируется по Name ASC.
- В форме: поле «Порядок» убрано.
- В таблице: убрана колонка «Порядок», ширины колонок сжаты по
  содержимому (Код 80px, Валюта 120px, НДС 100px, Название flex).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-24 12:14:01 +05:00
parent 51bef16758
commit 4f4df4a715
9 changed files with 1928 additions and 40 deletions

View file

@ -28,10 +28,10 @@ public async Task<ActionResult<PagedResult<CountryDto>>> List([FromQuery] PagedR
}
var total = await q.CountAsync(ct);
var items = await q
.OrderBy(c => c.SortOrder).ThenBy(c => c.Name)
.OrderBy(c => c.Name)
.Skip(req.Skip).Take(req.Take)
.Select(c => new CountryDto(
c.Id, c.Code, c.Name, c.SortOrder,
c.Id, c.Code, c.Name,
c.DefaultCurrencyId,
c.DefaultCurrency != null ? c.DefaultCurrency.Code : null,
c.DefaultCurrency != null ? c.DefaultCurrency.Symbol : null,
@ -54,7 +54,6 @@ public async Task<ActionResult<CountryDto>> Create([FromBody] CountryInput input
{
Code = input.Code.Trim().ToUpper(),
Name = input.Name,
SortOrder = input.SortOrder,
DefaultCurrencyId = input.DefaultCurrencyId,
VatRate = input.VatRate,
};
@ -71,7 +70,6 @@ public async Task<IActionResult> Update(Guid id, [FromBody] CountryInput input,
if (e is null) return NotFound();
e.Code = input.Code.Trim().ToUpper();
e.Name = input.Name;
e.SortOrder = input.SortOrder;
e.DefaultCurrencyId = input.DefaultCurrencyId;
e.VatRate = input.VatRate;
await _db.SaveChangesAsync(ct);
@ -89,6 +87,6 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
}
private static CountryDto Project(Country c) => new(
c.Id, c.Code, c.Name, c.SortOrder,
c.Id, c.Code, c.Name,
c.DefaultCurrencyId, c.DefaultCurrency?.Code, c.DefaultCurrency?.Symbol, c.VatRate);
}

View file

@ -31,22 +31,22 @@ public async Task StartAsync(CancellationToken ct)
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
private record CountrySeed(string Code, string Name, int SortOrder, string CurrencyCode, decimal VatRate);
private record CountrySeed(string Code, string Name, string CurrencyCode, decimal VatRate);
private static readonly CountrySeed[] CountrySeeds =
{
new("KZ", "Казахстан", 1, "KZT", 16m),
new("RU", "Россия", 2, "RUB", 20m),
new("CN", "Китай", 3, "CNY", 13m),
new("TR", "Турция", 4, "TRY", 18m),
new("BY", "Беларусь", 5, "BYN", 20m),
new("UZ", "Узбекистан", 6, "UZS", 12m),
new("KG", "Кыргызстан", 7, "KGS", 12m),
new("DE", "Германия", 10, "EUR", 19m),
new("US", "США", 11, "USD", 0m),
new("KR", "Южная Корея", 12, "KRW", 10m),
new("IT", "Италия", 13, "EUR", 22m),
new("PL", "Польша", 14, "PLN", 23m),
new("KZ", "Казахстан", "KZT", 16m),
new("RU", "Россия", "RUB", 20m),
new("CN", "Китай", "CNY", 13m),
new("TR", "Турция", "TRY", 18m),
new("BY", "Беларусь", "BYN", 20m),
new("UZ", "Узбекистан", "UZS", 12m),
new("KG", "Кыргызстан", "KGS", 12m),
new("DE", "Германия", "EUR", 19m),
new("US", "США", "USD", 0m),
new("KR", "Южная Корея", "KRW", 10m),
new("IT", "Италия", "EUR", 22m),
new("PL", "Польша", "PLN", 23m),
};
private static readonly Currency[] CurrencySeeds =
@ -83,7 +83,7 @@ private static async Task SeedCountriesAsync(AppDbContext db, CancellationToken
{
if (!existingCodes.Contains(s.Code))
{
db.Countries.Add(new Country { Code = s.Code, Name = s.Name, SortOrder = s.SortOrder, VatRate = s.VatRate });
db.Countries.Add(new Country { Code = s.Code, Name = s.Name, VatRate = s.VatRate });
}
}
}

View file

@ -3,7 +3,7 @@
namespace foodmarket.Application.Catalog;
public record CountryDto(
Guid Id, string Code, string Name, int SortOrder,
Guid Id, string Code, string Name,
Guid? DefaultCurrencyId, string? DefaultCurrencyCode, string? DefaultCurrencySymbol,
decimal VatRate);
@ -53,7 +53,7 @@ public record ProductDto(
// Upsert payloads (input)
public record CountryInput(
string Code, string Name, int SortOrder = 0,
string Code, string Name,
Guid? DefaultCurrencyId = null, decimal VatRate = 0m);
public record CurrencyInput(string Code, string Name, string Symbol, int MinorUnit = 2, bool IsActive = true);
public record UnitOfMeasureInput(string Code, string Name, string? Description = null, bool IsActive = true);

View file

@ -7,7 +7,6 @@ public class Country : Entity
{
public string Code { get; set; } = null!; // ISO 3166-1 alpha-2, e.g. "KZ"
public string Name { get; set; } = null!;
public int SortOrder { get; set; }
/// <summary>Валюта страны — при выборе страны в настройках организации
/// она становится валютой по умолчанию для этой организации.</summary>
public Guid? DefaultCurrencyId { get; set; }

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace foodmarket.Infrastructure.Persistence.Migrations
{
/// <summary>Удаляет countries.SortOrder — порядок страны больше не редактируется руками,
/// сортировка в UI и API идёт по Name ASC.</summary>
public partial class Phase5b_DropCountrySortOrder : Migration
{
protected override void Up(MigrationBuilder b)
{
b.DropColumn(name: "SortOrder", schema: "public", table: "countries");
}
protected override void Down(MigrationBuilder b)
{
b.AddColumn<int>(
name: "SortOrder", schema: "public", table: "countries",
type: "integer", nullable: false, defaultValue: 0);
}
}
}

View file

@ -442,9 +442,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");

View file

@ -21,7 +21,7 @@ export const packagingLabel: Record<Packaging, string> = {
}
export interface Country {
id: string; code: string; name: string; sortOrder: number
id: string; code: string; name: string
defaultCurrencyId: string | null
defaultCurrencyCode: string | null
defaultCurrencySymbol: string | null

View file

@ -17,12 +17,11 @@ interface Form {
id?: string
code: string
name: string
sortOrder: number
defaultCurrencyId: string | null
vatRate: number
}
const blank: Form = { code: '', name: '', sortOrder: 0, defaultCurrencyId: null, vatRate: 0 }
const blank: Form = { code: '', name: '', defaultCurrencyId: null, vatRate: 0 }
export function CountriesPage() {
const { data, isLoading, page, setPage, search, setSearch } = useCatalogList<Country>(URL)
@ -58,15 +57,14 @@ export function CountriesPage() {
isLoading={isLoading}
rowKey={(r) => r.id}
onRowClick={(r) => setForm({
id: r.id, code: r.code, name: r.name, sortOrder: r.sortOrder,
id: r.id, code: r.code, name: r.name,
defaultCurrencyId: r.defaultCurrencyId, vatRate: r.vatRate,
})}
columns={[
{ header: 'Код', width: '90px', cell: (r) => <span className="font-mono">{r.code}</span> },
{ header: 'Код', width: '80px', cell: (r) => <span className="font-mono">{r.code}</span> },
{ header: 'Название', cell: (r) => r.name },
{ header: 'Валюта', width: '130px', cell: (r) => r.defaultCurrencyCode ? `${r.defaultCurrencyCode} ${r.defaultCurrencySymbol ?? ''}` : '—' },
{ header: 'Валюта', width: '120px', cell: (r) => r.defaultCurrencyCode ? `${r.defaultCurrencyCode} ${r.defaultCurrencySymbol ?? ''}` : '—' },
{ header: 'НДС', width: '100px', className: 'text-right', cell: (r) => `${r.vatRate.toFixed(2)}%` },
{ header: 'Порядок', width: '100px', className: 'text-right', cell: (r) => r.sortOrder },
]}
/>
</ListPageShell>
@ -94,14 +92,9 @@ export function CountriesPage() {
>
{form && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<Field label="ISO-код (2 буквы)">
<TextInput value={form.code} maxLength={2} onChange={(e) => setForm({ ...form, code: e.target.value.toUpperCase() })} />
</Field>
<Field label="Порядок">
<TextInput type="number" value={form.sortOrder} onChange={(e) => setForm({ ...form, sortOrder: Number(e.target.value) })} />
</Field>
</div>
<Field label="Название">
<TextInput value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
</Field>