diff --git a/src/food-market.web/package.json b/src/food-market.web/package.json index 9711412..70ea679 100644 --- a/src/food-market.web/package.json +++ b/src/food-market.web/package.json @@ -16,10 +16,8 @@ "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-day-picker": "^9.14.0", "react-dom": "^19.2.5", "react-hook-form": "^7.73.1", "react-router-dom": "^7.14.1", diff --git a/src/food-market.web/pnpm-lock.yaml b/src/food-market.web/pnpm-lock.yaml index f4d138e..4b073ca 100644 --- a/src/food-market.web/pnpm-lock.yaml +++ b/src/food-market.web/pnpm-lock.yaml @@ -26,18 +26,12 @@ 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-day-picker: - specifier: ^9.14.0 - version: 9.14.0(react@19.2.5) react-dom: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) @@ -178,9 +172,6 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@date-fns/tz@1.4.1': - resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -396,10 +387,6 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@tabby_ai/hijri-converter@1.0.5': - resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} - engines: {node: '>=16.0.0'} - '@tailwindcss/node@4.2.3': resolution: {integrity: sha512-dhXFXkW2dGvX4r/fi24gyXM0t1mFMrpykQjqrdA4SuavaMagm4SY1u5G2SCJwu1/0x/5RlZJ2VPjP3mKYQfCkA==} @@ -784,12 +771,6 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - date-fns-jalali@4.1.0-0: - resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} - - 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'} @@ -1270,12 +1251,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - react-day-picker@9.14.0: - resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} - engines: {node: '>=18'} - peerDependencies: - react: '>=16.8.0' - react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: @@ -1623,8 +1598,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@date-fns/tz@1.4.1': {} - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1805,8 +1778,6 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@tabby_ai/hijri-converter@1.0.5': {} - '@tailwindcss/node@4.2.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2176,10 +2147,6 @@ snapshots: d3-timer@3.0.1: {} - date-fns-jalali@4.1.0-0: {} - - date-fns@4.1.0: {} - debug@4.4.3: dependencies: ms: 2.1.3 @@ -2592,14 +2559,6 @@ snapshots: punycode@2.3.1: {} - react-day-picker@9.14.0(react@19.2.5): - dependencies: - '@date-fns/tz': 1.4.1 - '@tabby_ai/hijri-converter': 1.0.5 - date-fns: 4.1.0 - date-fns-jalali: 4.1.0-0 - react: 19.2.5 - react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 diff --git a/src/food-market.web/src/components/DateField.tsx b/src/food-market.web/src/components/DateField.tsx deleted file mode 100644 index 26ff2d3..0000000 --- a/src/food-market.web/src/components/DateField.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { useEffect, useLayoutEffect, useRef, useState } from 'react' -import { createPortal } from 'react-dom' -import { Calendar as CalendarIcon } from 'lucide-react' -import { DayPicker } from 'react-day-picker' -import { ru } from 'date-fns/locale' -import 'react-day-picker/dist/style.css' -import { cn } from '@/lib/utils' - -interface Props { - /** Значение в формате ISO `YYYY-MM-DD` (как приходит/уходит к API). Пустая строка = нет даты. */ - value: string - onChange: (iso: string) => void - disabled?: boolean - required?: boolean - className?: string - /** Ширина контейнера. По умолчанию `w-40` (160px) — достаточно для DD.MM.YYYY. */ - widthClass?: string -} - -const inputClass = 'w-full h-10 rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 pr-9 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' - -function isoToDisplay(iso: string): string { - if (!iso) return '' - const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso) - return m ? `${m[3]}.${m[2]}.${m[1]}` : '' -} - -function displayToIso(s: string): string | null { - const m = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(s.trim()) - if (!m) return null - const [_, dd, mm, yyyy] = m - const d = Number(dd), mo = Number(mm), y = Number(yyyy) - if (mo < 1 || mo > 12 || d < 1 || d > 31) return null - const dt = new Date(y, mo - 1, d) - if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null - const iso = `${yyyy}-${mm}-${dd}` - void _ - return iso -} - -function isoToDate(iso: string): Date | undefined { - const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso) - if (!m) return undefined - return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) -} - -function dateToIso(d: Date): string { - const y = d.getFullYear() - const m = String(d.getMonth() + 1).padStart(2, '0') - const dd = String(d.getDate()).padStart(2, '0') - return `${y}-${m}-${dd}` -} - -/** Дата в едином RU-формате (DD.MM.YYYY) с попапом-календарём (локаль ru, capitalized). - * Хранится и отдаётся в ISO `YYYY-MM-DD`. Ширина по умолчанию ~w-40 — нет смысла - * растягивать на всю колонку, в дате 10 символов. */ -export function DateField({ value, onChange, disabled, required, className, widthClass = 'w-40' }: Props) { - const [draft, setDraft] = useState(isoToDisplay(value)) - const [open, setOpen] = useState(false) - const [pos, setPos] = useState<{ top: number; left: number } | null>(null) - const wrapRef = useRef(null) - const inputRef = useRef(null) - const popRef = useRef(null) - - useEffect(() => { setDraft(isoToDisplay(value)) }, [value]) - - useLayoutEffect(() => { - if (!open) return - const recompute = () => { - const el = wrapRef.current - if (!el) return - const r = el.getBoundingClientRect() - setPos({ top: r.bottom + 4, left: r.left }) - } - recompute() - const onScroll = () => recompute() - window.addEventListener('scroll', onScroll, true) - window.addEventListener('resize', onScroll) - return () => { - window.removeEventListener('scroll', onScroll, true) - window.removeEventListener('resize', onScroll) - } - }, [open]) - - useEffect(() => { - if (!open) return - const onDoc = (e: MouseEvent) => { - const t = e.target as Node - if (wrapRef.current?.contains(t)) return - if (popRef.current?.contains(t)) return - setOpen(false) - } - document.addEventListener('mousedown', onDoc) - return () => document.removeEventListener('mousedown', onDoc) - }, [open]) - - const commitDraft = (s: string) => { - if (s.trim() === '') { onChange(''); return } - const iso = displayToIso(s) - if (iso) onChange(iso) - else setDraft(isoToDisplay(value)) - } - - return ( -
- { - // Ограничиваем ввод цифрами и точками; авто-вставляем точки после dd и mm - let v = e.target.value.replace(/[^\d.]/g, '') - // Авто-точки: после 2-й и 5-й цифры - const digits = v.replace(/\./g, '') - if (digits.length >= 5) v = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4, 8)}` - else if (digits.length >= 3) v = `${digits.slice(0, 2)}.${digits.slice(2, 4)}` - else v = digits - setDraft(v) - }} - onBlur={() => commitDraft(draft)} - onFocus={(e) => e.currentTarget.select()} - className={inputClass} - /> - - {open && pos && createPortal( -
), - }} - className="z-[100] w-[340px] rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-md p-3 text-sm" - > - { - if (!d) return - const iso = dateToIso(d) - onChange(iso) - setDraft(isoToDisplay(iso)) - setOpen(false) - }} - footer={ -
- - -
- } - classNames={{ - months: 'flex flex-col', - month: 'relative space-y-2', - month_caption: 'flex items-center pt-0.5 pl-1 h-8 pr-16', - dropdowns: 'flex items-center gap-1.5 text-sm font-semibold', - dropdown_root: 'relative inline-flex items-center', - dropdown: 'appearance-none bg-transparent capitalize cursor-pointer focus:outline-none rounded hover:bg-slate-100 dark:hover:bg-slate-800 px-1.5 py-0.5 pr-5', - caption_label: 'capitalize pointer-events-none absolute opacity-0', - nav: 'absolute right-1 top-1 flex items-center gap-0.5', - button_previous: 'h-7 w-7 inline-flex items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800', - button_next: 'h-7 w-7 inline-flex items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800', - chevron: 'w-4 h-4 fill-current', - weekdays: 'flex', - weekday: 'w-9 text-center text-[0.75rem] font-normal text-slate-500 capitalize', - weeks: 'mt-1', - week: 'flex w-full mt-1', - day: 'w-9 h-9 p-0 text-center align-middle', - day_button: 'w-9 h-9 inline-flex items-center justify-center rounded-md text-sm font-normal hover:bg-slate-100 dark:hover:bg-slate-800 disabled:hover:bg-transparent', - today: '[&_button]:bg-[var(--color-brand)] [&_button]:text-white [&_button]:font-medium [&_button]:hover:bg-[var(--color-brand)]', - selected: '[&_button]:ring-2 [&_button]:ring-[var(--color-brand)] [&_button]:ring-offset-1', - outside: 'text-slate-300 dark:text-slate-600 opacity-60', - disabled: 'text-slate-300 opacity-50', - }} - /> -
, - document.body, - )} -
- ) -} diff --git a/src/food-market.web/src/pages/SupplyEditPage.tsx b/src/food-market.web/src/pages/SupplyEditPage.tsx index ee20c23..e8d6e74 100644 --- a/src/food-market.web/src/pages/SupplyEditPage.tsx +++ b/src/food-market.web/src/pages/SupplyEditPage.tsx @@ -4,8 +4,7 @@ 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, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field' -import { DateField } from '@/components/DateField' +import { Field, TextInput, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field' import { ProductPicker } from '@/components/ProductPicker' import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd' import { useStores, useCurrencies, useSuppliers, usePriceTypes } from '@/lib/useLookups' @@ -300,8 +299,9 @@ export function SupplyEditPage() {
- setForm({ ...form, date: iso })} /> + setForm({ ...form, date: e.target.value })} + className="max-w-[180px]" />