diff --git a/src/food-market.public/src/components/PhoneInput.tsx b/src/food-market.public/src/components/PhoneInput.tsx new file mode 100644 index 0000000..2cfd641 --- /dev/null +++ b/src/food-market.public/src/components/PhoneInput.tsx @@ -0,0 +1,72 @@ +import { useMemo, useRef, type InputHTMLAttributes } from 'react' + +function extractDigits(value: string): string { + if (!value) return '' + let d = value.replace(/\D/g, '') + if (d.length === 11 && (d[0] === '7' || d[0] === '8')) d = d.slice(1) + return d.slice(0, 10) +} + +function formatLocal(d: string): string { + if (d.length === 0) return '' + if (d.length <= 3) return d + if (d.length <= 6) return `${d.slice(0, 3)} ${d.slice(3)}` + if (d.length <= 8) return `${d.slice(0, 3)} ${d.slice(3, 6)} ${d.slice(6)}` + return `${d.slice(0, 3)} ${d.slice(3, 6)} ${d.slice(6, 8)} ${d.slice(8, 10)}` +} + +interface PhoneInputProps extends Omit, 'value' | 'onChange' | 'type'> { + value: string + onChange: (value: string) => void +} + +/** Телефон Казахстана с зашитым префиксом "+7 ". См. food-market.web/components/PhoneInput.tsx. */ +export function PhoneInput({ value, onChange, className, disabled, ...rest }: PhoneInputProps) { + const ref = useRef(null) + const digits = useMemo(() => extractDigits(value), [value]) + const display = `+7 ${formatLocal(digits)}`.trimEnd() + + const handleChange = (e: React.ChangeEvent) => { + const d = extractDigits(e.target.value) + onChange(d.length > 0 ? `+7${d}` : '') + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + const el = e.currentTarget + const start = el.selectionStart ?? 0 + const end = el.selectionEnd ?? 0 + if (e.key === 'Backspace' && start <= 3 && end === start) e.preventDefault() + if (e.key === 'Delete' && start < 3) e.preventDefault() + } + + const handleFocus = () => { + if (digits.length === 0) { + requestAnimationFrame(() => { + ref.current?.setSelectionRange(3, 3) + }) + } + } + + const handleClick = (e: React.MouseEvent) => { + const el = e.currentTarget + if ((el.selectionStart ?? 0) < 3) el.setSelectionRange(3, 3) + } + + return ( + + ) +} diff --git a/src/food-market.public/src/components/SignupForm.tsx b/src/food-market.public/src/components/SignupForm.tsx index 610c5e0..a33e333 100644 --- a/src/food-market.public/src/components/SignupForm.tsx +++ b/src/food-market.public/src/components/SignupForm.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { validateEmail, validatePassword, validatePhone } from '@/lib/validation' +import { PhoneInput } from '@/components/PhoneInput' // Админский / API endpoint — переключается через PUBLIC_APP_URL на этапе // билда. Дефолт — admin.food-market.kz (там и API и админ-SPA). @@ -93,9 +94,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { placeholder="Наименование организации" className={inputCls(!!fieldErrors.orgName)} /> - setPhone(e.target.value)} - required autoComplete="tel" inputMode="tel" - placeholder="+7 700 123 45 67" className={inputCls(!!fieldErrors.phone)} /> +
Тариф
diff --git a/src/food-market.web/src/components/PhoneInput.tsx b/src/food-market.web/src/components/PhoneInput.tsx new file mode 100644 index 0000000..4bd8bb0 --- /dev/null +++ b/src/food-market.web/src/components/PhoneInput.tsx @@ -0,0 +1,92 @@ +import { useMemo, useRef, type InputHTMLAttributes } from 'react' +import { cn } from '@/lib/utils' + +const inputClass = 'w-full h-10 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 tabular-nums' + +/** Извлекает 10 цифр KZ-номера (после +7) из любого формата. + * "+7 700 123 45 67" → "7001234567", "8(707)1234567" → "0712345670"… */ +function extractDigits(value: string): string { + if (!value) return '' + let d = value.replace(/\D/g, '') + // 11 цифр и ведущая 7 или 8 — это код страны, отрезаем. + if (d.length === 11 && (d[0] === '7' || d[0] === '8')) d = d.slice(1) + return d.slice(0, 10) +} + +/** Формат "XXX XXX XX XX" по 10 цифрам субскрайбера. */ +function formatLocal(d: string): string { + if (d.length === 0) return '' + if (d.length <= 3) return d + if (d.length <= 6) return `${d.slice(0, 3)} ${d.slice(3)}` + if (d.length <= 8) return `${d.slice(0, 3)} ${d.slice(3, 6)} ${d.slice(6)}` + return `${d.slice(0, 3)} ${d.slice(3, 6)} ${d.slice(6, 8)} ${d.slice(8, 10)}` +} + +interface PhoneInputProps extends Omit, 'value' | 'onChange' | 'type'> { + /** Каноничное значение в формате "+7XXXXXXXXXX" либо пустая строка. */ + value: string + /** Возвращает каноничное "+7XXXXXXXXXX" если введено 10 цифр, иначе пустую строку. */ + onChange: (value: string) => void +} + +/** Поле ввода телефона Казахстана. Префикс "+7 " зашит и не удаляется, + * принимаются только цифры (буквы и спецсимволы блокируются), при вводе + * автоматически форматируется как "+7 7XX XXX XX XX". На onChange наружу + * отдаётся каноничное "+7XXXXXXXXXX" (если все 10 цифр введены) или "". + * Поддерживает paste произвольного формата (включая "8 …" и "+7 …"). */ +export function PhoneInput({ value, onChange, className, disabled, ...rest }: PhoneInputProps) { + const ref = useRef(null) + + const digits = useMemo(() => extractDigits(value), [value]) + const display = `+7 ${formatLocal(digits)}`.trimEnd() + + const handleChange = (e: React.ChangeEvent) => { + const d = extractDigits(e.target.value) + onChange(d.length === 10 ? `+7${d}` : d.length > 0 ? `+7${d}` : '') + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + const el = e.currentTarget + const start = el.selectionStart ?? 0 + const end = el.selectionEnd ?? 0 + // Не позволяем удалить префикс "+7 " (3 символа). Backspace на позиции + // ≤3 без выделения, либо Delete на позиции <3. + if (e.key === 'Backspace' && start <= 3 && end === start) e.preventDefault() + if (e.key === 'Delete' && start < 3) e.preventDefault() + } + + const handleFocus = (e: React.FocusEvent) => { + // При первом фокусе на пустом поле — курсор после "+7 ". + if (digits.length === 0) { + requestAnimationFrame(() => { + const el = ref.current + if (!el) return + el.setSelectionRange(3, 3) + }) + } + } + + const handleClick = (e: React.MouseEvent) => { + const el = e.currentTarget + // Если кликнули внутрь "+7 " префикса — переставляем курсор после него. + if ((el.selectionStart ?? 0) < 3) el.setSelectionRange(3, 3) + } + + return ( + + ) +} diff --git a/src/food-market.web/src/pages/CounterpartiesPage.tsx b/src/food-market.web/src/pages/CounterpartiesPage.tsx index bbe627c..f1cb524 100644 --- a/src/food-market.web/src/pages/CounterpartiesPage.tsx +++ b/src/food-market.web/src/pages/CounterpartiesPage.tsx @@ -9,6 +9,7 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, TextArea, Select } from '@/components/Field' +import { PhoneInput } from '@/components/PhoneInput' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types' @@ -160,7 +161,7 @@ export function CounterpartiesPage() { setForm({ ...form, address: e.target.value })} /> - setForm({ ...form, phone: e.target.value })} /> + setForm({ ...form, phone: v })} /> setForm({ ...form, email: e.target.value })} /> diff --git a/src/food-market.web/src/pages/EmployeesPage.tsx b/src/food-market.web/src/pages/EmployeesPage.tsx index eafba17..65e7e9b 100644 --- a/src/food-market.web/src/pages/EmployeesPage.tsx +++ b/src/food-market.web/src/pages/EmployeesPage.tsx @@ -9,6 +9,7 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, TextArea, Checkbox } from '@/components/Field' +import { PhoneInput } from '@/components/PhoneInput' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' import type { PagedResult, RetailPoint } from '@/lib/types' import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage' @@ -290,7 +291,7 @@ export function EmployeesPage() { setForm({ ...form, email: e.target.value })} /> - setForm({ ...form, phone: e.target.value })} /> + setForm({ ...form, phone: v })} />
diff --git a/src/food-market.web/src/pages/SuperAdminOrgCreatePage.tsx b/src/food-market.web/src/pages/SuperAdminOrgCreatePage.tsx index 8c7df3d..d1d3150 100644 --- a/src/food-market.web/src/pages/SuperAdminOrgCreatePage.tsx +++ b/src/food-market.web/src/pages/SuperAdminOrgCreatePage.tsx @@ -5,6 +5,7 @@ import { api } from '@/lib/api' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, Select } from '@/components/Field' +import { PhoneInput } from '@/components/PhoneInput' import { useCountries, useCurrencies } from '@/lib/useLookups' import { PageHeader } from '@/components/PageHeader' @@ -79,7 +80,7 @@ export function SuperAdminOrgCreatePage() {
setBin(e.target.value)} /> - setPhone(e.target.value)} /> +
setAddress(e.target.value)} /> setEmail(e.target.value)} /> diff --git a/src/food-market.web/src/pages/SuperAdminOrgEmployeesPage.tsx b/src/food-market.web/src/pages/SuperAdminOrgEmployeesPage.tsx index 04925a0..1712379 100644 --- a/src/food-market.web/src/pages/SuperAdminOrgEmployeesPage.tsx +++ b/src/food-market.web/src/pages/SuperAdminOrgEmployeesPage.tsx @@ -10,6 +10,7 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, TextArea, Checkbox } from '@/components/Field' +import { PhoneInput } from '@/components/PhoneInput' import { useCatalogList } from '@/lib/useCatalog' import type { PagedResult } from '@/lib/types' import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage' @@ -283,7 +284,7 @@ export function SuperAdminOrgEmployeesPage() { setForm({ ...form, email: e.target.value })} /> - setForm({ ...form, phone: e.target.value })} /> + setForm({ ...form, phone: v })} />