feat(date-field): replace native input with react-datepicker — polished UX
Some checks are pending
Some checks are pending
Нативный <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
33e1572c3a
commit
d28c6e703a
|
|
@ -16,8 +16,10 @@
|
|||
"axios": "^1.15.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.5",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-hook-form": "^7.73.1",
|
||||
"react-router-dom": "^7.14.1",
|
||||
|
|
|
|||
|
|
@ -26,12 +26,18 @@ importers:
|
|||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
lucide-react:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0(react@19.2.5)
|
||||
react:
|
||||
specifier: ^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:
|
||||
specifier: ^19.2.5
|
||||
version: 19.2.5(react@19.2.5)
|
||||
|
|
@ -219,6 +225,27 @@ packages:
|
|||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
|
||||
peerDependencies:
|
||||
|
|
@ -771,6 +798,9 @@ packages:
|
|||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -1251,6 +1281,16 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
||||
peerDependencies:
|
||||
|
|
@ -1361,6 +1401,9 @@ packages:
|
|||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tabbable@6.4.0:
|
||||
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
||||
|
||||
tailwind-merge@3.5.0:
|
||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||
|
||||
|
|
@ -1660,6 +1703,31 @@ snapshots:
|
|||
'@eslint/core': 0.17.0
|
||||
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))':
|
||||
dependencies:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
|
|
@ -2147,6 +2215,8 @@ snapshots:
|
|||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
|
@ -2559,6 +2629,14 @@ snapshots:
|
|||
|
||||
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):
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
|
@ -2668,6 +2746,8 @@ snapshots:
|
|||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
tabbable@6.4.0: {}
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
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 { api } from '@/lib/api'
|
||||
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 { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
||||
import { useStores, useCurrencies, useSuppliers, usePriceTypes } from '@/lib/useLookups'
|
||||
|
|
@ -299,9 +300,8 @@ export function SupplyEditPage() {
|
|||
<Section title="Реквизиты документа">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-3">
|
||||
<Field label="Дата *">
|
||||
<TextInput type="date" required value={form.date} disabled={isPosted}
|
||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
||||
className="max-w-[180px]" />
|
||||
<DateField required value={form.date || null} disabled={isPosted}
|
||||
onChange={(iso) => setForm({ ...form, date: iso ?? '' })} />
|
||||
</Field>
|
||||
<Field label="Поставщик *">
|
||||
<Select
|
||||
|
|
|
|||
Loading…
Reference in a new issue