From 8a0f8c20f991b85e640fc9f61a3a6408bb61811f Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:30:00 +0500 Subject: [PATCH] =?UTF-8?q?fix(money-input):=20=D1=83=D0=B2=D0=B0=D0=B6?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20AllowFractionalPrices=20=D0=B2=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=85=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MoneyInput теперь сам читает useOrgSettings().allowFractionalPrices, а не только полагается на prop из вызова. Это закрывает два бага: 1. Когда настройка известна и запрещает дробное, но в state товара лежит дробная цена (например исторические данные из OtherSystem) — useEffect синхронизирует округлённое значение наружу, чтобы при сохранении не уходило значение, которого юзер не видит. 2. Пока org.data ещё не загружено, MoneyInput не режет дробь (fractional трактуется как true до приезда настройки), иначе в момент гидрации формы из ProductDto с дробной ценой компонент успевал её обрезать до того как настройка приходила. Все вызовы MoneyInput в ProductEdit / SupplyEdit / RetailSaleEdit / ProductsPage filters очищены от избыточного prop allowFractional — компонент берёт настройку сам. Override через prop остаётся доступным. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/food-market.web/src/components/Field.tsx | 38 ++++++++++++++----- .../src/pages/ProductEditPage.tsx | 4 +- .../src/pages/ProductsPage.tsx | 4 +- .../src/pages/RetailSaleEditPage.tsx | 8 ++-- .../src/pages/SupplyEditPage.tsx | 2 +- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/food-market.web/src/components/Field.tsx b/src/food-market.web/src/components/Field.tsx index b6bf286..8c90034 100644 --- a/src/food-market.web/src/components/Field.tsx +++ b/src/food-market.web/src/components/Field.tsx @@ -1,5 +1,6 @@ -import type { InputHTMLAttributes, SelectHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react' +import { useEffect, type InputHTMLAttributes, type SelectHTMLAttributes, type ReactNode, type TextareaHTMLAttributes } from 'react' import { cn } from '@/lib/utils' +import { useOrgSettings } from '@/lib/useOrgSettings' interface FieldProps { label: string @@ -38,7 +39,8 @@ interface MoneyInputProps { onChange: (v: number | null) => void currencyCode?: string | null currencySymbol?: string | null - /** Разрешать ли две цифры после запятой. По умолчанию false (целые). */ + /** Если задан — переопределяет настройку организации. Если не задан — + * читается из useOrgSettings().allowFractionalPrices. */ allowFractional?: boolean disabled?: boolean placeholder?: string @@ -47,34 +49,50 @@ interface MoneyInputProps { /** Денежное поле: только цифры (+ точка/запятая если allowFractional), * справа — символ валюты (₸/$/€). При allowFractional=false дробная часть - * отбрасывается на лету и отображается целым; иначе — два знака после - * запятой. Пустое поле → null. */ + * отбрасывается на лету и сразу синхронизируется в state наружу; иначе — + * две цифры после запятой. Пустое поле → null. */ export function MoneyInput({ - value, onChange, currencyCode, currencySymbol, allowFractional = false, + value, onChange, currencyCode, currencySymbol, allowFractional, disabled, placeholder = '0', className, }: MoneyInputProps) { + const org = useOrgSettings() + // Пока org.data не загружен и prop не задан — фактическое поведение + // оставляем «как сейчас в state», чтобы не резать дробь до приезда настройки. + const settingKnown = allowFractional !== undefined || org.data !== undefined + const fractional = allowFractional ?? org.data?.allowFractionalPrices ?? true + + // Когда настройка известна и она запрещает дробное, а в state лежит дробь — + // синхронизируем округлённое значение наружу. Это нужно чтобы при сохранении + // не уходило значение, которое юзер не видит на экране. + useEffect(() => { + if (!settingKnown || fractional) return + if (value == null) return + const rounded = Math.round(value) + if (rounded !== value) onChange(rounded) + }, [settingKnown, fractional, value, onChange]) + const suffix = currencySymbol || currencyCode || '₸' - const display = value == null ? '' : (allowFractional ? String(value) : String(Math.round(value))) + const display = value == null ? '' : (fractional ? String(value) : String(Math.round(value))) return (
e.currentTarget.select()} onChange={(e) => { let raw = e.target.value.replace(',', '.') - raw = allowFractional ? raw.replace(/[^\d.]/g, '') : raw.replace(/[^\d]/g, '') + raw = fractional ? raw.replace(/[^\d.]/g, '') : raw.replace(/[^\d]/g, '') if (raw === '') return onChange(null) - if (allowFractional) { + if (fractional) { const parts = raw.split('.') raw = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw } const n = Number(raw) if (!Number.isFinite(n)) return - onChange(allowFractional ? n : Math.round(n)) + onChange(fractional ? n : Math.round(n)) }} className={cn(inputClass, 'pr-10 text-right tabular-nums')} /> diff --git a/src/food-market.web/src/pages/ProductEditPage.tsx b/src/food-market.web/src/pages/ProductEditPage.tsx index 54015a8..33b0090 100644 --- a/src/food-market.web/src/pages/ProductEditPage.tsx +++ b/src/food-market.web/src/pages/ProductEditPage.tsx @@ -323,7 +323,7 @@ export function ProductEditPage() { onChange={(n) => setForm({ ...form, purchasePrice: n == null ? '' : String(n) })} currencyCode={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined} currencySymbol={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined} - allowFractional={org.data?.allowFractionalPrices ?? false} + /> {org.data?.multiCurrencyEnabled && ( @@ -379,7 +379,7 @@ export function ProductEditPage() { onChange={(n) => updatePrice(i, { amount: n ?? 0 })} currencyCode={currencies.data?.find((c) => c.id === p.currencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined} currencySymbol={currencies.data?.find((c) => c.id === p.currencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined} - allowFractional={org.data?.allowFractionalPrices ?? false} + />
{org.data?.multiCurrencyEnabled && ( diff --git a/src/food-market.web/src/pages/ProductsPage.tsx b/src/food-market.web/src/pages/ProductsPage.tsx index fbbfe48..1f9520c 100644 --- a/src/food-market.web/src/pages/ProductsPage.tsx +++ b/src/food-market.web/src/pages/ProductsPage.tsx @@ -230,7 +230,7 @@ export function ProductsPage() { onChange={(n) => { setFilters({ ...filters, purchasePriceFrom: n }); setPage(1) }} currencyCode={org.data?.defaultCurrencyCode ?? undefined} currencySymbol={org.data?.defaultCurrencySymbol ?? undefined} - allowFractional={org.data?.allowFractionalPrices ?? false} + placeholder="от" /> @@ -240,7 +240,7 @@ export function ProductsPage() { onChange={(n) => { setFilters({ ...filters, purchasePriceTo: n }); setPage(1) }} currencyCode={org.data?.defaultCurrencyCode ?? undefined} currencySymbol={org.data?.defaultCurrencySymbol ?? undefined} - allowFractional={org.data?.allowFractionalPrices ?? false} + placeholder="до" /> diff --git a/src/food-market.web/src/pages/RetailSaleEditPage.tsx b/src/food-market.web/src/pages/RetailSaleEditPage.tsx index 2007998..671cb39 100644 --- a/src/food-market.web/src/pages/RetailSaleEditPage.tsx +++ b/src/food-market.web/src/pages/RetailSaleEditPage.tsx @@ -289,14 +289,14 @@ export function RetailSaleEditPage() { onChange={(n) => setForm({ ...form, paidCash: n ?? 0 })} currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code} currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} - allowFractional={org.data?.allowFractionalPrices ?? false} /> + /> setForm({ ...form, paidCard: n ?? 0 })} currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code} currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} - allowFractional={org.data?.allowFractionalPrices ?? false} /> + />