Оптимизирована производительность React компонентов с помощью мемоизации

КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ:
• AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo
• SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций
• CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики
• EmployeesDashboard (268 kB) - мемоизация списков и функций
• SalesTab + AdvertisingTab - React.memo обертка

ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ:
 React.memo() для предотвращения лишних рендеров
 useMemo() для тяжелых вычислений
 useCallback() для стабильных ссылок на функции
 Мемоизация фильтрации и сортировки списков
 Оптимизация пропсов в компонентах-контейнерах

РЕЗУЛЬТАТЫ:
• Все компоненты успешно компилируются
• Линтер проходит без критических ошибок
• Сохранена вся функциональность
• Улучшена производительность рендеринга
• Снижена нагрузка на React дерево

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-06 13:18:45 +03:00
parent ef5de31ce7
commit bf27f3ba29
317 changed files with 26722 additions and 38332 deletions

View File

@ -1,20 +1,10 @@
"use client"
'use client'
import { useState, useRef } from 'react'
import Image from 'next/image'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Separator } from '@/components/ui/separator'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
Camera,
User,
X,
Save,
import {
Camera,
User,
X,
Save,
UserPlus,
Phone,
Mail,
@ -25,13 +15,23 @@ import {
AlertCircle,
Calendar,
RefreshCw,
FileImage
FileImage,
} from 'lucide-react'
import Image from 'next/image'
import { useState, useRef } from 'react'
import { toast } from 'sonner'
import {
formatPhoneInput,
formatPassportSeries,
formatPassportNumber,
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import {
formatPhoneInput,
formatPassportSeries,
formatPassportNumber,
formatSalary,
formatNameInput,
isValidEmail,
@ -40,7 +40,7 @@ import {
isValidPassportNumber,
isValidBirthDate,
isValidHireDate,
isValidSalary
isValidSalary,
} from '@/lib/input-masks'
interface EmployeeInlineFormProps {
@ -80,7 +80,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
position: '',
salary: 0,
avatar: '',
passportPhoto: ''
passportPhoto: '',
})
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
@ -98,13 +98,17 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
return field === 'firstName' ? 'Имя обязательно для заполнения' : 'Фамилия обязательна для заполнения'
}
if (String(value).length < 2) {
return field === 'firstName' ? 'Имя должно содержать минимум 2 символа' : 'Фамилия должна содержать минимум 2 символа'
return field === 'firstName'
? 'Имя должно содержать минимум 2 символа'
: 'Фамилия должна содержать минимум 2 символа'
}
if (!/^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(String(value))) {
return field === 'firstName' ? 'Имя может содержать только буквы, пробелы и дефисы' : 'Фамилия может содержать только буквы, пробелы и дефисы'
return field === 'firstName'
? 'Имя может содержать только буквы, пробелы и дефисы'
: 'Фамилия может содержать только буквы, пробелы и дефисы'
}
break
case 'middleName':
if (value && String(value).length > 0) {
if (String(value).length < 2) {
@ -115,7 +119,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
}
}
break
case 'position':
if (!value || String(value).trim() === '') {
return 'Должность обязательна для заполнения'
@ -124,7 +128,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
return 'Должность должна содержать минимум 2 символа'
}
break
case 'phone':
case 'whatsapp':
if (field === 'phone' && (!value || String(value).trim() === '')) {
@ -134,13 +138,13 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
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))
@ -149,7 +153,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
}
}
break
case 'salary':
const salaryValidation = isValidSalary(Number(value))
if (!salaryValidation.valid) {
@ -157,7 +161,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
}
break
}
return null
}
@ -179,43 +183,43 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
}
}
setFormData(prev => ({
setFormData((prev) => ({
...prev,
[field]: processedValue
[field]: processedValue,
}))
// Валидация в реальном времени
const error = validateField(field, processedValue)
setErrors(prev => ({
setErrors((prev) => ({
...prev,
[field]: error || ''
[field]: error || '',
}))
}
const handleSalaryChange = (value: string) => {
const numericValue = parseInt(value.replace(/\D/g, '')) || 0
setFormData(prev => ({
setFormData((prev) => ({
...prev,
salary: numericValue
salary: numericValue,
}))
const error = validateField('salary', numericValue)
setErrors(prev => ({
setErrors((prev) => ({
...prev,
salary: error || ''
salary: error || '',
}))
}
const handleFileUpload = async (file: File, type: 'avatar' | 'passport') => {
const setLoading = type === 'avatar' ? setIsUploadingAvatar : setIsUploadingPassport
setLoading(true)
try {
const formDataUpload = new FormData()
formDataUpload.append('file', file)
let endpoint: string
if (type === 'avatar') {
// Для аватара используем upload-avatar API и добавляем временный userId
formDataUpload.append('userId', `temp_${Date.now()}`)
@ -228,7 +232,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
const response = await fetch(endpoint, {
method: 'POST',
body: formDataUpload
body: formDataUpload,
})
if (!response.ok) {
@ -237,33 +241,32 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error || 'Неизвестная ошибка при загрузке')
}
setFormData(prev => ({
setFormData((prev) => ({
...prev,
[type === 'avatar' ? 'avatar' : 'passportPhoto']: result.url
[type === 'avatar' ? 'avatar' : 'passportPhoto']: result.url,
}))
toast.success(`${type === 'avatar' ? 'Фото' : 'Паспорт'} успешно загружен`)
} catch (error) {
console.error(`Error uploading ${type}:`, error)
const errorMessage = error instanceof Error ? error.message : `Ошибка при загрузке ${type === 'avatar' ? 'фото' : 'паспорта'}`
const errorMessage =
error instanceof Error ? error.message : `Ошибка при загрузке ${type === 'avatar' ? 'фото' : 'паспорта'}`
toast.error(errorMessage)
} finally {
setLoading(false)
}
}
const validateForm = (): boolean => {
const newErrors: ValidationErrors = {}
// Валидируем все поля
Object.keys(formData).forEach(field => {
Object.keys(formData).forEach((field) => {
const error = validateField(field, formData[field as keyof typeof formData])
if (error) {
newErrors[field] = error
@ -271,13 +274,13 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
})
setErrors(newErrors)
// Дебаг: показываем все ошибки в консоли
if (Object.keys(newErrors).filter(key => newErrors[key]).length > 0) {
console.log('Ошибки валидации:', newErrors)
if (Object.keys(newErrors).filter((key) => newErrors[key]).length > 0) {
console.warn('Ошибки валидации:', newErrors)
}
return Object.keys(newErrors).filter(key => newErrors[key]).length === 0
return Object.keys(newErrors).filter((key) => newErrors[key]).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
@ -302,7 +305,7 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
telegram: formData.telegram || undefined,
whatsapp: formData.whatsapp || undefined,
passportPhoto: formData.passportPhoto || undefined,
hireDate: new Date().toISOString().split('T')[0]
hireDate: new Date().toISOString().split('T')[0],
}
onSave(employeeData)
@ -329,337 +332,335 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
<>
<Card className="glass-card p-6 mb-6">
<form onSubmit={handleSubmit}>
<div className="flex flex-col lg:flex-row gap-6">
{/* Информация о сотруднике - точно как в карточке */}
<div className="lg:w-80 flex-shrink-0">
<div className="flex items-start space-x-4 mb-4">
{/* Блок с аватаром и фото паспорта вертикально */}
<div className="flex flex-col items-center gap-4">
{/* Аватар с иконкой камеры */}
<div className="flex flex-col items-center gap-2">
<div className="relative">
<Avatar className="h-16 w-16 ring-2 ring-white/20">
{formData.avatar && formData.avatar.trim() !== '' ? (
<AvatarImage src={formData.avatar} alt="Фото сотрудника" />
) : null}
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white font-semibold text-lg">
{getInitials() || <User className="h-8 w-8" />}
</AvatarFallback>
</Avatar>
<div className="absolute -bottom-1 -right-1">
<label htmlFor="avatar-upload-inline" className="cursor-pointer">
<div className="w-5 h-5 bg-purple-600 rounded-full flex items-center justify-center hover:bg-purple-700 transition-colors">
{isUploadingAvatar ? (
<RefreshCw className="h-2.5 w-2.5 text-white animate-spin" />
) : (
<Camera className="h-2.5 w-2.5 text-white" />
)}
</div>
</label>
<div className="flex flex-col lg:flex-row gap-6">
{/* Информация о сотруднике - точно как в карточке */}
<div className="lg:w-80 flex-shrink-0">
<div className="flex items-start space-x-4 mb-4">
{/* Блок с аватаром и фото паспорта вертикально */}
<div className="flex flex-col items-center gap-4">
{/* Аватар с иконкой камеры */}
<div className="flex flex-col items-center gap-2">
<div className="relative">
<Avatar className="h-16 w-16 ring-2 ring-white/20">
{formData.avatar && formData.avatar.trim() !== '' ? (
<AvatarImage src={formData.avatar} alt="Фото сотрудника" />
) : null}
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white font-semibold text-lg">
{getInitials() || <User className="h-8 w-8" />}
</AvatarFallback>
</Avatar>
<div className="absolute -bottom-1 -right-1">
<label htmlFor="avatar-upload-inline" className="cursor-pointer">
<div className="w-5 h-5 bg-purple-600 rounded-full flex items-center justify-center hover:bg-purple-700 transition-colors">
{isUploadingAvatar ? (
<RefreshCw className="h-2.5 w-2.5 text-white animate-spin" />
) : (
<Camera className="h-2.5 w-2.5 text-white" />
)}
</div>
</label>
</div>
</div>
<span className="text-white/60 text-xs text-center">Аватар</span>
</div>
{/* Фото паспорта */}
<div className="flex flex-col items-center gap-2">
<div className="relative">
<div className="w-16 h-16 rounded-lg ring-2 ring-white/20 bg-white/5 flex items-center justify-center overflow-hidden">
{formData.passportPhoto && formData.passportPhoto.trim() !== '' ? (
<img
src={formData.passportPhoto}
alt="Фото паспорта"
className="w-full h-full object-cover cursor-pointer"
onClick={() => setShowPassportPreview(true)}
/>
) : (
<FileImage className="h-6 w-6 text-white/40" />
)}
</div>
<div className="absolute -bottom-1 -right-1">
<label htmlFor="passport-upload-inline" className="cursor-pointer">
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center hover:bg-blue-700 transition-colors">
{isUploadingPassport ? (
<RefreshCw className="h-2.5 w-2.5 text-white animate-spin" />
) : (
<Camera className="h-2.5 w-2.5 text-white" />
)}
</div>
</label>
</div>
</div>
<span className="text-white/60 text-xs text-center">Паспорт</span>
</div>
<span className="text-white/60 text-xs text-center">Аватар</span>
</div>
{/* Фото паспорта */}
<div className="flex flex-col items-center gap-2">
<div className="relative">
<div className="w-16 h-16 rounded-lg ring-2 ring-white/20 bg-white/5 flex items-center justify-center overflow-hidden">
{formData.passportPhoto && formData.passportPhoto.trim() !== '' ? (
<img
src={formData.passportPhoto}
alt="Фото паспорта"
className="w-full h-full object-cover cursor-pointer"
onClick={() => setShowPassportPreview(true)}
/>
) : (
<FileImage className="h-6 w-6 text-white/40" />
)}
</div>
<div className="absolute -bottom-1 -right-1">
<label htmlFor="passport-upload-inline" className="cursor-pointer">
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center hover:bg-blue-700 transition-colors">
{isUploadingPassport ? (
<RefreshCw className="h-2.5 w-2.5 text-white animate-spin" />
) : (
<Camera className="h-2.5 w-2.5 text-white" />
)}
</div>
</label>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<h3 className="text-white font-semibold text-lg">
<UserPlus className="h-5 w-5 text-purple-400 inline mr-2" />
Новый сотрудник
</h3>
<div className="flex gap-1">
<Button
type="button"
size="sm"
variant="ghost"
onClick={onCancel}
className="text-red-400/60 hover:text-red-300 hover:bg-red-500/10 h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<span className="text-white/60 text-xs text-center">Паспорт</span>
<div className="mb-4">
<div className="space-y-3">
{/* Имя */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Имя *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.firstName ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.firstName} />
</div>
</div>
{/* Фамилия */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Фамилия *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.lastName ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.lastName} />
</div>
</div>
{/* Отчество */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.middleName}
onChange={(e) => handleInputChange('middleName', e.target.value)}
placeholder="Отчество"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.middleName ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.middleName} />
</div>
</div>
{/* Должность */}
<div className="flex items-center text-white/70">
<Briefcase className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.position}
onChange={(e) => handleInputChange('position', e.target.value)}
placeholder="Должность *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.position ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.position} />
</div>
</div>
{/* Телефон */}
<div className="flex items-center text-white/70">
<Phone className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="Телефон *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.phone ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.phone} />
</div>
</div>
{/* Email */}
<div className="flex items-center text-white/70">
<Mail className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Email"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.email ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.email} />
</div>
</div>
{/* Дата рождения */}
<div className="flex items-center text-white/70">
<Calendar className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
placeholder="Дата рождения"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.birthDate ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.birthDate} />
</div>
</div>
{/* Зарплата */}
<div className="flex items-center text-white/70">
<DollarSign className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.salary ? formatSalary(formData.salary.toString()) : ''}
onChange={(e) => handleSalaryChange(e.target.value)}
placeholder="Зарплата"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.salary ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.salary} />
</div>
</div>
{/* Telegram */}
<div className="flex items-center text-white/70">
<MessageCircle className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.telegram}
onChange={(e) => handleInputChange('telegram', e.target.value)}
placeholder="@telegram"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.telegram ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.telegram} />
</div>
</div>
{/* WhatsApp */}
<div className="flex items-center text-white/70">
<Phone className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.whatsapp}
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
placeholder="WhatsApp"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.whatsapp ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.whatsapp} />
</div>
</div>
</div>
</div>
<div className="space-y-2 text-sm">
{/* Скрытые input элементы для загрузки файлов */}
<input
id="avatar-upload-inline"
ref={avatarInputRef}
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0], 'avatar')}
className="hidden"
disabled={isUploadingAvatar}
/>
<input
id="passport-upload-inline"
ref={passportInputRef}
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0], 'passport')}
className="hidden"
disabled={isUploadingPassport}
/>
</div>
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<h3 className="text-white font-semibold text-lg">
<UserPlus className="h-5 w-5 text-purple-400 inline mr-2" />
Новый сотрудник
</h3>
<div className="flex gap-1">
<Button
type="button"
size="sm"
variant="ghost"
onClick={onCancel}
className="text-red-400/60 hover:text-red-300 hover:bg-red-500/10 h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Табель работы - точно как в карточке но пустой */}
<div className="flex-1 space-y-4">
<h4 className="text-white/80 font-medium mb-3 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Табель работы (будет доступен после создания)
</h4>
{/* Пустая сетка календаря */}
<div className="grid grid-cols-7 gap-2 opacity-50">
{/* Заголовки дней недели */}
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map((day) => (
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
{day}
</div>
))}
{/* Пустые дни месяца */}
{Array.from({ length: 35 }, (_, i) => {
const day = i + 1
if (day > 31) return <div key={i} className="p-2"></div>
return (
<div key={i} className="relative p-2 min-h-[60px] border rounded-lg bg-white/5 border-white/10">
<div className="flex flex-col items-center justify-center h-full">
<span className="font-semibold text-sm text-white/40">{day <= 31 ? day : ''}</span>
</div>
</div>
)
})}
</div>
{/* Статистика - пустая */}
<div className="grid grid-cols-4 gap-3 mt-4 opacity-50">
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Рабочих дней</p>
</div>
<div className="mb-4">
<div className="space-y-3">
{/* Имя */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Имя *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.firstName ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.firstName} />
</div>
</div>
{/* Фамилия */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Фамилия *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.lastName ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.lastName} />
</div>
</div>
{/* Отчество */}
<div className="flex items-center text-white/70">
<User className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.middleName}
onChange={(e) => handleInputChange('middleName', e.target.value)}
placeholder="Отчество"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.middleName ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.middleName} />
</div>
</div>
{/* Должность */}
<div className="flex items-center text-white/70">
<Briefcase className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.position}
onChange={(e) => handleInputChange('position', e.target.value)}
placeholder="Должность *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.position ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.position} />
</div>
</div>
{/* Телефон */}
<div className="flex items-center text-white/70">
<Phone className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="Телефон *"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.phone ? 'border-red-400' : ''}`}
required
/>
<ErrorMessage error={errors.phone} />
</div>
</div>
{/* Email */}
<div className="flex items-center text-white/70">
<Mail className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Email"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.email ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.email} />
</div>
</div>
{/* Дата рождения */}
<div className="flex items-center text-white/70">
<Calendar className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
placeholder="Дата рождения"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.birthDate ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.birthDate} />
</div>
</div>
{/* Зарплата */}
<div className="flex items-center text-white/70">
<DollarSign className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.salary ? formatSalary(formData.salary.toString()) : ''}
onChange={(e) => handleSalaryChange(e.target.value)}
placeholder="Зарплата"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.salary ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.salary} />
</div>
</div>
{/* Telegram */}
<div className="flex items-center text-white/70">
<MessageCircle className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.telegram}
onChange={(e) => handleInputChange('telegram', e.target.value)}
placeholder="@telegram"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.telegram ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.telegram} />
</div>
</div>
{/* WhatsApp */}
<div className="flex items-center text-white/70">
<Phone className="h-4 w-4 mr-3 flex-shrink-0" />
<div className="flex-1">
<Input
value={formData.whatsapp}
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
placeholder="WhatsApp"
className={`glass-input text-white placeholder:text-white/40 h-9 ${errors.whatsapp ? 'border-red-400' : ''}`}
/>
<ErrorMessage error={errors.whatsapp} />
</div>
</div>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Отпуск</p>
</div>
<div className="space-y-2 text-sm">
{/* Скрытые input элементы для загрузки файлов */}
<input
id="avatar-upload-inline"
ref={avatarInputRef}
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0], 'avatar')}
className="hidden"
disabled={isUploadingAvatar}
/>
<input
id="passport-upload-inline"
ref={passportInputRef}
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0], 'passport')}
className="hidden"
disabled={isUploadingPassport}
/>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Больничный</p>
</div>
<div className="text-center p-3 bg-white/5 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0ч</p>
<p className="text-white/40 text-xs">Всего часов</p>
</div>
</div>
{/* Кнопка сохранения */}
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading || isUploadingAvatar || isUploadingPassport}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
size="lg"
>
{isLoading ? (
'Создание сотрудника...'
) : (
<>
<Save className="h-4 w-4 mr-2" />
Создать сотрудника
</>
)}
</Button>
</div>
</div>
</div>
{/* Табель работы - точно как в карточке но пустой */}
<div className="flex-1 space-y-4">
<h4 className="text-white/80 font-medium mb-3 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Табель работы (будет доступен после создания)
</h4>
{/* Пустая сетка календаря */}
<div className="grid grid-cols-7 gap-2 opacity-50">
{/* Заголовки дней недели */}
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
{day}
</div>
))}
{/* Пустые дни месяца */}
{Array.from({ length: 35 }, (_, i) => {
const day = i + 1
if (day > 31) return <div key={i} className="p-2"></div>
return (
<div
key={i}
className="relative p-2 min-h-[60px] border rounded-lg bg-white/5 border-white/10"
>
<div className="flex flex-col items-center justify-center h-full">
<span className="font-semibold text-sm text-white/40">{day <= 31 ? day : ''}</span>
</div>
</div>
)
})}
</div>
{/* Статистика - пустая */}
<div className="grid grid-cols-4 gap-3 mt-4 opacity-50">
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Рабочих дней</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Отпуск</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0</p>
<p className="text-white/40 text-xs">Больничный</p>
</div>
<div className="text-center p-3 bg-white/5 rounded-lg">
<p className="text-white/40 font-semibold text-lg">0ч</p>
<p className="text-white/40 text-xs">Всего часов</p>
</div>
</div>
{/* Кнопка сохранения */}
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading || isUploadingAvatar || isUploadingPassport}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
size="lg"
>
{isLoading ? 'Создание сотрудника...' : (
<>
<Save className="h-4 w-4 mr-2" />
Создать сотрудника
</>
)}
</Button>
</div>
</div>
</div>
</form>
</Card>
@ -671,11 +672,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
</DialogHeader>
<div className="flex justify-center">
{formData.passportPhoto && formData.passportPhoto.trim() !== '' && (
<Image
<Image
src={formData.passportPhoto}
alt="Паспорт"
width={600}
height={800}
height={800}
className="max-w-full max-h-[70vh] object-contain rounded-lg"
/>
)}
@ -684,4 +685,4 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
</Dialog>
</>
)
}
}