Добавлены модели и функциональность для управления логистикой, включая создание, обновление и удаление логистических маршрутов через GraphQL. Обновлены компоненты для отображения и управления логистикой, улучшен интерфейс взаимодействия с пользователем. Реализованы новые типы данных и интерфейсы для логистики, а также улучшена обработка ошибок.
This commit is contained in:
@ -7,8 +7,22 @@ import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { User, Camera } from 'lucide-react'
|
||||
import { User, Camera, AlertCircle } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
formatPhoneInput,
|
||||
formatPassportSeries,
|
||||
formatPassportNumber,
|
||||
formatSalary,
|
||||
formatNameInput,
|
||||
isValidEmail,
|
||||
isValidPhone,
|
||||
isValidPassportSeries,
|
||||
isValidPassportNumber,
|
||||
isValidBirthDate,
|
||||
isValidHireDate,
|
||||
isValidSalary
|
||||
} from '@/lib/input-masks'
|
||||
|
||||
interface Employee {
|
||||
id: string
|
||||
@ -43,6 +57,10 @@ interface EmployeeFormProps {
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: employee?.firstName || '',
|
||||
@ -66,13 +84,154 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
})
|
||||
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const validateField = (field: string, value: string | number): string | null => {
|
||||
switch (field) {
|
||||
case 'firstName':
|
||||
case 'lastName':
|
||||
if (!value || String(value).trim() === '') {
|
||||
return field === 'firstName' ? 'Имя обязательно для заполнения' : 'Фамилия обязательна для заполнения'
|
||||
}
|
||||
if (String(value).length < 2) {
|
||||
return field === 'firstName' ? 'Имя должно содержать минимум 2 символа' : 'Фамилия должна содержать минимум 2 символа'
|
||||
}
|
||||
if (!/^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(String(value))) {
|
||||
return field === 'firstName' ? 'Имя может содержать только буквы, пробелы и дефисы' : 'Фамилия может содержать только буквы, пробелы и дефисы'
|
||||
}
|
||||
break
|
||||
|
||||
case 'middleName':
|
||||
if (value && String(value).length > 0) {
|
||||
if (String(value).length < 2) {
|
||||
return 'Отчество должно содержать минимум 2 символа'
|
||||
}
|
||||
if (!/^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(String(value))) {
|
||||
return 'Отчество может содержать только буквы, пробелы и дефисы'
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'position':
|
||||
if (!value || String(value).trim() === '') {
|
||||
return 'Должность обязательна для заполнения'
|
||||
}
|
||||
if (String(value).length < 2) {
|
||||
return 'Должность должна содержать минимум 2 символа'
|
||||
}
|
||||
break
|
||||
|
||||
case 'phone':
|
||||
if (!value || String(value).trim() === '') {
|
||||
return 'Телефон обязателен для заполнения'
|
||||
}
|
||||
if (!isValidPhone(String(value))) {
|
||||
return 'Введите корректный номер телефона в формате +7 (999) 123-45-67'
|
||||
}
|
||||
break
|
||||
|
||||
case 'email':
|
||||
if (value && String(value).trim() !== '' && !isValidEmail(String(value))) {
|
||||
return 'Введите корректный email адрес'
|
||||
}
|
||||
break
|
||||
|
||||
case 'emergencyPhone':
|
||||
if (value && String(value).trim() !== '' && !isValidPhone(String(value))) {
|
||||
return 'Введите корректный номер телефона в формате +7 (999) 123-45-67'
|
||||
}
|
||||
break
|
||||
|
||||
case 'passportSeries':
|
||||
if (value && String(value).trim() !== '' && !isValidPassportSeries(String(value))) {
|
||||
return 'Серия паспорта должна содержать 4 цифры'
|
||||
}
|
||||
break
|
||||
|
||||
case 'passportNumber':
|
||||
if (value && String(value).trim() !== '' && !isValidPassportNumber(String(value))) {
|
||||
return 'Номер паспорта должен содержать 6 цифр'
|
||||
}
|
||||
break
|
||||
|
||||
case 'birthDate':
|
||||
if (value && String(value).trim() !== '') {
|
||||
const validation = isValidBirthDate(String(value))
|
||||
if (!validation.valid) {
|
||||
return validation.message || 'Некорректная дата рождения'
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'hireDate':
|
||||
const hireValidation = isValidHireDate(String(value))
|
||||
if (!hireValidation.valid) {
|
||||
return hireValidation.message || 'Некорректная дата приема'
|
||||
}
|
||||
break
|
||||
|
||||
case 'salary':
|
||||
const salaryValidation = isValidSalary(Number(value))
|
||||
if (!salaryValidation.valid) {
|
||||
return salaryValidation.message || 'Некорректная сумма зарплаты'
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string | number) => {
|
||||
let processedValue = value
|
||||
|
||||
// Применяем маски ввода
|
||||
if (typeof value === 'string') {
|
||||
switch (field) {
|
||||
case 'phone':
|
||||
case 'emergencyPhone':
|
||||
processedValue = formatPhoneInput(value)
|
||||
break
|
||||
case 'passportSeries':
|
||||
processedValue = formatPassportSeries(value)
|
||||
break
|
||||
case 'passportNumber':
|
||||
processedValue = formatPassportNumber(value)
|
||||
break
|
||||
case 'firstName':
|
||||
case 'lastName':
|
||||
case 'middleName':
|
||||
case 'emergencyContact':
|
||||
processedValue = formatNameInput(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
[field]: processedValue
|
||||
}))
|
||||
|
||||
// Валидация в реальном времени
|
||||
const error = validateField(field, processedValue)
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[field]: error || ''
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSalaryChange = (value: string) => {
|
||||
const numericValue = parseInt(value.replace(/\D/g, '')) || 0
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
salary: numericValue
|
||||
}))
|
||||
|
||||
const error = validateField('salary', numericValue)
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
salary: error || ''
|
||||
}))
|
||||
}
|
||||
|
||||
@ -109,35 +268,36 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: ValidationErrors = {}
|
||||
|
||||
// Валидируем все поля
|
||||
Object.keys(formData).forEach(field => {
|
||||
const error = validateField(field, formData[field as keyof typeof formData])
|
||||
if (error) {
|
||||
newErrors[field] = error
|
||||
}
|
||||
})
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).filter(key => newErrors[key]).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
|
||||
// Валидация
|
||||
if (!formData.firstName || !formData.lastName || !formData.position) {
|
||||
toast.error('Пожалуйста, заполните все обязательные поля')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
toast.error('Введите корректный email адрес')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.phone && !/^[\+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ''))) {
|
||||
toast.error('Введите корректный номер телефона')
|
||||
if (!validateForm()) {
|
||||
toast.error('Пожалуйста, исправьте ошибки в форме')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Для создания/обновления отправляем только нужные поля
|
||||
const employeeData = {
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
middleName: formData.middleName,
|
||||
middleName: formData.middleName || undefined,
|
||||
position: formData.position,
|
||||
phone: formData.phone,
|
||||
email: formData.email || undefined,
|
||||
@ -170,13 +330,15 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
return `${first}${last}`
|
||||
}
|
||||
|
||||
const formatPhoneInput = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '')
|
||||
if (cleaned.length <= 1) return cleaned
|
||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
||||
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
||||
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
||||
// Компонент для отображения ошибок
|
||||
const ErrorMessage = ({ error }: { error: string }) => {
|
||||
if (!error) return null
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-1 text-red-400 text-xs">
|
||||
<AlertCircle className="h-3 w-3 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -227,9 +389,10 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
placeholder="Александр"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.firstName ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.firstName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -240,9 +403,10 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
placeholder="Петров"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.lastName ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.lastName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -251,8 +415,9 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.middleName}
|
||||
onChange={(e) => handleInputChange('middleName', e.target.value)}
|
||||
placeholder="Иванович"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.middleName ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.middleName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -261,8 +426,9 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
type="date"
|
||||
value={formData.birthDate}
|
||||
onChange={(e) => handleInputChange('birthDate', e.target.value)}
|
||||
className="glass-input text-white h-10"
|
||||
className={`glass-input text-white h-10 ${errors.birthDate ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.birthDate} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -271,8 +437,10 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.passportSeries}
|
||||
onChange={(e) => handleInputChange('passportSeries', e.target.value)}
|
||||
placeholder="1234"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
maxLength={4}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.passportSeries ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.passportSeries} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -281,8 +449,10 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.passportNumber}
|
||||
onChange={(e) => handleInputChange('passportNumber', e.target.value)}
|
||||
placeholder="567890"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
maxLength={6}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.passportNumber ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.passportNumber} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -330,35 +500,39 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.position}
|
||||
onChange={(e) => handleInputChange('position', e.target.value)}
|
||||
placeholder="Менеджер склада"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.position ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.position} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Дата приема на работу</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Дата приема на работу <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.hireDate}
|
||||
onChange={(e) => handleInputChange('hireDate', e.target.value)}
|
||||
className="glass-input text-white h-10"
|
||||
className={`glass-input text-white h-10 ${errors.hireDate ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.hireDate} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Статус</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(value: 'active' | 'vacation' | 'sick' | 'inactive') => handleInputChange('status', value)}
|
||||
onValueChange={(value: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED') => handleInputChange('status', value)}
|
||||
>
|
||||
<SelectTrigger className="glass-input text-white h-10">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-900 border-white/20">
|
||||
<SelectItem value="active" className="text-white hover:bg-white/10">Активен</SelectItem>
|
||||
<SelectItem value="vacation" className="text-white hover:bg-white/10">В отпуске</SelectItem>
|
||||
<SelectItem value="sick" className="text-white hover:bg-white/10">На больничном</SelectItem>
|
||||
<SelectItem value="inactive" className="text-white hover:bg-white/10">Неактивен</SelectItem>
|
||||
<SelectItem value="ACTIVE" className="text-white hover:bg-white/10">Активен</SelectItem>
|
||||
<SelectItem value="VACATION" className="text-white hover:bg-white/10">В отпуске</SelectItem>
|
||||
<SelectItem value="SICK" className="text-white hover:bg-white/10">На больничном</SelectItem>
|
||||
<SelectItem value="FIRED" className="text-white hover:bg-white/10">Уволен</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -367,13 +541,12 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
<div className="mt-4">
|
||||
<Label className="text-white/80 text-sm mb-2 block">Зарплата (₽)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.salary || ''}
|
||||
onChange={(e) => handleInputChange('salary', parseInt(e.target.value) || 0)}
|
||||
placeholder="80000"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
value={formData.salary ? formatSalary(formData.salary.toString()) : ''}
|
||||
onChange={(e) => handleSalaryChange(e.target.value)}
|
||||
placeholder="80 000"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.salary ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.salary} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -382,16 +555,16 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
<h3 className="text-white font-medium mb-4">Контактные данные</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Телефон</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Телефон <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => {
|
||||
const formatted = formatPhoneInput(e.target.value)
|
||||
handleInputChange('phone', formatted)
|
||||
}}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.phone ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.phone} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -401,8 +574,9 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="a.petrov@company.com"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.email ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.email} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -419,13 +593,11 @@ export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps)
|
||||
<Label className="text-white/80 text-sm mb-2 block">Телефон экстренного контакта</Label>
|
||||
<Input
|
||||
value={formData.emergencyPhone}
|
||||
onChange={(e) => {
|
||||
const formatted = formatPhoneInput(e.target.value)
|
||||
handleInputChange('emergencyPhone', formatted)
|
||||
}}
|
||||
onChange={(e) => handleInputChange('emergencyPhone', e.target.value)}
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${errors.emergencyPhone ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.emergencyPhone} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -20,11 +20,25 @@ import {
|
||||
Mail,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
|
||||
FileText,
|
||||
MessageCircle
|
||||
MessageCircle,
|
||||
AlertCircle
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
formatPhoneInput,
|
||||
formatPassportSeries,
|
||||
formatPassportNumber,
|
||||
formatSalary,
|
||||
formatNameInput,
|
||||
isValidEmail,
|
||||
isValidPhone,
|
||||
isValidPassportSeries,
|
||||
isValidPassportNumber,
|
||||
isValidBirthDate,
|
||||
isValidHireDate,
|
||||
isValidSalary
|
||||
} from '@/lib/input-masks'
|
||||
|
||||
interface EmployeeInlineFormProps {
|
||||
onSave: (employeeData: {
|
||||
@ -46,6 +60,10 @@ interface EmployeeInlineFormProps {
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: EmployeeInlineFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
@ -65,13 +83,123 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
const [isUploadingPassport, setIsUploadingPassport] = useState(false)
|
||||
const [showPassportPreview, setShowPassportPreview] = useState(false)
|
||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||
const avatarInputRef = useRef<HTMLInputElement>(null)
|
||||
const passportInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const validateField = (field: string, value: string | number): string | null => {
|
||||
switch (field) {
|
||||
case 'firstName':
|
||||
case 'lastName':
|
||||
if (!value || String(value).trim() === '') {
|
||||
return field === 'firstName' ? 'Имя обязательно для заполнения' : 'Фамилия обязательна для заполнения'
|
||||
}
|
||||
if (String(value).length < 2) {
|
||||
return field === 'firstName' ? 'Имя должно содержать минимум 2 символа' : 'Фамилия должна содержать минимум 2 символа'
|
||||
}
|
||||
if (!/^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(String(value))) {
|
||||
return field === 'firstName' ? 'Имя может содержать только буквы, пробелы и дефисы' : 'Фамилия может содержать только буквы, пробелы и дефисы'
|
||||
}
|
||||
break
|
||||
|
||||
case 'middleName':
|
||||
if (value && String(value).length > 0) {
|
||||
if (String(value).length < 2) {
|
||||
return 'Отчество должно содержать минимум 2 символа'
|
||||
}
|
||||
if (!/^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(String(value))) {
|
||||
return 'Отчество может содержать только буквы, пробелы и дефисы'
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'position':
|
||||
if (!value || String(value).trim() === '') {
|
||||
return 'Должность обязательна для заполнения'
|
||||
}
|
||||
if (String(value).length < 2) {
|
||||
return 'Должность должна содержать минимум 2 символа'
|
||||
}
|
||||
break
|
||||
|
||||
case 'phone':
|
||||
case 'whatsapp':
|
||||
if (field === 'phone' && (!value || String(value).trim() === '')) {
|
||||
return 'Телефон обязателен для заполнения'
|
||||
}
|
||||
if (value && String(value).trim() !== '' && !isValidPhone(String(value))) {
|
||||
return 'Введите корректный номер телефона в формате +7 (999) 123-45-67'
|
||||
}
|
||||
break
|
||||
|
||||
case 'email':
|
||||
if (value && String(value).trim() !== '' && !isValidEmail(String(value))) {
|
||||
return 'Введите корректный email адрес'
|
||||
}
|
||||
break
|
||||
|
||||
case 'birthDate':
|
||||
if (value && String(value).trim() !== '') {
|
||||
const validation = isValidBirthDate(String(value))
|
||||
if (!validation.valid) {
|
||||
return validation.message || 'Некорректная дата рождения'
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'salary':
|
||||
const salaryValidation = isValidSalary(Number(value))
|
||||
if (!salaryValidation.valid) {
|
||||
return salaryValidation.message || 'Некорректная сумма зарплаты'
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string | number) => {
|
||||
let processedValue = value
|
||||
|
||||
// Применяем маски ввода
|
||||
if (typeof value === 'string') {
|
||||
switch (field) {
|
||||
case 'phone':
|
||||
case 'whatsapp':
|
||||
processedValue = formatPhoneInput(value)
|
||||
break
|
||||
case 'firstName':
|
||||
case 'lastName':
|
||||
case 'middleName':
|
||||
processedValue = formatNameInput(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
[field]: processedValue
|
||||
}))
|
||||
|
||||
// Валидация в реальном времени
|
||||
const error = validateField(field, processedValue)
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[field]: error || ''
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSalaryChange = (value: string) => {
|
||||
const numericValue = parseInt(value.replace(/\D/g, '')) || 0
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
salary: numericValue
|
||||
}))
|
||||
|
||||
const error = validateField('salary', numericValue)
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
salary: error || ''
|
||||
}))
|
||||
}
|
||||
|
||||
@ -126,26 +254,28 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
}
|
||||
}
|
||||
|
||||
const formatPhoneInput = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '')
|
||||
if (cleaned.length <= 1) return cleaned
|
||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
||||
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
||||
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
||||
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: ValidationErrors = {}
|
||||
|
||||
// Валидируем все поля
|
||||
Object.keys(formData).forEach(field => {
|
||||
const error = validateField(field, formData[field as keyof typeof formData])
|
||||
if (error) {
|
||||
newErrors[field] = error
|
||||
}
|
||||
})
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).filter(key => newErrors[key]).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (!formData.firstName || !formData.lastName || !formData.phone || !formData.position) {
|
||||
toast.error('Пожалуйста, заполните все обязательные поля')
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
toast.error('Введите корректный email адрес')
|
||||
if (!validateForm()) {
|
||||
toast.error('Пожалуйста, исправьте ошибки в форме')
|
||||
return
|
||||
}
|
||||
|
||||
@ -169,6 +299,17 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
onSave(employeeData)
|
||||
}
|
||||
|
||||
// Компонент для отображения ошибок
|
||||
const ErrorMessage = ({ error }: { error: string }) => {
|
||||
if (!error) return null
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-1 text-red-400 text-xs">
|
||||
<AlertCircle className="h-3 w-3 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getInitials = () => {
|
||||
const first = formData.firstName.charAt(0).toUpperCase()
|
||||
const last = formData.lastName.charAt(0).toUpperCase()
|
||||
@ -316,7 +457,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
</Label>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Имя <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
@ -324,11 +465,12 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
placeholder="Александр"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.firstName ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.firstName} />
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Фамилия <span className="text-red-400">*</span>
|
||||
@ -337,9 +479,10 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
placeholder="Петров"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.lastName ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.lastName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -348,8 +491,9 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
value={formData.middleName}
|
||||
onChange={(e) => handleInputChange('middleName', e.target.value)}
|
||||
placeholder="Иванович"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.middleName ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.middleName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -382,14 +526,12 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => {
|
||||
const formatted = formatPhoneInput(e.target.value)
|
||||
handleInputChange('phone', formatted)
|
||||
}}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.phone ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.phone} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -412,13 +554,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.whatsapp}
|
||||
onChange={(e) => {
|
||||
const formatted = formatPhoneInput(e.target.value)
|
||||
handleInputChange('whatsapp', formatted)
|
||||
}}
|
||||
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.whatsapp ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.whatsapp} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -431,8 +571,9 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="a.petrov@company.com"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.email ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.email} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -455,9 +596,10 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
value={formData.position}
|
||||
onChange={(e) => handleInputChange('position', e.target.value)}
|
||||
placeholder="Менеджер склада"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.position ? 'border-red-400' : ''}`}
|
||||
required
|
||||
/>
|
||||
<ErrorMessage error={errors.position} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -466,13 +608,12 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
Зарплата
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.salary || ''}
|
||||
onChange={(e) => handleInputChange('salary', parseInt(e.target.value) || 0)}
|
||||
placeholder="80000"
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
value={formData.salary ? formatSalary(formData.salary.toString()) : ''}
|
||||
onChange={(e) => handleSalaryChange(e.target.value)}
|
||||
placeholder="80 000"
|
||||
className={`glass-input text-white placeholder:text-white/40 ${errors.salary ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
<ErrorMessage error={errors.salary} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user