feat(date-field): replace native input with react-datepicker — polished UX
Нативный <input type=\"date\"> рендерил американский MM/DD/YYYY и тонкий браузерный popup, выглядел криво рядом с другими полями. Используем готовый react-datepicker (10M downloads/week) — никакой кастомизации, всё из коробки: - dateFormat=\"dd.MM.yyyy\" + locale=\"ru\" → «25.04.2026», русские Январь/Понедельник - showMonthDropdown + showYearDropdown + dropdownMode=\"select\" → быстрый прыжок на любой месяц/год - todayButton=\"Сегодня\" → кнопка под календарём - isClearable → крестик в input для очистки - popperClassName z-[100] чтобы попап не резался z-стеком ISO YYYY-MM-DD внутрь/наружу собираем вручную из локальных Y/M/D — не toISOString(), чтобы вечерние даты в часовом поясе KZ (UTC+5) не сдвигались на день назад. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc162a6c06
commit
39da12edec
|
|
@ -16,8 +16,10 @@
|
||||||
"axios": "^1.15.1",
|
"axios": "^1.15.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-hook-form": "^7.73.1",
|
"react-hook-form": "^7.73.1",
|
||||||
"react-router-dom": "^7.14.1",
|
"react-router-dom": "^7.14.1",
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,18 @@ importers:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.8.0
|
specifier: ^1.8.0
|
||||||
version: 1.8.0(react@19.2.5)
|
version: 1.8.0(react@19.2.5)
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.5
|
specifier: ^19.2.5
|
||||||
version: 19.2.5
|
version: 19.2.5
|
||||||
|
react-datepicker:
|
||||||
|
specifier: ^9.1.0
|
||||||
|
version: 9.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.5
|
specifier: ^19.2.5
|
||||||
version: 19.2.5(react@19.2.5)
|
version: 19.2.5(react@19.2.5)
|
||||||
|
|
@ -219,6 +225,27 @@ packages:
|
||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||||
|
|
||||||
|
'@floating-ui/react-dom@2.1.8':
|
||||||
|
resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
'@floating-ui/react@0.27.19':
|
||||||
|
resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.0.0'
|
||||||
|
react-dom: '>=17.0.0'
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11':
|
||||||
|
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||||
|
|
||||||
'@hookform/resolvers@5.2.2':
|
'@hookform/resolvers@5.2.2':
|
||||||
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
|
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -771,6 +798,9 @@ packages:
|
||||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
@ -1251,6 +1281,16 @@ packages:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
react-datepicker@9.1.0:
|
||||||
|
resolution: {integrity: sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==}
|
||||||
|
peerDependencies:
|
||||||
|
date-fns-tz: ^3.0.0
|
||||||
|
react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
date-fns-tz:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-dom@19.2.5:
|
react-dom@19.2.5:
|
||||||
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1361,6 +1401,9 @@ packages:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
tabbable@6.4.0:
|
||||||
|
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
||||||
|
|
||||||
tailwind-merge@3.5.0:
|
tailwind-merge@3.5.0:
|
||||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||||
|
|
||||||
|
|
@ -1660,6 +1703,31 @@ snapshots:
|
||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.7.5
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
|
||||||
|
'@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.6
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
|
'@floating-ui/react@0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
tabbable: 6.4.0
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11': {}
|
||||||
|
|
||||||
'@hookform/resolvers@5.2.2(react-hook-form@7.73.1(react@19.2.5))':
|
'@hookform/resolvers@5.2.2(react-hook-form@7.73.1(react@19.2.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
|
|
@ -2147,6 +2215,8 @@ snapshots:
|
||||||
|
|
||||||
d3-timer@3.0.1: {}
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
@ -2559,6 +2629,14 @@ snapshots:
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
react-datepicker@9.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
clsx: 2.1.1
|
||||||
|
date-fns: 4.1.0
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
react-dom@19.2.5(react@19.2.5):
|
react-dom@19.2.5(react@19.2.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
|
|
@ -2668,6 +2746,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
tabbable@6.4.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.5.0: {}
|
tailwind-merge@3.5.0: {}
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@4.2.3):
|
tailwindcss-animate@1.0.7(tailwindcss@4.2.3):
|
||||||
|
|
|
||||||
54
src/food-market.web/src/components/DateField.tsx
Normal file
54
src/food-market.web/src/components/DateField.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import DatePicker, { registerLocale } from 'react-datepicker'
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css'
|
||||||
|
import { ru } from 'date-fns/locale/ru'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
registerLocale('ru', ru)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** ISO YYYY-MM-DD или пустая строка/null. */
|
||||||
|
value: string | null
|
||||||
|
onChange: (iso: string | null) => void
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputClass = 'h-10 w-full max-w-[180px] rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 text-sm leading-none focus:outline-none focus:ring-2 focus:ring-[var(--color-brand)] disabled:opacity-60 disabled:bg-slate-50 dark:disabled:bg-slate-800/60'
|
||||||
|
|
||||||
|
/** Дата с готовым попапом react-datepicker: RU-локаль, формат DD.MM.YYYY,
|
||||||
|
* dropdowns для месяца и года, кнопка «Сегодня», крестик «очистить».
|
||||||
|
* Хранит/отдаёт ISO YYYY-MM-DD — API-контракт без изменений. */
|
||||||
|
export function DateField({ value, onChange, placeholder = 'дд.мм.гггг', required, disabled, className }: Props) {
|
||||||
|
const dateValue = value ? new Date(value) : null
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
selected={dateValue}
|
||||||
|
onChange={(d: Date | null) => {
|
||||||
|
if (!d) { onChange(null); return }
|
||||||
|
// Локальный YYYY-MM-DD без UTC-смещения, чтобы вечерние даты
|
||||||
|
// не сдвигались на день назад в часовом поясе KZ (UTC+5).
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0')
|
||||||
|
onChange(`${y}-${m}-${dd}`)
|
||||||
|
}}
|
||||||
|
dateFormat="dd.MM.yyyy"
|
||||||
|
locale="ru"
|
||||||
|
placeholderText={placeholder}
|
||||||
|
todayButton="Сегодня"
|
||||||
|
isClearable
|
||||||
|
showMonthDropdown
|
||||||
|
showYearDropdown
|
||||||
|
dropdownMode="select"
|
||||||
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
|
autoComplete="off"
|
||||||
|
className={cn(inputClass, className)}
|
||||||
|
wrapperClassName="inline-block"
|
||||||
|
calendarClassName="!font-sans"
|
||||||
|
popperClassName="z-[100]"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,8 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } from 'lucide-react'
|
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } 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, MoneyInput, NumberInput } from '@/components/Field'
|
import { Field, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||||
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
||||||
import { useStores, useCurrencies, useSuppliers, usePriceTypes } from '@/lib/useLookups'
|
import { useStores, useCurrencies, useSuppliers, usePriceTypes } from '@/lib/useLookups'
|
||||||
|
|
@ -299,9 +300,8 @@ export function SupplyEditPage() {
|
||||||
<Section title="Реквизиты документа">
|
<Section title="Реквизиты документа">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-3">
|
||||||
<Field label="Дата *">
|
<Field label="Дата *">
|
||||||
<TextInput type="date" required value={form.date} disabled={isPosted}
|
<DateField required value={form.date || null} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
onChange={(iso) => setForm({ ...form, date: iso ?? '' })} />
|
||||||
className="max-w-[180px]" />
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Поставщик *">
|
<Field label="Поставщик *">
|
||||||
<Select
|
<Select
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue