From 2976070a2af8afaf14774d492a5fa6410ed0bd10 Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:52:44 +0500 Subject: [PATCH] =?UTF-8?q?feat(product+filters):=20=D1=81=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B3=D0=BE=D0=B4=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20(sh?= =?UTF-8?q?elfLifeDays)=20+=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20?= =?UTF-8?q?=D0=BE=D1=82/=D0=B4=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProductEditPage: - В секции «Основное» добавлено поле «Срок годности (дней)» рядом с Артикулом — NumberInput, целое ≥ 0, hint «не обязательное поле». - form.shelfLifeDays хранится строкой и сериализуется в payload как number | null. ProductsPage filters: - Добавлен диапазон «Срок годности (дней) — от / до». - Удалён остаток фильтра isActive (поле выпилено в Phase3b foundation). ProductsController.List: - Принимает shelfLifeDaysFrom / shelfLifeDaysTo (int?) и применяет ≥ / ≤ к p.ShelfLifeDays. - Также теперь принимает referencePriceFrom / referencePriceTo (новые имена); старые purchasePriceFrom/To работают как алиасы для обратной совместимости с уже отрендеренным UI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Controllers/Catalog/ProductsController.cs | 14 +++++++-- .../src/pages/ProductEditPage.tsx | 12 ++++++++ .../src/pages/ProductsPage.tsx | 30 +++++++++++++++---- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index 35b7438..167094a 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -94,6 +94,10 @@ private async Task ResolveDefaultVatAsync(CancellationToken ct) [FromQuery] bool? isMarked, [FromQuery] decimal? purchasePriceFrom, [FromQuery] decimal? purchasePriceTo, + [FromQuery] decimal? referencePriceFrom, + [FromQuery] decimal? referencePriceTo, + [FromQuery] int? shelfLifeDaysFrom, + [FromQuery] int? shelfLifeDaysTo, CancellationToken ct) { var q = QueryIncludes().AsNoTracking(); @@ -115,8 +119,14 @@ private async Task ResolveDefaultVatAsync(CancellationToken ct) if (isService is not null) q = q.Where(p => p.IsService == isService); if (packaging is not null) q = q.Where(p => p.Packaging == packaging); if (isMarked is not null) q = q.Where(p => p.IsMarked == isMarked); - if (purchasePriceFrom is not null) q = q.Where(p => p.ReferencePrice >= purchasePriceFrom); - if (purchasePriceTo is not null) q = q.Where(p => p.ReferencePrice <= purchasePriceTo); + // referencePriceFrom/To — новый, актуальный параметр; purchasePriceFrom/To + // — обратная совместимость c прежним UI (тоже по ReferencePrice). + var refFrom = referencePriceFrom ?? purchasePriceFrom; + 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); if (!string.IsNullOrWhiteSpace(req.Search)) { diff --git a/src/food-market.web/src/pages/ProductEditPage.tsx b/src/food-market.web/src/pages/ProductEditPage.tsx index 26313de..b54ef22 100644 --- a/src/food-market.web/src/pages/ProductEditPage.tsx +++ b/src/food-market.web/src/pages/ProductEditPage.tsx @@ -34,6 +34,7 @@ interface Form { referencePrice: string purchaseCurrencyId: string imageUrl: string + shelfLifeDays: string prices: PriceRow[] barcodes: BarcodeRow[] } @@ -47,6 +48,7 @@ const emptyForm: Form = { minStock: '', maxStock: '', referencePrice: '', purchaseCurrencyId: '', imageUrl: '', + shelfLifeDays: '', prices: [], barcodes: [], } @@ -89,6 +91,7 @@ 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 })), }) @@ -149,6 +152,7 @@ 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 })), } @@ -250,6 +254,14 @@ export function ProductEditPage() { setForm({ ...form, article: e.target.value })} /> + + setForm({ ...form, shelfLifeDays: n == null ? '' : String(Math.max(0, Math.round(n))) })} + placeholder="—" + /> +

не обязательное поле

+