refactor(units): drop Description, hide Code from non-SuperAdmin UI
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 1m24s
CI / Web (React + Vite) (push) Successful in 42s
Docker API / Build + push API (push) Successful in 1m33s
Docker Web / Build + push Web (push) Successful in 37s
Docker API / Deploy API on stage (push) Successful in 18s
Docker Web / Deploy Web on stage (push) Successful in 12s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 1m24s
CI / Web (React + Vite) (push) Successful in 42s
Docker API / Build + push API (push) Successful in 1m33s
Docker Web / Build + push Web (push) Successful in 37s
Docker API / Deploy API on stage (push) Successful in 18s
Docker Web / Deploy Web on stage (push) Successful in 12s
Description у пяти канонических ОКЕИ-единиц никогда не заполнялось ни UI,
ни импортом, ни сидером — выкидываем поле полностью (Domain → EF-config
→ DTO → Input → frontend types → Super-Admin форма). Migration
Phase5d_DropUnitOfMeasureDescription дропает колонку.
Code оставляем в БД (нужен для интеграций МойСклад/1С), но скрываем от
org Admin'а:
- /catalog/units-of-measure — только колонки Name + кнопка toggle, без
Code и Description; поиск/сортировка только по Name.
- /super-admin/units-of-measure — Code продолжает показываться в таблице
и форме редактирования.
Дропдаун единиц в ProductEditPage / ProductQuickCreateModal уже отдаёт
только {u.name} в options, проверено. На SupplyEditPage/RetailSaleEditPage
в строках документа отображается unitName, Code не показывался — без
изменений.
This commit is contained in:
parent
37cd9aa94b
commit
bf53629092
|
|
@ -69,7 +69,7 @@ public UnitsOfMeasureController(AppDbContext db, ITenantContext tenant)
|
||||||
var items = await q
|
var items = await q
|
||||||
.Skip(req.Skip).Take(req.Take)
|
.Skip(req.Skip).Take(req.Take)
|
||||||
.Select(u => new UnitOfMeasureDto(
|
.Select(u => new UnitOfMeasureDto(
|
||||||
u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, true))
|
u.Id, u.Code, u.Name, u.OrganizationId, u.IsActive, true))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
return new PagedResult<UnitOfMeasureDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
return new PagedResult<UnitOfMeasureDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ public async Task<ActionResult<UnitOfMeasureDto>> Get(Guid id, CancellationToken
|
||||||
var enabled = !orgId.HasValue || await _db.OrgUnitsOfMeasure
|
var enabled = !orgId.HasValue || await _db.OrgUnitsOfMeasure
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.AnyAsync(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == id, ct);
|
.AnyAsync(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == id, ct);
|
||||||
return new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, enabled);
|
return new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.OrganizationId, u.IsActive, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Включить global для текущей орги. Идемпотентно: повторный
|
/// <summary>Включить global для текущей орги. Идемпотентно: повторный
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class SuperAdminUnitsOfMeasureController : ControllerBase
|
||||||
var items = await q
|
var items = await q
|
||||||
.Skip(req.Skip).Take(req.Take)
|
.Skip(req.Skip).Take(req.Take)
|
||||||
.Select(u => new UnitOfMeasureDto(
|
.Select(u => new UnitOfMeasureDto(
|
||||||
u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, true))
|
u.Id, u.Code, u.Name, u.OrganizationId, u.IsActive, true))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
return new PagedResult<UnitOfMeasureDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
return new PagedResult<UnitOfMeasureDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +61,7 @@ public async Task<ActionResult<UnitOfMeasureDto>> Get(Guid id, CancellationToken
|
||||||
.FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == null, ct);
|
.FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == null, ct);
|
||||||
return u is null
|
return u is null
|
||||||
? NotFound()
|
? NotFound()
|
||||||
: new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, true);
|
: new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.OrganizationId, u.IsActive, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|
@ -81,13 +81,12 @@ public async Task<ActionResult<UnitOfMeasureDto>> Create([FromBody] UnitOfMeasur
|
||||||
OrganizationId = null,
|
OrganizationId = null,
|
||||||
Code = input.Code.Trim(),
|
Code = input.Code.Trim(),
|
||||||
Name = input.Name.Trim(),
|
Name = input.Name.Trim(),
|
||||||
Description = input.Description,
|
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
};
|
};
|
||||||
_db.UnitsOfMeasure.Add(e);
|
_db.UnitsOfMeasure.Add(e);
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
return CreatedAtAction(nameof(Get), new { id = e.Id },
|
||||||
new UnitOfMeasureDto(e.Id, e.Code, e.Name, e.Description, e.OrganizationId, e.IsActive, true));
|
new UnitOfMeasureDto(e.Id, e.Code, e.Name, e.OrganizationId, e.IsActive, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
|
|
@ -110,7 +109,6 @@ public async Task<IActionResult> Update(Guid id, [FromBody] UnitOfMeasureInput i
|
||||||
|
|
||||||
e.Code = input.Code.Trim();
|
e.Code = input.Code.Trim();
|
||||||
e.Name = input.Name.Trim();
|
e.Name = input.Name.Trim();
|
||||||
e.Description = input.Description;
|
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public record CountryDto(
|
||||||
public record CurrencyDto(Guid Id, string Code, string Name, string Symbol);
|
public record CurrencyDto(Guid Id, string Code, string Name, string Symbol);
|
||||||
|
|
||||||
public record UnitOfMeasureDto(
|
public record UnitOfMeasureDto(
|
||||||
Guid Id, string Code, string Name, string? Description, Guid? OrganizationId,
|
Guid Id, string Code, string Name, Guid? OrganizationId,
|
||||||
bool IsActive = true, bool IsEnabledForOrg = true);
|
bool IsActive = true, bool IsEnabledForOrg = true);
|
||||||
|
|
||||||
public record PriceTypeDto(
|
public record PriceTypeDto(
|
||||||
|
|
@ -62,7 +62,7 @@ public record CountryInput(
|
||||||
string Code, string Name,
|
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);
|
public record CurrencyInput(string Code, string Name, string Symbol);
|
||||||
public record UnitOfMeasureInput(string Code, string Name, string? Description = null);
|
public record UnitOfMeasureInput(string Code, string Name);
|
||||||
public record PriceTypeInput(
|
public record PriceTypeInput(
|
||||||
string Name, bool IsRequired = false,
|
string Name, bool IsRequired = false,
|
||||||
bool IsRetail = false, int SortOrder = 0);
|
bool IsRetail = false, int SortOrder = 0);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,5 @@ public class UnitOfMeasure : Entity, IOptionalTenantEntity
|
||||||
public Guid? OrganizationId { get; set; }
|
public Guid? OrganizationId { get; set; }
|
||||||
public string Code { get; set; } = null!; // ОКЕИ код: "796" (шт), "166" (кг), "112" (л)
|
public string Code { get; set; } = null!; // ОКЕИ код: "796" (шт), "166" (кг), "112" (л)
|
||||||
public string Name { get; set; } = null!; // "штука", "килограмм", "литр"
|
public string Name { get; set; } = null!; // "штука", "килограмм", "литр"
|
||||||
public string? Description { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ private static void ConfigureUnit(EntityTypeBuilder<UnitOfMeasure> b)
|
||||||
b.ToTable("units_of_measure");
|
b.ToTable("units_of_measure");
|
||||||
b.Property(x => x.Code).HasMaxLength(10).IsRequired();
|
b.Property(x => x.Code).HasMaxLength(10).IsRequired();
|
||||||
b.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
b.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
||||||
b.Property(x => x.Description).HasMaxLength(500);
|
|
||||||
b.Property(x => x.IsActive).HasDefaultValue(true);
|
b.Property(x => x.IsActive).HasDefaultValue(true);
|
||||||
// Phase5c: после миграции OrganizationId всегда NULL — глобальный справочник.
|
// Phase5c: после миграции OrganizationId всегда NULL — глобальный справочник.
|
||||||
// Уникальность по Code только среди активных, чтобы можно было
|
// Уникальность по Code только среди активных, чтобы можно было
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using foodmarket.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>Phase5d — выкидываем UnitOfMeasure.Description: для пяти
|
||||||
|
/// канонических ОКЕИ-единиц («штука», «кг», ...) нечего описывать,
|
||||||
|
/// поле никогда не заполнялось ни UI, ни импортом, ни сидером.
|
||||||
|
/// Code остаётся (нужен для интеграций МойСклад/1С), но скрыт в UI
|
||||||
|
/// от org Admin'а.</summary>
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260508100000_Phase5d_DropUnitOfMeasureDescription")]
|
||||||
|
public partial class Phase5d_DropUnitOfMeasureDescription : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder b)
|
||||||
|
{
|
||||||
|
b.DropColumn(
|
||||||
|
name: "Description",
|
||||||
|
schema: "public",
|
||||||
|
table: "units_of_measure");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder b)
|
||||||
|
{
|
||||||
|
b.AddColumn<string>(
|
||||||
|
name: "Description",
|
||||||
|
schema: "public",
|
||||||
|
table: "units_of_measure",
|
||||||
|
type: "character varying(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ export interface Country {
|
||||||
}
|
}
|
||||||
export interface Currency { id: string; code: string; name: string; symbol: string }
|
export interface Currency { id: string; code: string; name: string; symbol: string }
|
||||||
export interface UnitOfMeasure {
|
export interface UnitOfMeasure {
|
||||||
id: string; code: string; name: string; description: string | null; organizationId: string | null;
|
id: string; code: string; name: string; organizationId: string | null;
|
||||||
isActive: boolean; isEnabledForOrg: boolean
|
isActive: boolean; isEnabledForOrg: boolean
|
||||||
}
|
}
|
||||||
export interface PriceType { id: string; name: string; isRequired: boolean; isSystem: boolean; isRetail: boolean; sortOrder: number }
|
export interface PriceType { id: string; name: string; isRequired: boolean; isSystem: boolean; isRetail: boolean; sortOrder: number }
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,9 @@ interface Form {
|
||||||
id?: string
|
id?: string
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const blank: Form = { code: '', name: '', description: '' }
|
const blank: Form = { code: '', name: '' }
|
||||||
|
|
||||||
export function SuperAdminUnitsOfMeasurePage() {
|
export function SuperAdminUnitsOfMeasurePage() {
|
||||||
const list = useCatalogList<UnitOfMeasure>(URL)
|
const list = useCatalogList<UnitOfMeasure>(URL)
|
||||||
|
|
@ -80,12 +79,11 @@ export function SuperAdminUnitsOfMeasurePage() {
|
||||||
onSortChange={list.setSort}
|
onSortChange={list.setSort}
|
||||||
onRowClick={(r) => {
|
onRowClick={(r) => {
|
||||||
setSubmitError(null)
|
setSubmitError(null)
|
||||||
setForm({ id: r.id, code: r.code, name: r.name, description: r.description ?? '' })
|
setForm({ id: r.id, code: r.code, name: r.name })
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'Код', width: '90px', sortKey: 'code', cell: (r) => <span className="font-mono">{r.code}</span> },
|
{ header: 'Код', width: '90px', sortKey: 'code', cell: (r) => <span className="font-mono">{r.code}</span> },
|
||||||
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
|
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
|
||||||
{ header: 'Описание', cell: (r) => r.description ?? '—' },
|
|
||||||
{
|
{
|
||||||
header: 'Статус',
|
header: 'Статус',
|
||||||
width: '110px',
|
width: '110px',
|
||||||
|
|
@ -121,9 +119,6 @@ export function SuperAdminUnitsOfMeasurePage() {
|
||||||
<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>
|
||||||
<Field label="Описание">
|
|
||||||
<TextInput value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
|
||||||
</Field>
|
|
||||||
{submitError && (
|
{submitError && (
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{submitError}</p>
|
<p className="text-sm text-red-600 dark:text-red-400">{submitError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,10 @@ export function UnitsOfMeasurePage() {
|
||||||
sortOrder={list.sortOrder}
|
sortOrder={list.sortOrder}
|
||||||
onSortChange={list.setSort}
|
onSortChange={list.setSort}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'Код', width: '90px', sortKey: 'code', cell: (r) => <span className="font-mono">{r.code}</span> },
|
// Колонку «Код» (ОКЕИ 796/166/...) не показываем org-юзеру: код нужен
|
||||||
|
// только для интеграций (МойСклад/1С) и виден SuperAdmin'у на
|
||||||
|
// /super-admin/units. Здесь админ орги выбирает по понятному имени.
|
||||||
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
|
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
|
||||||
{ header: 'Описание', cell: (r) => r.description ?? '—' },
|
|
||||||
{
|
{
|
||||||
header: 'Для орги',
|
header: 'Для орги',
|
||||||
width: '120px',
|
width: '120px',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue