feat(product-card): drop ShelfLifeDays + recompose classification + auto-article + barcode trash hide
- Удаление поля «Срок годности (дней)»:
• Domain.Product.ShelfLifeDays убран,
• миграция Phase3b_DropProductShelfLifeDays — DROP COLUMN,
• DTO/Input/UI/фильтр в списке товаров — выпилены.
- Перекомпоновка секции «Классификация» в карточке товара:
• ряд 1 (3 col): Группа * | Единица измерения * | Фасовка,
• ряд 2 (2 col): Основной поставщик | Страна происхождения,
• Страна происхождения видна только если включена настройка
Organization.ShowCountryOfOriginOnProduct (default false).
• Та же миграция добавляет колонку, в OrganizationSettingsPage
появляется галка «Показывать «Страну происхождения» на товаре»
с подсказкой про импорт.
- Артикул теперь обязательное поле с авто-генерацией:
• ProductEditPage: метка «Артикул *», required,
• генератор generateArticle() (timestamp[-6] + 3 random) — у нового
товара поле сразу заполнено,
• canSave требует непустой article. Уникальность подтверждает
сервер (он также имеет свой fallback-генератор max+1).
- Иконка корзины в секции «Штрихкоды» рендерится только при
form.barcodes.length > 1 — для единственной строки удаления нет
(минимум 1 штрихкод обязателен, удалять единственный нельзя).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fea3498b8b
commit
eaf5b7399b
|
|
@ -115,8 +115,6 @@ private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
|
|||
[FromQuery] decimal? purchasePriceTo,
|
||||
[FromQuery] decimal? referencePriceFrom,
|
||||
[FromQuery] decimal? referencePriceTo,
|
||||
[FromQuery] int? shelfLifeDaysFrom,
|
||||
[FromQuery] int? shelfLifeDaysTo,
|
||||
[FromQuery] decimal? systemPriceFrom,
|
||||
[FromQuery] decimal? systemPriceTo,
|
||||
CancellationToken ct)
|
||||
|
|
@ -146,8 +144,6 @@ private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
|
|||
var refTo = referencePriceTo ?? purchasePriceTo;
|
||||
if (refFrom is not null) q = q.Where(p => p.ReferencePrice >= refFrom);
|
||||
if (refTo is not null) q = q.Where(p => p.ReferencePrice <= refTo);
|
||||
if (shelfLifeDaysFrom is not null) q = q.Where(p => p.ShelfLifeDays >= shelfLifeDaysFrom);
|
||||
if (shelfLifeDaysTo is not null) q = q.Where(p => p.ShelfLifeDays <= shelfLifeDaysTo);
|
||||
// Фильтр по системной (главной розничной) цене — берём Prices c PriceType.IsSystem=true.
|
||||
if (systemPriceFrom is not null)
|
||||
q = q.Where(p => p.Prices.Any(pr => pr.PriceType!.IsSystem && pr.Amount >= systemPriceFrom));
|
||||
|
|
@ -421,7 +417,7 @@ public async Task<ActionResult<IReadOnlyList<BarcodeDuplicate>>> BarcodeDuplicat
|
|||
p.ReferencePrice, p.ReferencePriceUpdatedAt,
|
||||
p.PurchaseCurrencyId, p.PurchaseCurrency != null ? p.PurchaseCurrency.Code : null,
|
||||
p.Cost, p.LastSupplyAt,
|
||||
p.ImageUrl, p.ShelfLifeDays,
|
||||
p.ImageUrl,
|
||||
p.Prices.Select(pr => new ProductPriceDto(pr.Id, pr.PriceTypeId, pr.PriceType!.Name, pr.Amount, pr.CurrencyId, pr.Currency!.Code)).ToList(),
|
||||
p.Barcodes.Select(b => new ProductBarcodeDto(b.Id, b.Code, b.Type, b.IsPrimary)).ToList());
|
||||
|
||||
|
|
@ -451,6 +447,5 @@ private static void Apply(Product e, ProductInput i)
|
|||
}
|
||||
e.PurchaseCurrencyId = i.PurchaseCurrencyId;
|
||||
e.ImageUrl = i.ImageUrl;
|
||||
e.ShelfLifeDays = i.ShelfLifeDays;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ public record OrgSettingsDto(
|
|||
bool ShowMarkedOnProduct,
|
||||
bool ShowMinMaxStock,
|
||||
bool AllowFractionalPrices,
|
||||
bool ShowReferencePriceOnProduct);
|
||||
bool ShowReferencePriceOnProduct,
|
||||
bool ShowCountryOfOriginOnProduct);
|
||||
|
||||
// DefaultCurrencyId не принимается — он read-only, выводится из страны (Country.DefaultCurrencyId).
|
||||
public record OrgSettingsInput(
|
||||
|
|
@ -47,7 +48,8 @@ public record OrgSettingsInput(
|
|||
bool ShowMarkedOnProduct,
|
||||
bool ShowMinMaxStock,
|
||||
bool AllowFractionalPrices,
|
||||
bool ShowReferencePriceOnProduct);
|
||||
bool ShowReferencePriceOnProduct,
|
||||
bool ShowCountryOfOriginOnProduct);
|
||||
|
||||
[HttpGet("settings")]
|
||||
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
|
||||
|
|
@ -84,6 +86,7 @@ public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInp
|
|||
o.ShowMinMaxStock = input.ShowMinMaxStock;
|
||||
o.AllowFractionalPrices = input.AllowFractionalPrices;
|
||||
o.ShowReferencePriceOnProduct = input.ShowReferencePriceOnProduct;
|
||||
o.ShowCountryOfOriginOnProduct = input.ShowCountryOfOriginOnProduct;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
await _db.Entry(o).Reference(x => x.DefaultCurrency).LoadAsync(ct);
|
||||
|
|
@ -112,5 +115,6 @@ private async Task<decimal> ReadVatRateAsync(string countryCode, CancellationTok
|
|||
o.ShowMarkedOnProduct,
|
||||
o.ShowMinMaxStock,
|
||||
o.AllowFractionalPrices,
|
||||
o.ShowReferencePriceOnProduct);
|
||||
o.ShowReferencePriceOnProduct,
|
||||
o.ShowCountryOfOriginOnProduct);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public record ProductDto(
|
|||
decimal? ReferencePrice, DateTime? ReferencePriceUpdatedAt,
|
||||
Guid? PurchaseCurrencyId, string? PurchaseCurrencyCode,
|
||||
decimal Cost, DateTime? LastSupplyAt,
|
||||
string? ImageUrl, int? ShelfLifeDays,
|
||||
string? ImageUrl,
|
||||
IReadOnlyList<ProductPriceDto> Prices,
|
||||
IReadOnlyList<ProductBarcodeDto> Barcodes);
|
||||
|
||||
|
|
@ -90,6 +90,6 @@ public record ProductInput(
|
|||
bool IsService = false, Packaging Packaging = Packaging.Piece, bool IsMarked = false,
|
||||
[Range(0, 1e10)] decimal? MinStock = null, [Range(0, 1e10)] decimal? MaxStock = null,
|
||||
[Range(0, 1e10)] decimal? ReferencePrice = null, Guid? PurchaseCurrencyId = null,
|
||||
string? ImageUrl = null, [Range(0, 100000)] int? ShelfLifeDays = null,
|
||||
string? ImageUrl = null,
|
||||
IReadOnlyList<ProductPriceInput>? Prices = null,
|
||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ public class Product : TenantEntity
|
|||
public DateTime? LastSupplyAt { get; set; }
|
||||
|
||||
public string? ImageUrl { get; set; } // основное изображение (остальные в ProductImage)
|
||||
/// <summary>Срок годности в днях (для отчётов и фильтрации). Не обязательное.</summary>
|
||||
public int? ShelfLifeDays { get; set; }
|
||||
|
||||
public ICollection<ProductPrice> Prices { get; set; } = [];
|
||||
public ICollection<ProductBarcode> Barcodes { get; set; } = [];
|
||||
|
|
|
|||
|
|
@ -57,4 +57,9 @@ public class Organization : Entity
|
|||
/// <summary>Показывать ли в карточке товара поле «Эталонная цена».
|
||||
/// Default: true.</summary>
|
||||
public bool ShowReferencePriceOnProduct { get; set; } = true;
|
||||
|
||||
/// <summary>Показывать ли в карточке товара поле «Страна происхождения».
|
||||
/// Default: false. Большинству KZ-магазинов это поле не нужно;
|
||||
/// включать для торговцев импортом или маркируемой продукцией.</summary>
|
||||
public bool ShowCountryOfOriginOnProduct { get; set; }
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>products.ShelfLifeDays удалён — поле «Срок годности (дней)»
|
||||
/// решили убрать целиком из системы. Также добавлена настройка
|
||||
/// organizations.ShowCountryOfOriginOnProduct (default false) — поле
|
||||
/// «Страна происхождения» в карточке теперь скрыто пока не включено в настройках.</summary>
|
||||
public partial class Phase3b_DropProductShelfLifeDays : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.DropColumn(name: "ShelfLifeDays", schema: "public", table: "products");
|
||||
b.AddColumn<bool>(
|
||||
name: "ShowCountryOfOriginOnProduct", schema: "public", table: "organizations",
|
||||
type: "boolean", nullable: false, defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.DropColumn(name: "ShowCountryOfOriginOnProduct", schema: "public", table: "organizations");
|
||||
b.AddColumn<int>(
|
||||
name: "ShelfLifeDays", schema: "public", table: "products",
|
||||
type: "integer", nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -605,9 +605,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
|||
b.Property<DateTime?>("ReferencePriceUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("ShelfLifeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UnitOfMeasureId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
|
|
@ -1096,6 +1093,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
|||
b.Property<bool>("MultiCurrencyEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("ShowCountryOfOriginOnProduct")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("ShowMarkedOnProduct")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,16 @@ export function generateEan13InternalPrefix2(): string {
|
|||
return ean13()
|
||||
}
|
||||
|
||||
/** Сгенерировать числовой артикул для товара. На бэкенде есть надёжный
|
||||
* max(article)+1 генератор, но для UX удобно сразу подставить значение в поле:
|
||||
* берём последние 6 цифр времени + 3 случайных. Уникальность подтверждает
|
||||
* сервер; конфликт по unique index возвращается как 400 «Артикул уже занят». */
|
||||
export function generateArticle(): string {
|
||||
const ts = Date.now().toString().slice(-6)
|
||||
const rnd = Math.floor(100 + Math.random() * 900).toString()
|
||||
return ts + rnd
|
||||
}
|
||||
|
||||
/** Сгенерировать штрихкод под указанный формат. */
|
||||
export function generateBarcode(type: BarcodeType): string {
|
||||
switch (type) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export interface Product {
|
|||
referencePrice: number | null; referencePriceUpdatedAt: string | null;
|
||||
purchaseCurrencyId: string | null; purchaseCurrencyCode: string | null;
|
||||
cost: number; lastSupplyAt: string | null;
|
||||
imageUrl: string | null; shelfLifeDays: number | null;
|
||||
imageUrl: string | null;
|
||||
prices: ProductPrice[]; barcodes: ProductBarcode[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface OrgSettings {
|
|||
showMinMaxStock: boolean
|
||||
allowFractionalPrices: boolean
|
||||
showReferencePriceOnProduct: boolean
|
||||
showCountryOfOriginOnProduct: boolean
|
||||
}
|
||||
|
||||
export function useOrgSettings() {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function OrganizationSettingsPage() {
|
|||
showMinMaxStock: form.showMinMaxStock,
|
||||
allowFractionalPrices: form.allowFractionalPrices,
|
||||
showReferencePriceOnProduct: form.showReferencePriceOnProduct,
|
||||
showCountryOfOriginOnProduct: form.showCountryOfOriginOnProduct,
|
||||
}
|
||||
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
||||
},
|
||||
|
|
@ -161,6 +162,16 @@ export function OrganizationSettingsPage() {
|
|||
Справочная цена закупа — необязательное поле. Авто-заполняется первой
|
||||
проведённой приёмкой и через 30 дней без приёмок переписывается на текущую себестоимость.
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
label='Показывать «Страну происхождения» на товаре'
|
||||
checked={form.showCountryOfOriginOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showCountryOfOriginOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
По умолчанию выключено. Включай если торгуешь импортом или ведёшь
|
||||
учёт по странам — тогда в карточке товара будет селект «Страна происхождения».
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="mt-4 flex gap-3 items-center">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
||||
import { ProductImageGallery } from '@/components/ProductImageGallery'
|
||||
import { generateEan13InternalPrefix2, generateBarcode } from '@/lib/barcode'
|
||||
import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode'
|
||||
|
||||
interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string }
|
||||
interface BarcodeRow { id?: string; code: string; type: BarcodeType; isPrimary: boolean }
|
||||
|
|
@ -34,7 +34,6 @@ interface Form {
|
|||
referencePrice: string
|
||||
purchaseCurrencyId: string
|
||||
imageUrl: string
|
||||
shelfLifeDays: string
|
||||
prices: PriceRow[]
|
||||
barcodes: BarcodeRow[]
|
||||
}
|
||||
|
|
@ -48,7 +47,6 @@ const emptyForm: Form = {
|
|||
minStock: '', maxStock: '',
|
||||
referencePrice: '', purchaseCurrencyId: '',
|
||||
imageUrl: '',
|
||||
shelfLifeDays: '',
|
||||
prices: [],
|
||||
barcodes: [],
|
||||
}
|
||||
|
|
@ -91,7 +89,6 @@ export function ProductEditPage() {
|
|||
referencePrice: p.referencePrice?.toString() ?? '',
|
||||
purchaseCurrencyId: p.purchaseCurrencyId ?? '',
|
||||
imageUrl: p.imageUrl ?? '',
|
||||
shelfLifeDays: p.shelfLifeDays?.toString() ?? '',
|
||||
prices: p.prices.map((x) => ({ id: x.id, priceTypeId: x.priceTypeId, amount: x.amount, currencyId: x.currencyId })),
|
||||
barcodes: p.barcodes.map((x) => ({ id: x.id, code: x.code, type: x.type, isPrimary: x.isPrimary })),
|
||||
})
|
||||
|
|
@ -119,6 +116,13 @@ export function ProductEditPage() {
|
|||
if (isNew && org.data && form.vat === 16 && org.data.vatRate !== 16) {
|
||||
setForm((f) => ({ ...f, vat: org.data!.vatRate }))
|
||||
}
|
||||
// У нового товара сразу автогенерируется артикул — уникальность
|
||||
// окончательно подтверждает сервер. Поле остаётся редактируемым,
|
||||
// пользователь может заменить вручную.
|
||||
if (isNew && form.article === '') {
|
||||
const a = generateArticle()
|
||||
setForm((f) => ({ ...f, article: a }))
|
||||
}
|
||||
// У нового товара сразу один сгенерированный штрихкод, чтобы кнопку Сохранить
|
||||
// можно было нажать без лишних кликов.
|
||||
if (isNew && form.barcodes.length === 0) {
|
||||
|
|
@ -127,7 +131,7 @@ export function ProductEditPage() {
|
|||
barcodes: [{ code: generateEan13InternalPrefix2(), type: BarcodeType.Ean13, isPrimary: true }],
|
||||
}))
|
||||
}
|
||||
}, [isNew, units.data, groups.data, currencies.data, org.data, form.unitOfMeasureId, form.productGroupId, form.purchaseCurrencyId, form.vat, form.barcodes.length])
|
||||
}, [isNew, units.data, groups.data, currencies.data, org.data, form.unitOfMeasureId, form.productGroupId, form.purchaseCurrencyId, form.vat, form.article, form.barcodes.length])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
|
@ -152,7 +156,6 @@ export function ProductEditPage() {
|
|||
referencePrice: form.referencePrice === '' ? null : Number(form.referencePrice),
|
||||
purchaseCurrencyId: form.purchaseCurrencyId || null,
|
||||
imageUrl: form.imageUrl || null,
|
||||
shelfLifeDays: form.shelfLifeDays === '' ? null : Number(form.shelfLifeDays),
|
||||
prices: form.prices.map((p) => ({ priceTypeId: p.priceTypeId, amount: Number(p.amount), currencyId: p.currencyId })),
|
||||
barcodes: form.barcodes.map((b) => ({ code: b.code, type: b.type, isPrimary: b.isPrimary })),
|
||||
}
|
||||
|
|
@ -204,6 +207,7 @@ export function ProductEditPage() {
|
|||
})
|
||||
|
||||
const canSave = form.name.trim().length > 0
|
||||
&& form.article.trim().length > 0
|
||||
&& !!form.unitOfMeasureId
|
||||
&& !!form.productGroupId
|
||||
&& form.barcodes.length > 0
|
||||
|
|
@ -259,16 +263,8 @@ export function ProductEditPage() {
|
|||
<Field label="Название *" className="col-span-2">
|
||||
<TextInput required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Артикул">
|
||||
<TextInput value={form.article} onChange={(e) => setForm({ ...form, article: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Срок годности (дней)">
|
||||
<NumberInput
|
||||
value={form.shelfLifeDays === '' ? null : Number(form.shelfLifeDays)}
|
||||
onChange={(n) => setForm({ ...form, shelfLifeDays: n == null ? '' : String(Math.max(0, Math.round(n))) })}
|
||||
placeholder="—"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">не обязательное поле</p>
|
||||
<Field label="Артикул *">
|
||||
<TextInput required value={form.article} onChange={(e) => setForm({ ...form, article: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Описание" className="col-span-3">
|
||||
<TextArea rows={2} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||
|
|
@ -278,30 +274,16 @@ export function ProductEditPage() {
|
|||
|
||||
<Section title="Классификация">
|
||||
<Grid cols={3}>
|
||||
<Field label="Единица измерения *">
|
||||
<Select required value={form.unitOfMeasureId} onChange={(e) => setForm({ ...form, unitOfMeasureId: e.target.value })}>
|
||||
{units.data?.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Группа *">
|
||||
<Select required value={form.productGroupId} onChange={(e) => setForm({ ...form, productGroupId: e.target.value })}>
|
||||
{groups.data?.map((g) => <option key={g.id} value={g.id}>{g.path}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Страна происхождения">
|
||||
<Select value={form.countryOfOriginId} onChange={(e) => setForm({ ...form, countryOfOriginId: e.target.value })}>
|
||||
<option value="">—</option>
|
||||
{countries.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
<Field label="Единица измерения *">
|
||||
<Select required value={form.unitOfMeasureId} onChange={(e) => setForm({ ...form, unitOfMeasureId: e.target.value })}>
|
||||
{units.data?.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Основной поставщик">
|
||||
<Select value={form.defaultSupplierId} onChange={(e) => setForm({ ...form, defaultSupplierId: e.target.value })}>
|
||||
<option value="">—</option>
|
||||
{suppliers.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
</Grid>
|
||||
<Grid cols={3}>
|
||||
<Field label="Фасовка">
|
||||
<Select value={form.packaging} onChange={(e) => setForm({ ...form, packaging: Number(e.target.value) as Packaging })}>
|
||||
<option value={Packaging.Piece}>Штучный</option>
|
||||
|
|
@ -310,6 +292,22 @@ export function ProductEditPage() {
|
|||
</Select>
|
||||
</Field>
|
||||
</Grid>
|
||||
<Grid cols={2}>
|
||||
<Field label="Основной поставщик">
|
||||
<Select value={form.defaultSupplierId} onChange={(e) => setForm({ ...form, defaultSupplierId: e.target.value })}>
|
||||
<option value="">—</option>
|
||||
{suppliers.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
{org.data?.showCountryOfOriginOnProduct && (
|
||||
<Field label="Страна происхождения">
|
||||
<Select value={form.countryOfOriginId} onChange={(e) => setForm({ ...form, countryOfOriginId: e.target.value })}>
|
||||
<option value="">—</option>
|
||||
{countries.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
</Grid>
|
||||
{org.data?.showVatEnabledOnProduct && form.vatEnabled && (
|
||||
<Grid cols={3}>
|
||||
<Field label="Ставка НДС, %">
|
||||
|
|
@ -509,14 +507,18 @@ export function ProductEditPage() {
|
|||
onChange={(v) => setForm({ ...form, barcodes: form.barcodes.map((x, ix) => ({ ...x, isPrimary: ix === i ? v : false })) })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeBarcode(i)}
|
||||
className="col-span-1 text-slate-400 hover:text-red-600 flex justify-center"
|
||||
title="Удалить строку"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
{form.barcodes.length > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeBarcode(i)}
|
||||
className="col-span-1 text-slate-400 hover:text-red-600 flex justify-center"
|
||||
title="Удалить строку"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="col-span-1" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ interface Filters {
|
|||
isMarked: TriFilter
|
||||
systemPriceFrom: number | null
|
||||
systemPriceTo: number | null
|
||||
shelfLifeDaysFrom: number | null
|
||||
shelfLifeDaysTo: number | null
|
||||
}
|
||||
|
||||
const defaultFilters: Filters = {
|
||||
|
|
@ -34,8 +32,6 @@ const defaultFilters: Filters = {
|
|||
isMarked: 'all',
|
||||
systemPriceFrom: null,
|
||||
systemPriceTo: null,
|
||||
shelfLifeDaysFrom: null,
|
||||
shelfLifeDaysTo: null,
|
||||
}
|
||||
|
||||
const toExtra = (f: Filters): Record<string, string | number | boolean | undefined> => {
|
||||
|
|
@ -46,8 +42,6 @@ const toExtra = (f: Filters): Record<string, string | number | boolean | undefin
|
|||
if (f.isMarked !== 'all') e.isMarked = f.isMarked === 'yes'
|
||||
if (f.systemPriceFrom != null) e.systemPriceFrom = f.systemPriceFrom
|
||||
if (f.systemPriceTo != null) e.systemPriceTo = f.systemPriceTo
|
||||
if (f.shelfLifeDaysFrom != null) e.shelfLifeDaysFrom = f.shelfLifeDaysFrom
|
||||
if (f.shelfLifeDaysTo != null) e.shelfLifeDaysTo = f.shelfLifeDaysTo
|
||||
return e
|
||||
}
|
||||
|
||||
|
|
@ -59,8 +53,6 @@ const activeFilterCount = (f: Filters) => {
|
|||
if (f.isMarked !== 'all') n++
|
||||
if (f.systemPriceFrom != null) n++
|
||||
if (f.systemPriceTo != null) n++
|
||||
if (f.shelfLifeDaysFrom != null) n++
|
||||
if (f.shelfLifeDaysTo != null) n++
|
||||
return n
|
||||
}
|
||||
|
||||
|
|
@ -258,23 +250,6 @@ export function ProductsPage() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-500">Срок годности (дней)</span>
|
||||
<input
|
||||
type="number" min="0"
|
||||
value={filters.shelfLifeDaysFrom ?? ''}
|
||||
placeholder="от"
|
||||
onChange={(e) => { const v = e.target.value === '' ? null : Number(e.target.value); setFilters({ ...filters, shelfLifeDaysFrom: v }); setPage(1) }}
|
||||
className="w-20 px-2 py-0.5 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900"
|
||||
/>
|
||||
<input
|
||||
type="number" min="0"
|
||||
value={filters.shelfLifeDaysTo ?? ''}
|
||||
placeholder="до"
|
||||
onChange={(e) => { const v = e.target.value === '' ? null : Number(e.target.value); setFilters({ ...filters, shelfLifeDaysTo: v }); setPage(1) }}
|
||||
className="w-20 px-2 py-0.5 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
{activeCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
Loading…
Reference in a new issue