fix(product-edit): человечная ошибка 400 + блок Save при незаполненных IsRequired ценах
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 59s
CI / Web (React + Vite) (push) Successful in 37s
Docker Web / Build + push Web (push) Successful in 25s
Docker Web / Deploy Web on stage (push) Successful in 11s

Сервер 400-нул сохранение товара когда обязательная цена пуста, а UI
показывал «Request failed with status code 400» без указания причины.

- onError save mutation: достаём response.data.error из axios-ответа
  и кладём в setError; общий generic message остаётся как fallback.
- canSave дополнен проверкой что у каждого PriceType с IsRequired=true
  есть строка в form.prices с amount > 0 (та же что делает бэкенд,
  чтобы кнопка не отправляла запрос обречённый на 400).
- Под секцией цен — красная подсказка-список незаполненных обязательных
  типов цен («Заполни обязательные цены: «Розничная2» (значение должно
  быть больше 0).»).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-25 23:58:49 +05:00
parent 8379fe116a
commit 5614fb9422

View file

@ -166,7 +166,10 @@ export function ProductEditPage() {
qc.invalidateQueries({ queryKey: ['/api/catalog/products'] }) qc.invalidateQueries({ queryKey: ['/api/catalog/products'] })
navigate(created ? `/catalog/products/${created.id}` : '/catalog/products') navigate(created ? `/catalog/products/${created.id}` : '/catalog/products')
}, },
onError: (e: Error) => setError(e.message), onError: (e: Error) => {
const msg = (e as { response?: { data?: { error?: string } } }).response?.data?.error ?? e.message
setError(msg)
},
}) })
const remove = useMutation({ const remove = useMutation({
@ -190,10 +193,21 @@ export function ProductEditPage() {
const updateBarcode = (i: number, patch: Partial<BarcodeRow>) => const updateBarcode = (i: number, patch: Partial<BarcodeRow>) =>
setForm({ ...form, barcodes: form.barcodes.map((b, ix) => ix === i ? { ...b, ...patch } : b) }) setForm({ ...form, barcodes: form.barcodes.map((b, ix) => ix === i ? { ...b, ...patch } : b) })
// У каждого PriceType с IsRequired=true должна быть запись в form.prices
// с Amount > 0. Сервер делает ту же проверку (400 иначе), но дублируем
// на фронте чтобы кнопка Save не клатилась с непонятным «Request failed with status 400».
const missingRequiredPrices = (priceTypes.data ?? [])
.filter((pt) => pt.isRequired)
.filter((pt) => {
const row = form.prices.find((p) => p.priceTypeId === pt.id)
return !row || row.amount <= 0
})
const canSave = form.name.trim().length > 0 const canSave = form.name.trim().length > 0
&& !!form.unitOfMeasureId && !!form.unitOfMeasureId
&& !!form.productGroupId && !!form.productGroupId
&& form.barcodes.length > 0 && form.barcodes.length > 0
&& missingRequiredPrices.length === 0
return ( return (
<form onSubmit={onSubmit} className="flex flex-col h-full"> <form onSubmit={onSubmit} className="flex flex-col h-full">
@ -447,6 +461,11 @@ export function ProductEditPage() {
{priceTypes.data?.length === 0 && ( {priceTypes.data?.length === 0 && (
<div className="text-sm text-slate-400 py-2">Нет ни одного типа цен. Создай в «Настройки Типы цен».</div> <div className="text-sm text-slate-400 py-2">Нет ни одного типа цен. Создай в «Настройки Типы цен».</div>
)} )}
{missingRequiredPrices.length > 0 && (
<div className="text-sm text-red-600 mt-2">
Заполни обязательные цены: {missingRequiredPrices.map((pt) => `«${pt.name}»`).join(', ')} (значение должно быть больше 0).
</div>
)}
</div> </div>
</Section> </Section>