ui(products-list): убрать фильтр «Со штрихкодом», добавить «Закупочная цена от/до»
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 29s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 42s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 13s

Поскольку штрихкод теперь обязательный (минимум 1 у каждого товара),
фильтр «Со штрихкодом» бессмыслен — убран из UI и контроллера.

Вместо него — два MoneyInput «Закупочная цена от/до» в панели
фильтров. Использует символ валюты по умолчанию из настроек
организации и уважает AllowFractionalPrices.

Backend: ProductsController.List принимает purchasePriceFrom /
purchasePriceTo (decimal?), применяет ≥ / ≤ к PurchasePrice;
параметр hasBarcode удалён.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-25 12:23:45 +05:00
parent a49db1c90d
commit 6b3491056b
2 changed files with 36 additions and 8 deletions

View file

@ -79,7 +79,8 @@ private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
[FromQuery] Packaging? packaging, [FromQuery] Packaging? packaging,
[FromQuery] bool? isMarked, [FromQuery] bool? isMarked,
[FromQuery] bool? isActive, [FromQuery] bool? isActive,
[FromQuery] bool? hasBarcode, [FromQuery] decimal? purchasePriceFrom,
[FromQuery] decimal? purchasePriceTo,
CancellationToken ct) CancellationToken ct)
{ {
var q = QueryIncludes().AsNoTracking(); var q = QueryIncludes().AsNoTracking();
@ -102,8 +103,8 @@ private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
if (packaging is not null) q = q.Where(p => p.Packaging == packaging); 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 (isMarked is not null) q = q.Where(p => p.IsMarked == isMarked);
if (isActive is not null) q = q.Where(p => p.IsActive == isActive); if (isActive is not null) q = q.Where(p => p.IsActive == isActive);
if (hasBarcode is not null) if (purchasePriceFrom is not null) q = q.Where(p => p.PurchasePrice >= purchasePriceFrom);
q = hasBarcode == true ? q.Where(p => p.Barcodes.Any()) : q.Where(p => !p.Barcodes.Any()); if (purchasePriceTo is not null) q = q.Where(p => p.PurchasePrice <= purchasePriceTo);
if (!string.IsNullOrWhiteSpace(req.Search)) if (!string.IsNullOrWhiteSpace(req.Search))
{ {

View file

@ -8,6 +8,7 @@ import { Plus, Filter, X, FolderTree } from 'lucide-react'
import { useCatalogList } from '@/lib/useCatalog' import { useCatalogList } from '@/lib/useCatalog'
import { useOrgSettings } from '@/lib/useOrgSettings' import { useOrgSettings } from '@/lib/useOrgSettings'
import { ProductGroupTree } from '@/components/ProductGroupTree' import { ProductGroupTree } from '@/components/ProductGroupTree'
import { MoneyInput } from '@/components/Field'
import { packagingLabel, type Product } from '@/lib/types' import { packagingLabel, type Product } from '@/lib/types'
const URL = '/api/catalog/products' const URL = '/api/catalog/products'
@ -20,7 +21,8 @@ interface Filters {
isService: TriFilter isService: TriFilter
packaging: number | null // null = все, 1/2/3 = Piece/Weight/Liquid packaging: number | null // null = все, 1/2/3 = Piece/Weight/Liquid
isMarked: TriFilter isMarked: TriFilter
hasBarcode: TriFilter purchasePriceFrom: number | null
purchasePriceTo: number | null
} }
const defaultFilters: Filters = { const defaultFilters: Filters = {
@ -29,7 +31,8 @@ const defaultFilters: Filters = {
isService: 'all', isService: 'all',
packaging: null, packaging: null,
isMarked: 'all', isMarked: 'all',
hasBarcode: 'all', purchasePriceFrom: null,
purchasePriceTo: null,
} }
const toExtra = (f: Filters): Record<string, string | number | boolean | undefined> => { const toExtra = (f: Filters): Record<string, string | number | boolean | undefined> => {
@ -39,7 +42,8 @@ const toExtra = (f: Filters): Record<string, string | number | boolean | undefin
if (f.isService !== 'all') e.isService = f.isService === 'yes' if (f.isService !== 'all') e.isService = f.isService === 'yes'
if (f.packaging) e.packaging = f.packaging if (f.packaging) e.packaging = f.packaging
if (f.isMarked !== 'all') e.isMarked = f.isMarked === 'yes' if (f.isMarked !== 'all') e.isMarked = f.isMarked === 'yes'
if (f.hasBarcode !== 'all') e.hasBarcode = f.hasBarcode === 'yes' if (f.purchasePriceFrom != null) e.purchasePriceFrom = f.purchasePriceFrom
if (f.purchasePriceTo != null) e.purchasePriceTo = f.purchasePriceTo
return e return e
} }
@ -50,7 +54,8 @@ const activeFilterCount = (f: Filters) => {
if (f.isService !== 'all') n++ if (f.isService !== 'all') n++
if (f.packaging) n++ if (f.packaging) n++
if (f.isMarked !== 'all') n++ if (f.isMarked !== 'all') n++
if (f.hasBarcode !== 'all') n++ if (f.purchasePriceFrom != null) n++
if (f.purchasePriceTo != null) n++
return n return n
} }
@ -217,7 +222,29 @@ export function ProductsPage() {
{showMarked && ( {showMarked && (
<Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} /> <Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} />
)} )}
<Tri label="Со штрихкодом" value={filters.hasBarcode} onChange={(v) => { setFilters({ ...filters, hasBarcode: v }); setPage(1) }} yesLabel="есть" noLabel="нет" /> <div className="flex items-center gap-2 text-xs">
<span className="text-slate-500">Закупочная цена</span>
<div className="w-32">
<MoneyInput
value={filters.purchasePriceFrom}
onChange={(n) => { setFilters({ ...filters, purchasePriceFrom: n }); setPage(1) }}
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
allowFractional={org.data?.allowFractionalPrices ?? false}
placeholder="от"
/>
</div>
<div className="w-32">
<MoneyInput
value={filters.purchasePriceTo}
onChange={(n) => { setFilters({ ...filters, purchasePriceTo: n }); setPage(1) }}
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
allowFractional={org.data?.allowFractionalPrices ?? false}
placeholder="до"
/>
</div>
</div>
{activeCount > 0 && ( {activeCount > 0 && (
<button <button
type="button" type="button"