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 total = await q.CountAsync(ct);
var items = await q var items = await q
.OrderBy(c => c.SortOrder).ThenBy(c => c.Name) .OrderBy(c => c.Name)
.Skip(req.Skip).Take(req.Take) .Skip(req.Skip).Take(req.Take)
.Select(c => new CountryDto( .Select(c => new CountryDto(
c.Id, c.Code, c.Name, c.SortOrder, c.Id, c.Code, c.Name,
c.DefaultCurrencyId, c.DefaultCurrencyId,
c.DefaultCurrency != null ? c.DefaultCurrency.Code : null, c.DefaultCurrency != null ? c.DefaultCurrency.Code : null,
c.DefaultCurrency != null ? c.DefaultCurrency.Symbol : 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(), Code = input.Code.Trim().ToUpper(),
Name = input.Name, Name = input.Name,
SortOrder = input.SortOrder,
DefaultCurrencyId = input.DefaultCurrencyId, DefaultCurrencyId = input.DefaultCurrencyId,
VatRate = input.VatRate, VatRate = input.VatRate,
}; };
@ -71,7 +70,6 @@ public async Task<IActionResult> Update(Guid id, [FromBody] CountryInput input,
if (e is null) return NotFound(); if (e is null) return NotFound();
e.Code = input.Code.Trim().ToUpper(); e.Code = input.Code.Trim().ToUpper();
e.Name = input.Name; e.Name = input.Name;
e.SortOrder = input.SortOrder;
e.DefaultCurrencyId = input.DefaultCurrencyId; e.DefaultCurrencyId = input.DefaultCurrencyId;
e.VatRate = input.VatRate; e.VatRate = input.VatRate;
await _db.SaveChangesAsync(ct); await _db.SaveChangesAsync(ct);
@ -89,6 +87,6 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
} }
private static CountryDto Project(Country c) => new( 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); 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; 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 = private static readonly CountrySeed[] CountrySeeds =
{ {
new("KZ", "Казахстан", 1, "KZT", 16m), new("KZ", "Казахстан", "KZT", 16m),
new("RU", "Россия", 2, "RUB", 20m), new("RU", "Россия", "RUB", 20m),
new("CN", "Китай", 3, "CNY", 13m), new("CN", "Китай", "CNY", 13m),
new("TR", "Турция", 4, "TRY", 18m), new("TR", "Турция", "TRY", 18m),
new("BY", "Беларусь", 5, "BYN", 20m), new("BY", "Беларусь", "BYN", 20m),
new("UZ", "Узбекистан", 6, "UZS", 12m), new("UZ", "Узбекистан", "UZS", 12m),
new("KG", "Кыргызстан", 7, "KGS", 12m), new("KG", "Кыргызстан", "KGS", 12m),
new("DE", "Германия", 10, "EUR", 19m), new("DE", "Германия", "EUR", 19m),
new("US", "США", 11, "USD", 0m), new("US", "США", "USD", 0m),
new("KR", "Южная Корея", 12, "KRW", 10m), new("KR", "Южная Корея", "KRW", 10m),
new("IT", "Италия", 13, "EUR", 22m), new("IT", "Италия", "EUR", 22m),
new("PL", "Польша", 14, "PLN", 23m), new("PL", "Польша", "PLN", 23m),
}; };
private static readonly Currency[] CurrencySeeds = private static readonly Currency[] CurrencySeeds =
@ -83,7 +83,7 @@ private static async Task SeedCountriesAsync(AppDbContext db, CancellationToken
{ {
if (!existingCodes.Contains(s.Code)) 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; namespace foodmarket.Application.Catalog;
public record CountryDto( public record CountryDto(
Guid Id, string Code, string Name, int SortOrder, Guid Id, string Code, string Name,
Guid? DefaultCurrencyId, string? DefaultCurrencyCode, string? DefaultCurrencySymbol, Guid? DefaultCurrencyId, string? DefaultCurrencyCode, string? DefaultCurrencySymbol,
decimal VatRate); decimal VatRate);
@ -53,7 +53,7 @@ public record ProductDto(
// Upsert payloads (input) // Upsert payloads (input)
public record CountryInput( public record CountryInput(
string Code, string Name, int SortOrder = 0, string Code, string Name,
Guid? DefaultCurrencyId = null, decimal VatRate = 0m); Guid? DefaultCurrencyId = null, decimal VatRate = 0m);
public record CurrencyInput(string Code, string Name, string Symbol, int MinorUnit = 2, bool IsActive = true); 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); 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 Code { get; set; } = null!; // ISO 3166-1 alpha-2, e.g. "KZ"
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
public int SortOrder { get; set; }
/// <summary>Валюта страны — при выборе страны в настройках организации /// <summary>Валюта страны — при выборе страны в настройках организации
/// она становится валютой по умолчанию для этой организации.</summary> /// она становится валютой по умолчанию для этой организации.</summary>
public Guid? DefaultCurrencyId { get; set; } 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) .HasMaxLength(100)
.HasColumnType("character varying(100)"); .HasColumnType("character varying(100)");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");

View file

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

View file

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