Добавлены модели и функциональность для управления логистикой, включая создание, обновление и удаление логистических маршрутов через GraphQL. Обновлены компоненты для отображения и управления логистикой, улучшен интерфейс взаимодействия с пользователем. Реализованы новые типы данных и интерфейсы для логистики, а также улучшена обработка ошибок.

This commit is contained in:
Bivekich
2025-07-18 15:40:12 +03:00
parent 7e7e4a9b4a
commit 93bb5827d2
20 changed files with 5015 additions and 667 deletions

View File

@ -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>