diff --git a/src/food-market.api/Controllers/Purchases/SuppliesController.cs b/src/food-market.api/Controllers/Purchases/SuppliesController.cs index d289a63..689a082 100644 --- a/src/food-market.api/Controllers/Purchases/SuppliesController.cs +++ b/src/food-market.api/Controllers/Purchases/SuppliesController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using foodmarket.Application.Common; using foodmarket.Application.Inventory; using foodmarket.Domain.Inventory; @@ -46,7 +47,10 @@ public record SupplyDto( decimal Total, DateTime? PostedAt, IReadOnlyList 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( DateTime Date, Guid SupplierId, Guid StoreId, Guid CurrencyId, string? SupplierInvoiceNumber, DateTime? SupplierInvoiceDate, diff --git a/src/food-market.api/Controllers/Sales/RetailSalesController.cs b/src/food-market.api/Controllers/Sales/RetailSalesController.cs index a9a46e5..88cd012 100644 --- a/src/food-market.api/Controllers/Sales/RetailSalesController.cs +++ b/src/food-market.api/Controllers/Sales/RetailSalesController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using foodmarket.Application.Common; using foodmarket.Application.Inventory; using foodmarket.Domain.Inventory; @@ -47,10 +48,17 @@ public record RetailSaleDto( string? Notes, DateTime? PostedAt, IReadOnlyList 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( 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, IReadOnlyList Lines); diff --git a/src/food-market.application/Catalog/CatalogDtos.cs b/src/food-market.application/Catalog/CatalogDtos.cs index fe2144e..0ec26ed 100644 --- a/src/food-market.application/Catalog/CatalogDtos.cs +++ b/src/food-market.application/Catalog/CatalogDtos.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using foodmarket.Domain.Catalog; namespace foodmarket.Application.Catalog; @@ -73,14 +74,14 @@ public record CounterpartyInput( string? Address, string? Phone, string? Email, 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 ProductPriceInput(Guid PriceTypeId, decimal Amount, Guid CurrencyId); +public record ProductPriceInput(Guid PriceTypeId, [Range(0, 1e10)] decimal Amount, Guid CurrencyId); public record ProductInput( 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, bool IsService = false, Packaging Packaging = Packaging.Piece, bool IsMarked = false, - decimal? MinStock = null, decimal? MaxStock = null, - decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null, + [Range(0, 1e10)] decimal? MinStock = null, [Range(0, 1e10)] decimal? MaxStock = null, + [Range(0, 1e10)] decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null, string? ImageUrl = null, bool IsActive = true, IReadOnlyList? Prices = null, IReadOnlyList? Barcodes = null); diff --git a/src/food-market.web/src/components/Field.tsx b/src/food-market.web/src/components/Field.tsx index 509a06f..3f3651c 100644 --- a/src/food-market.web/src/components/Field.tsx +++ b/src/food-market.web/src/components/Field.tsx @@ -33,6 +33,88 @@ export function Select(props: SelectHTMLAttributes) { return 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')} + /> + + {suffix} + + + ) +} + +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 ( + 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({ label, checked, diff --git a/src/food-market.web/src/components/Pagination.tsx b/src/food-market.web/src/components/Pagination.tsx index c5c5ab5..21f48a7 100644 --- a/src/food-market.web/src/components/Pagination.tsx +++ b/src/food-market.web/src/components/Pagination.tsx @@ -1,5 +1,3 @@ -import { useEffect, useState } from 'react' - interface PaginationProps { page: 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 from = (page - 1) * pageSize + 1 const to = Math.min(page * pageSize, total) - const [jumpValue, setJumpValue] = useState(String(page)) - - useEffect(() => { setJumpValue(String(page)) }, [page]) 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 ( -
+
{from}–{to} из {total} -
+
Страница - setJumpValue(e.target.value)} - onBlur={commitJump} - 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" - /> + из {totalPages}
diff --git a/src/food-market.web/src/pages/ProductEditPage.tsx b/src/food-market.web/src/pages/ProductEditPage.tsx index 1eedddd..76db28f 100644 --- a/src/food-market.web/src/pages/ProductEditPage.tsx +++ b/src/food-market.web/src/pages/ProductEditPage.tsx @@ -4,7 +4,7 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query' import { ArrowLeft, Plus, Trash2, Save } from 'lucide-react' import { api } from '@/lib/api' 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 { useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes, useSuppliers, } from '@/lib/useLookups' @@ -278,10 +278,9 @@ export function ProductEditPage() { {org.data?.showVatEnabledOnProduct && form.vatEnabled && ( - setForm({ ...form, vat: Number(e.target.value) })} + onChange={(n) => setForm({ ...form, vat: n ?? 0 })} /> @@ -303,7 +302,12 @@ export function ProductEditPage() {
- setForm({ ...form, purchasePrice: e.target.value })} /> + 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} + /> {org.data?.multiCurrencyEnabled && ( @@ -320,10 +324,18 @@ export function ProductEditPage() { - setForm({ ...form, minStock: e.target.value })} placeholder="—" /> + setForm({ ...form, minStock: n == null ? '' : String(n) })} + placeholder="—" + /> - setForm({ ...form, maxStock: e.target.value })} placeholder="—" /> + setForm({ ...form, maxStock: n == null ? '' : String(n) })} + placeholder="—" + /> @@ -344,11 +356,13 @@ export function ProductEditPage() { {priceTypes.data?.map((pt) => )}
-
- updatePrice(i, { amount: Number(e.target.value) })} /> - {!org.data?.multiCurrencyEnabled && ( - {org.data?.defaultCurrencySymbol ?? ''} - )} +
+ 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} + />
{org.data?.multiCurrencyEnabled && (
diff --git a/src/food-market.web/src/pages/RetailSaleEditPage.tsx b/src/food-market.web/src/pages/RetailSaleEditPage.tsx index 9b8ce8f..a22924d 100644 --- a/src/food-market.web/src/pages/RetailSaleEditPage.tsx +++ b/src/food-market.web/src/pages/RetailSaleEditPage.tsx @@ -4,7 +4,7 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query' import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react' import { api } from '@/lib/api' 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 { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' @@ -285,12 +285,16 @@ export function RetailSaleEditPage() { - setForm({ ...form, paidCash: Number(e.target.value) })} /> + 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} /> - setForm({ ...form, paidCard: Number(e.target.value) })} /> + 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} />