feat(forms): MoneyInput/NumberInput + select-пагинация + Range на бэкенде
UI: - Pagination: ввод страницы заменён на select (option 1..totalPages), по выбору сразу setPage. Стрелки ← → остаются. - Field.tsx: добавлены MoneyInput (decimal + суффикс ₸/$/€) и NumberInput (decimal без валюты). Оба фильтруют ввод регулярно (только цифры + точка/запятая→точка), при focus — выделяют значение. - ProductEditPage: purchasePrice / vat / minStock / maxStock / amount в ценах продаж переведены на новые компоненты; символ валюты — из выбранной валюты позиции/закупки или из defaultCurrencySymbol орг. - SupplyEditPage / RetailSaleEditPage: quantity/unitPrice/discount в строках, paidCash/paidCard в шапке — на NumberInput/MoneyInput с символом из form.currencyId. - CountriesPage: vatRate — NumberInput. API: - ProductInput / ProductPriceInput / SupplyLineInput / RetailSaleLineInput / RetailSaleInput — добавлены [Range(0,1e10)] на денежные/количественные поля и [Range(0,100)] на проценты. ASP.NET автоматически валидирует и возвращает 400 при выходе. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
adf2c90904
commit
23e29be21b
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using foodmarket.Application.Common;
|
using foodmarket.Application.Common;
|
||||||
using foodmarket.Application.Inventory;
|
using foodmarket.Application.Inventory;
|
||||||
using foodmarket.Domain.Inventory;
|
using foodmarket.Domain.Inventory;
|
||||||
|
|
@ -46,7 +47,10 @@ public record SupplyDto(
|
||||||
decimal Total, DateTime? PostedAt,
|
decimal Total, DateTime? PostedAt,
|
||||||
IReadOnlyList<SupplyLineDto> Lines);
|
IReadOnlyList<SupplyLineDto> Lines);
|
||||||
|
|
||||||
public record SupplyLineInput(Guid ProductId, decimal Quantity, decimal UnitPrice);
|
public record SupplyLineInput(
|
||||||
|
Guid ProductId,
|
||||||
|
[Range(0, 1e10)] decimal Quantity,
|
||||||
|
[Range(0, 1e10)] decimal UnitPrice);
|
||||||
public record SupplyInput(
|
public record SupplyInput(
|
||||||
DateTime Date, Guid SupplierId, Guid StoreId, Guid CurrencyId,
|
DateTime Date, Guid SupplierId, Guid StoreId, Guid CurrencyId,
|
||||||
string? SupplierInvoiceNumber, DateTime? SupplierInvoiceDate,
|
string? SupplierInvoiceNumber, DateTime? SupplierInvoiceDate,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using foodmarket.Application.Common;
|
using foodmarket.Application.Common;
|
||||||
using foodmarket.Application.Inventory;
|
using foodmarket.Application.Inventory;
|
||||||
using foodmarket.Domain.Inventory;
|
using foodmarket.Domain.Inventory;
|
||||||
|
|
@ -47,10 +48,17 @@ public record RetailSaleDto(
|
||||||
string? Notes, DateTime? PostedAt,
|
string? Notes, DateTime? PostedAt,
|
||||||
IReadOnlyList<RetailSaleLineDto> Lines);
|
IReadOnlyList<RetailSaleLineDto> Lines);
|
||||||
|
|
||||||
public record RetailSaleLineInput(Guid ProductId, decimal Quantity, decimal UnitPrice, decimal Discount, decimal VatPercent);
|
public record RetailSaleLineInput(
|
||||||
|
Guid ProductId,
|
||||||
|
[Range(0, 1e10)] decimal Quantity,
|
||||||
|
[Range(0, 1e10)] decimal UnitPrice,
|
||||||
|
[Range(0, 1e10)] decimal Discount,
|
||||||
|
[Range(0, 100)] decimal VatPercent);
|
||||||
public record RetailSaleInput(
|
public record RetailSaleInput(
|
||||||
DateTime Date, Guid StoreId, Guid? RetailPointId, Guid? CustomerId, Guid CurrencyId,
|
DateTime Date, Guid StoreId, Guid? RetailPointId, Guid? CustomerId, Guid CurrencyId,
|
||||||
PaymentMethod Payment, decimal PaidCash, decimal PaidCard,
|
PaymentMethod Payment,
|
||||||
|
[Range(0, 1e10)] decimal PaidCash,
|
||||||
|
[Range(0, 1e10)] decimal PaidCard,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
IReadOnlyList<RetailSaleLineInput> Lines);
|
IReadOnlyList<RetailSaleLineInput> Lines);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using foodmarket.Domain.Catalog;
|
using foodmarket.Domain.Catalog;
|
||||||
|
|
||||||
namespace foodmarket.Application.Catalog;
|
namespace foodmarket.Application.Catalog;
|
||||||
|
|
@ -73,14 +74,14 @@ public record CounterpartyInput(
|
||||||
string? Address, string? Phone, string? Email,
|
string? Address, string? Phone, string? Email,
|
||||||
string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive = true);
|
string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive = true);
|
||||||
public record ProductBarcodeInput(string Code, BarcodeType Type = BarcodeType.Ean13, bool IsPrimary = false);
|
public record ProductBarcodeInput(string Code, BarcodeType Type = BarcodeType.Ean13, bool IsPrimary = false);
|
||||||
public record ProductPriceInput(Guid PriceTypeId, decimal Amount, Guid CurrencyId);
|
public record ProductPriceInput(Guid PriceTypeId, [Range(0, 1e10)] decimal Amount, Guid CurrencyId);
|
||||||
public record ProductInput(
|
public record ProductInput(
|
||||||
string Name, string? Article, string? Description,
|
string Name, string? Article, string? Description,
|
||||||
Guid UnitOfMeasureId, decimal? Vat, bool VatEnabled,
|
Guid UnitOfMeasureId, [Range(0, 100)] decimal? Vat, bool VatEnabled,
|
||||||
Guid? ProductGroupId, Guid? DefaultSupplierId, Guid? CountryOfOriginId,
|
Guid? ProductGroupId, Guid? DefaultSupplierId, Guid? CountryOfOriginId,
|
||||||
bool IsService = false, Packaging Packaging = Packaging.Piece, bool IsMarked = false,
|
bool IsService = false, Packaging Packaging = Packaging.Piece, bool IsMarked = false,
|
||||||
decimal? MinStock = null, decimal? MaxStock = null,
|
[Range(0, 1e10)] decimal? MinStock = null, [Range(0, 1e10)] decimal? MaxStock = null,
|
||||||
decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null,
|
[Range(0, 1e10)] decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null,
|
||||||
string? ImageUrl = null, bool IsActive = true,
|
string? ImageUrl = null, bool IsActive = true,
|
||||||
IReadOnlyList<ProductPriceInput>? Prices = null,
|
IReadOnlyList<ProductPriceInput>? Prices = null,
|
||||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,88 @@ export function Select(props: SelectHTMLAttributes<HTMLSelectElement>) {
|
||||||
return <select {...props} className={cn(inputClass, props.className)} />
|
return <select {...props} className={cn(inputClass, props.className)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MoneyInputProps {
|
||||||
|
value: number | null | undefined
|
||||||
|
onChange: (v: number | null) => void
|
||||||
|
currencyCode?: string | null
|
||||||
|
currencySymbol?: string | null
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Денежное поле: только цифры + точка/запятая (запятая → точка), 2 знака,
|
||||||
|
* справа — символ валюты (₸/$/€). Запятая, пробелы и буквы фильтруются.
|
||||||
|
* Пустое поле → null. */
|
||||||
|
export function MoneyInput({
|
||||||
|
value, onChange, currencyCode, currencySymbol, disabled, placeholder = '0', className,
|
||||||
|
}: MoneyInputProps) {
|
||||||
|
const suffix = currencySymbol || currencyCode || '₸'
|
||||||
|
const display = value == null ? '' : String(value)
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
disabled={disabled}
|
||||||
|
value={display}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onFocus={(e) => e.currentTarget.select()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value.replace(',', '.').replace(/[^\d.]/g, '')
|
||||||
|
if (raw === '') return onChange(null)
|
||||||
|
// Не более одной точки.
|
||||||
|
const parts = raw.split('.')
|
||||||
|
const cleaned = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw
|
||||||
|
const n = Number(cleaned)
|
||||||
|
if (!Number.isFinite(n)) return
|
||||||
|
onChange(n)
|
||||||
|
}}
|
||||||
|
className={cn(inputClass, 'pr-10 text-right tabular-nums')}
|
||||||
|
/>
|
||||||
|
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sm text-slate-500">
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberInputProps {
|
||||||
|
value: number | null | undefined
|
||||||
|
onChange: (v: number | null) => void
|
||||||
|
step?: number
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Числовое поле без валюты: только цифры + точка/запятая. Пустое → null. */
|
||||||
|
export function NumberInput({
|
||||||
|
value, onChange, disabled, placeholder = '0', className,
|
||||||
|
}: NumberInputProps) {
|
||||||
|
const display = value == null ? '' : String(value)
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
disabled={disabled}
|
||||||
|
value={display}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onFocus={(e) => e.currentTarget.select()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value.replace(',', '.').replace(/[^\d.]/g, '')
|
||||||
|
if (raw === '') return onChange(null)
|
||||||
|
const parts = raw.split('.')
|
||||||
|
const cleaned = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw
|
||||||
|
const n = Number(cleaned)
|
||||||
|
if (!Number.isFinite(n)) return
|
||||||
|
onChange(n)
|
||||||
|
}}
|
||||||
|
className={cn(inputClass, 'text-right tabular-nums', className)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Checkbox({
|
export function Checkbox({
|
||||||
label,
|
label,
|
||||||
checked,
|
checked,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
|
|
@ -11,24 +9,13 @@ export function Pagination({ page, pageSize, total, onPageChange }: PaginationPr
|
||||||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||||
const from = (page - 1) * pageSize + 1
|
const from = (page - 1) * pageSize + 1
|
||||||
const to = Math.min(page * pageSize, total)
|
const to = Math.min(page * pageSize, total)
|
||||||
const [jumpValue, setJumpValue] = useState<string>(String(page))
|
|
||||||
|
|
||||||
useEffect(() => { setJumpValue(String(page)) }, [page])
|
|
||||||
|
|
||||||
if (total === 0) return null
|
if (total === 0) return null
|
||||||
|
|
||||||
const commitJump = () => {
|
|
||||||
const n = parseInt(jumpValue, 10)
|
|
||||||
if (!Number.isFinite(n)) { setJumpValue(String(page)); return }
|
|
||||||
const clamped = Math.min(Math.max(1, n), totalPages)
|
|
||||||
if (clamped !== page) onPageChange(clamped)
|
|
||||||
setJumpValue(String(clamped))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between mt-3 text-sm text-slate-500">
|
<div className="flex flex-wrap items-center justify-between gap-3 mt-3 text-sm text-slate-500">
|
||||||
<span>{from}–{to} из {total}</span>
|
<span>{from}–{to} из {total}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
onClick={() => onPageChange(page - 1)}
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
|
@ -38,16 +25,15 @@ export function Pagination({ page, pageSize, total, onPageChange }: PaginationPr
|
||||||
</button>
|
</button>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span>Страница</span>
|
<span>Страница</span>
|
||||||
<input
|
<select
|
||||||
type="number"
|
value={page}
|
||||||
min={1}
|
onChange={(e) => onPageChange(Number(e.target.value))}
|
||||||
max={totalPages}
|
className="px-1.5 py-0.5 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 tabular-nums"
|
||||||
value={jumpValue}
|
>
|
||||||
onChange={(e) => setJumpValue(e.target.value)}
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
|
||||||
onBlur={commitJump}
|
<option key={p} value={p}>{p}</option>
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitJump() } }}
|
))}
|
||||||
className="w-14 px-1.5 py-0.5 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-center tabular-nums"
|
</select>
|
||||||
/>
|
|
||||||
<span>из {totalPages}</span>
|
<span>из {totalPages}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Pagination } from '@/components/Pagination'
|
||||||
import { SearchBar } from '@/components/SearchBar'
|
import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Select } from '@/components/Field'
|
import { Field, TextInput, Select, NumberInput } from '@/components/Field'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import { useCurrencies } from '@/lib/useLookups'
|
import { useCurrencies } from '@/lib/useLookups'
|
||||||
import type { Country } from '@/lib/types'
|
import type { Country } from '@/lib/types'
|
||||||
|
|
@ -112,11 +112,9 @@ export function CountriesPage() {
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Ставка НДС, %">
|
<Field label="Ставка НДС, %">
|
||||||
<TextInput
|
<NumberInput
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={form.vatRate}
|
value={form.vatRate}
|
||||||
onChange={(e) => setForm({ ...form, vatRate: Number(e.target.value) })}
|
onChange={(n) => setForm({ ...form, vatRate: n ?? 0 })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Plus, Trash2, Save } from 'lucide-react'
|
import { ArrowLeft, Plus, Trash2, Save } from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select, Checkbox } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||||
import {
|
import {
|
||||||
useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes, useSuppliers,
|
useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes, useSuppliers,
|
||||||
} from '@/lib/useLookups'
|
} from '@/lib/useLookups'
|
||||||
|
|
@ -278,10 +278,9 @@ export function ProductEditPage() {
|
||||||
{org.data?.showVatEnabledOnProduct && form.vatEnabled && (
|
{org.data?.showVatEnabledOnProduct && form.vatEnabled && (
|
||||||
<Grid cols={3}>
|
<Grid cols={3}>
|
||||||
<Field label="Ставка НДС, %">
|
<Field label="Ставка НДС, %">
|
||||||
<TextInput
|
<NumberInput
|
||||||
type="number" step="0.01" min="0"
|
|
||||||
value={form.vat}
|
value={form.vat}
|
||||||
onChange={(e) => setForm({ ...form, vat: Number(e.target.value) })}
|
onChange={(n) => setForm({ ...form, vat: n ?? 0 })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -303,7 +302,12 @@ export function ProductEditPage() {
|
||||||
<Section title="Закупка">
|
<Section title="Закупка">
|
||||||
<Grid cols={4}>
|
<Grid cols={4}>
|
||||||
<Field label="Закупочная цена">
|
<Field label="Закупочная цена">
|
||||||
<TextInput type="number" step="0.01" value={form.purchasePrice} onChange={(e) => setForm({ ...form, purchasePrice: e.target.value })} />
|
<MoneyInput
|
||||||
|
value={form.purchasePrice === '' ? null : Number(form.purchasePrice)}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Field label="Валюта закупки">
|
<Field label="Валюта закупки">
|
||||||
|
|
@ -320,10 +324,18 @@ export function ProductEditPage() {
|
||||||
<AdvancedSection>
|
<AdvancedSection>
|
||||||
<Grid cols={4}>
|
<Grid cols={4}>
|
||||||
<Field label="Минимальный остаток (для уведомления)">
|
<Field label="Минимальный остаток (для уведомления)">
|
||||||
<TextInput type="number" step="0.001" value={form.minStock} onChange={(e) => setForm({ ...form, minStock: e.target.value })} placeholder="—" />
|
<NumberInput
|
||||||
|
value={form.minStock === '' ? null : Number(form.minStock)}
|
||||||
|
onChange={(n) => setForm({ ...form, minStock: n == null ? '' : String(n) })}
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Максимальный остаток (для автозаказа)">
|
<Field label="Максимальный остаток (для автозаказа)">
|
||||||
<TextInput type="number" step="0.001" value={form.maxStock} onChange={(e) => setForm({ ...form, maxStock: e.target.value })} placeholder="—" />
|
<NumberInput
|
||||||
|
value={form.maxStock === '' ? null : Number(form.maxStock)}
|
||||||
|
onChange={(n) => setForm({ ...form, maxStock: n == null ? '' : String(n) })}
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Grid>
|
</Grid>
|
||||||
</AdvancedSection>
|
</AdvancedSection>
|
||||||
|
|
@ -344,11 +356,13 @@ export function ProductEditPage() {
|
||||||
{priceTypes.data?.map((pt) => <option key={pt.id} value={pt.id}>{pt.name}</option>)}
|
{priceTypes.data?.map((pt) => <option key={pt.id} value={pt.id}>{pt.name}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-3 flex items-center gap-2">
|
<div className="col-span-3">
|
||||||
<TextInput type="number" step="0.01" value={p.amount} onChange={(e) => updatePrice(i, { amount: Number(e.target.value) })} />
|
<MoneyInput
|
||||||
{!org.data?.multiCurrencyEnabled && (
|
value={p.amount}
|
||||||
<span className="text-sm text-slate-500">{org.data?.defaultCurrencySymbol ?? ''}</span>
|
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}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
|
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select, MoneyInput, NumberInput } from '@/components/Field'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -285,12 +285,16 @@ export function RetailSaleEditPage() {
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Получено наличными">
|
<Field label="Получено наличными">
|
||||||
<TextInput type="number" step="0.01" value={form.paidCash} disabled={isPosted}
|
<MoneyInput value={form.paidCash} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, paidCash: Number(e.target.value) })} />
|
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} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Получено картой">
|
<Field label="Получено картой">
|
||||||
<TextInput type="number" step="0.01" value={form.paidCard} disabled={isPosted}
|
<MoneyInput value={form.paidCard} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, paidCard: Number(e.target.value) })} />
|
onChange={(n) => 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} />
|
||||||
</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}
|
||||||
|
|
@ -332,19 +336,23 @@ export function RetailSaleEditPage() {
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<TextInput type="number" step="0.001" disabled={isPosted}
|
<NumberInput disabled={isPosted}
|
||||||
className="text-right font-mono" value={l.quantity}
|
value={l.quantity}
|
||||||
onChange={(e) => updateLine(i, { quantity: Number(e.target.value) })} />
|
onChange={(n) => updateLine(i, { quantity: n ?? 0 })} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<TextInput type="number" step="0.01" disabled={isPosted}
|
<MoneyInput disabled={isPosted}
|
||||||
className="text-right font-mono" value={l.unitPrice}
|
value={l.unitPrice}
|
||||||
onChange={(e) => updateLine(i, { unitPrice: Number(e.target.value) })} />
|
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
||||||
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<TextInput type="number" step="0.01" disabled={isPosted}
|
<MoneyInput disabled={isPosted}
|
||||||
className="text-right font-mono" value={l.discount}
|
value={l.discount}
|
||||||
onChange={(e) => updateLine(i, { discount: Number(e.target.value) })} />
|
onChange={(n) => updateLine(i, { discount: n ?? 0 })}
|
||||||
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} />
|
||||||
</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 })}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
|
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select, MoneyInput, NumberInput } from '@/components/Field'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -312,16 +312,16 @@ export function SupplyEditPage() {
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<TextInput type="number" step="0.001" disabled={isPosted}
|
<NumberInput disabled={isPosted}
|
||||||
className="text-right font-mono"
|
|
||||||
value={l.quantity}
|
value={l.quantity}
|
||||||
onChange={(e) => updateLine(i, { quantity: Number(e.target.value) })} />
|
onChange={(n) => updateLine(i, { quantity: n ?? 0 })} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<TextInput type="number" step="0.01" disabled={isPosted}
|
<MoneyInput disabled={isPosted}
|
||||||
className="text-right font-mono"
|
|
||||||
value={l.unitPrice}
|
value={l.unitPrice}
|
||||||
onChange={(e) => updateLine(i, { unitPrice: Number(e.target.value) })} />
|
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
|
||||||
|
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
|
||||||
|
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} />
|
||||||
</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