feat(supply+products-list): чекбокс «Проведено» с confirm + системная розничная в списке
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 43s
CI / Web (React + Vite) (push) Successful in 35s
Docker Web / Build + push Web (push) Successful in 26s
Docker Web / Deploy Web on stage (push) Successful in 12s

Supply edit page:
- Кнопки «Провести / Отменить проведение» заменены на чекбокс «Проведено»
  у заголовка. При попытке отметки — confirm «После проведения товары
  будут оприходованы на склад. Продолжить?», при снятии — confirm
  «Снять проведение? Остатки откатятся, себестоимость останется
  (пересчитать вручную при необходимости).».
- Чекбокс disabled пока хотя бы одна строка пустая (Quantity ≤ 0
  или UnitPrice ≤ 0) или вообще нет строк.
- Backend (post/unpost) уже корректно делает StockMovement и
  пересчёт цен из Phase3a — UI просто переключает status.

Products list:
- Колонка «Эталонная цена» заменена на колонку системной розничной.
  Заголовок = Name той PriceType что IsSystem=true (если пользователь
  переименовал «Розничная цена» → «Продажная цена», заголовок
  колонки автоматически становится «Продажная цена»).
- Значение = Product.Prices[ системного PriceType ].Amount.
  Если у товара нет такой записи — «—» (тире).
- Подпись фильтра «Закупочная цена» → «Эталонная цена» (поведение
  фильтра по диапазону цены остаётся прежним).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-25 22:49:42 +05:00
parent 3c274541e9
commit 5a020cfafa
2 changed files with 42 additions and 22 deletions

View file

@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
import { Plus, Filter, X, FolderTree } from 'lucide-react'
import { useCatalogList } from '@/lib/useCatalog'
import { useOrgSettings } from '@/lib/useOrgSettings'
import { usePriceTypes } from '@/lib/useLookups'
import { ProductGroupTree } from '@/components/ProductGroupTree'
import { MoneyInput } from '@/components/Field'
import { packagingLabel, type Product } from '@/lib/types'
@ -103,6 +104,8 @@ export function ProductsPage() {
const [filtersOpen, setFiltersOpen] = useState(false)
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Product>(URL, toExtra(filters))
const org = useOrgSettings()
const priceTypes = usePriceTypes()
const systemPriceType = priceTypes.data?.find((pt) => pt.isSystem) ?? priceTypes.data?.find((pt) => pt.isDefault) ?? priceTypes.data?.[0]
const showVat = org.data?.showVatEnabledOnProduct ?? false
const showService = org.data?.showServiceOnProduct ?? false
const showMarked = org.data?.showMarkedOnProduct ?? false
@ -127,15 +130,23 @@ export function ProductsPage() {
{ header: 'Штрихкод', width: '160px', cell: (r) => (
<span className="font-mono">{r.barcodes[0]?.code ?? '—'}</span>
)},
{ header: 'Эталонная цена', width: '160px', className: 'text-right font-mono', sortKey: 'referencePrice', cell: (r) => {
if (r.referencePrice == null) return '—'
// Колонка системной розничной цены: заголовок = PriceType.Name той записи
// что помечена IsSystem (если пользователь её переименовал — заголовок меняется).
{
header: systemPriceType?.name ?? 'Розничная цена',
width: '170px',
className: 'text-right font-mono',
cell: (r) => {
const pr = systemPriceType ? r.prices?.find(x => x.priceTypeId === systemPriceType.id) : undefined
if (!pr) return '—'
const fractional = org.data?.allowFractionalPrices ?? false
const num = r.referencePrice.toLocaleString('ru',
const num = pr.amount.toLocaleString('ru',
fractional
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
: { maximumFractionDigits: 0 })
return `${num} ${r.purchaseCurrencyCode ?? ''}`.trim()
}},
return `${num} ${pr.currencyCode ?? ''}`.trim()
},
},
]
if (showVat) {
baseColumns.push({ header: 'НДС', width: '90px', className: 'text-right', sortKey: 'vat', cell: (r) => r.vatEnabled ? `${r.vat.toFixed(2)}%` : '—' })
@ -223,7 +234,7 @@ export function ProductsPage() {
<Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} />
)}
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-500">Закупочная цена</span>
<span className="text-slate-500">Эталонная цена</span>
<div className="w-32">
<MoneyInput
value={filters.referencePriceFrom}

View file

@ -1,7 +1,7 @@
import { useState, useEffect, type FormEvent, type ReactNode } from 'react'
import { useNavigate, useParams, Link } from 'react-router-dom'
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } from 'lucide-react'
import { api } from '@/lib/api'
import { Button } from '@/components/Button'
import { Field, TextInput, TextArea, Select, MoneyInput, NumberInput } from '@/components/Field'
@ -223,11 +223,25 @@ export function SupplyEditPage() {
</p>
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
{isPosted && (
<Button type="button" variant="secondary" onClick={() => unpost.mutate()} disabled={unpost.isPending}>
<Undo2 className="w-4 h-4" /> Отменить проведение
</Button>
<div className="flex gap-3 flex-shrink-0 items-center">
{!isNew && (
<label className={`flex items-center gap-2 text-sm ${form.lines.length === 0 ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}>
<input
type="checkbox"
checked={isPosted}
disabled={post.isPending || unpost.isPending || form.lines.length === 0
|| form.lines.some(l => l.quantity <= 0 || l.unitPrice <= 0)}
onChange={(e) => {
if (e.target.checked) {
if (confirm('После проведения товары будут оприходованы на склад. Продолжить?')) post.mutate()
} else {
if (confirm('Снять проведение? Остатки откатятся, себестоимость останется (пересчитать вручную при необходимости).')) unpost.mutate()
}
}}
className="w-4 h-4 rounded border-slate-300 text-[var(--color-brand)] focus:ring-[var(--color-brand)]"
/>
<span className="font-medium">Проведено</span>
</label>
)}
{isDraft && !isNew && (
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик приёмки?')) remove.mutate() }}>
@ -235,15 +249,10 @@ export function SupplyEditPage() {
</Button>
)}
{isDraft && (
<Button type="submit" variant="secondary" disabled={!canSave || save.isPending}>
<Button type="submit" disabled={!canSave || save.isPending}>
<Save className="w-4 h-4" /> {save.isPending ? 'Сохраняю…' : 'Сохранить'}
</Button>
)}
{isDraft && !isNew && (
<Button type="button" onClick={() => post.mutate()} disabled={post.isPending || form.lines.length === 0}>
<CheckCircle className="w-4 h-4" /> {post.isPending ? 'Провожу…' : 'Провести'}
</Button>
)}
</div>
</div>