fix(money-input): уважать AllowFractionalPrices в формах редактирования
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 33s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 18s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 33s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 18s
MoneyInput теперь сам читает useOrgSettings().allowFractionalPrices, а не только полагается на prop из вызова. Это закрывает два бага: 1. Когда настройка известна и запрещает дробное, но в state товара лежит дробная цена (например исторические данные из MoySklad) — 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) <noreply@anthropic.com>
This commit is contained in:
parent
1ee4b84e53
commit
52a420ea3d
|
|
@ -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 { cn } from '@/lib/utils'
|
||||||
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
||||||
interface FieldProps {
|
interface FieldProps {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -38,7 +39,8 @@ interface MoneyInputProps {
|
||||||
onChange: (v: number | null) => void
|
onChange: (v: number | null) => void
|
||||||
currencyCode?: string | null
|
currencyCode?: string | null
|
||||||
currencySymbol?: string | null
|
currencySymbol?: string | null
|
||||||
/** Разрешать ли две цифры после запятой. По умолчанию false (целые). */
|
/** Если задан — переопределяет настройку организации. Если не задан —
|
||||||
|
* читается из useOrgSettings().allowFractionalPrices. */
|
||||||
allowFractional?: boolean
|
allowFractional?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
|
@ -47,34 +49,50 @@ interface MoneyInputProps {
|
||||||
|
|
||||||
/** Денежное поле: только цифры (+ точка/запятая если allowFractional),
|
/** Денежное поле: только цифры (+ точка/запятая если allowFractional),
|
||||||
* справа — символ валюты (₸/$/€). При allowFractional=false дробная часть
|
* справа — символ валюты (₸/$/€). При allowFractional=false дробная часть
|
||||||
* отбрасывается на лету и отображается целым; иначе — два знака после
|
* отбрасывается на лету и сразу синхронизируется в state наружу; иначе —
|
||||||
* запятой. Пустое поле → null. */
|
* две цифры после запятой. Пустое поле → null. */
|
||||||
export function MoneyInput({
|
export function MoneyInput({
|
||||||
value, onChange, currencyCode, currencySymbol, allowFractional = false,
|
value, onChange, currencyCode, currencySymbol, allowFractional,
|
||||||
disabled, placeholder = '0', className,
|
disabled, placeholder = '0', className,
|
||||||
}: MoneyInputProps) {
|
}: 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 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 (
|
return (
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode={allowFractional ? 'decimal' : 'numeric'}
|
inputMode={fractional ? 'decimal' : 'numeric'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={display}
|
value={display}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onFocus={(e) => e.currentTarget.select()}
|
onFocus={(e) => e.currentTarget.select()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let raw = e.target.value.replace(',', '.')
|
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 (raw === '') return onChange(null)
|
||||||
if (allowFractional) {
|
if (fractional) {
|
||||||
const parts = raw.split('.')
|
const parts = raw.split('.')
|
||||||
raw = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw
|
raw = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw
|
||||||
}
|
}
|
||||||
const n = Number(raw)
|
const n = Number(raw)
|
||||||
if (!Number.isFinite(n)) return
|
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')}
|
className={cn(inputClass, 'pr-10 text-right tabular-nums')}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ export function ProductEditPage() {
|
||||||
onChange={(n) => setForm({ ...form, purchasePrice: n == null ? '' : String(n) })}
|
onChange={(n) => setForm({ ...form, purchasePrice: n == null ? '' : String(n) })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
|
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}
|
currencySymbol={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{org.data?.multiCurrencyEnabled && (
|
||||||
|
|
@ -379,7 +379,7 @@ export function ProductEditPage() {
|
||||||
onChange={(n) => updatePrice(i, { amount: n ?? 0 })}
|
onChange={(n) => updatePrice(i, { amount: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === p.currencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
|
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}
|
currencySymbol={currencies.data?.find((c) => c.id === p.currencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{org.data?.multiCurrencyEnabled && (
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ export function ProductsPage() {
|
||||||
onChange={(n) => { setFilters({ ...filters, purchasePriceFrom: n }); setPage(1) }}
|
onChange={(n) => { setFilters({ ...filters, purchasePriceFrom: n }); setPage(1) }}
|
||||||
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
||||||
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false}
|
|
||||||
placeholder="от"
|
placeholder="от"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -240,7 +240,7 @@ export function ProductsPage() {
|
||||||
onChange={(n) => { setFilters({ ...filters, purchasePriceTo: n }); setPage(1) }}
|
onChange={(n) => { setFilters({ ...filters, purchasePriceTo: n }); setPage(1) }}
|
||||||
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
||||||
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false}
|
|
||||||
placeholder="до"
|
placeholder="до"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -289,14 +289,14 @@ export function RetailSaleEditPage() {
|
||||||
onChange={(n) => setForm({ ...form, paidCash: n ?? 0 })}
|
onChange={(n) => setForm({ ...form, paidCash: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false} />
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Получено картой">
|
<Field label="Получено картой">
|
||||||
<MoneyInput value={form.paidCard} disabled={isPosted}
|
<MoneyInput value={form.paidCard} disabled={isPosted}
|
||||||
onChange={(n) => setForm({ ...form, paidCard: n ?? 0 })}
|
onChange={(n) => setForm({ ...form, paidCard: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false} />
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Примечание" className="md:col-span-3">
|
<Field label="Примечание" className="md:col-span-3">
|
||||||
<TextArea rows={2} value={form.notes} disabled={isPosted}
|
<TextArea rows={2} value={form.notes} disabled={isPosted}
|
||||||
|
|
@ -348,7 +348,7 @@ export function RetailSaleEditPage() {
|
||||||
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false} />
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<MoneyInput disabled={isPosted}
|
<MoneyInput disabled={isPosted}
|
||||||
|
|
@ -356,7 +356,7 @@ export function RetailSaleEditPage() {
|
||||||
onChange={(n) => updateLine(i, { discount: n ?? 0 })}
|
onChange={(n) => updateLine(i, { discount: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false} />
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 text-right font-mono font-semibold">
|
<td className="py-2 px-3 text-right font-mono font-semibold">
|
||||||
{lineTotal(l).toLocaleString('ru', { maximumFractionDigits: 2 })}
|
{lineTotal(l).toLocaleString('ru', { maximumFractionDigits: 2 })}
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ export function SupplyEditPage() {
|
||||||
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
|
||||||
allowFractional={org.data?.allowFractionalPrices ?? false} />
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 text-right font-mono font-semibold">
|
<td className="py-2 px-3 text-right font-mono font-semibold">
|
||||||
{lineTotal(l).toLocaleString('ru', { maximumFractionDigits: 2 })}
|
{lineTotal(l).toLocaleString('ru', { maximumFractionDigits: 2 })}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue