feat(supply+products-list): чекбокс «Проведено» с confirm + системная розничная в списке
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:
parent
b79c71591d
commit
b257ea528d
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue