Добавлены модели и функциональность для управления логистикой, включая создание, обновление и удаление логистических маршрутов через GraphQL. Обновлены компоненты для отображения и управления логистикой, улучшен интерфейс взаимодействия с пользователем. Реализованы новые типы данных и интерфейсы для логистики, а также улучшена обработка ошибок.
This commit is contained in:
@ -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