
• Исправлена ошибка React Hooks в EmployeesDashboard - перемещен useMemo на верхний уровень компонента • Устранены ошибки TypeScript в ScheduleRecord интерфейсе • Добавлена типизация GraphQL скаляров и резолверов • Исправлены типы Apollo Client и error handling • Очищены неиспользуемые импорты в компонентах Employee • Переименованы неиспользуемые переменные в warehouse-statistics • Исправлен экспорт RefreshCw иконки 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
681 lines
28 KiB
TypeScript
681 lines
28 KiB
TypeScript
'use client'
|
||
|
||
import {
|
||
Camera,
|
||
User,
|
||
X,
|
||
Save,
|
||
UserPlus,
|
||
AlertCircle,
|
||
RefreshCw,
|
||
FileImage,
|
||
Briefcase,
|
||
Phone,
|
||
Mail,
|
||
Calendar,
|
||
DollarSign,
|
||
MessageCircle,
|
||
} from 'lucide-react'
|
||
import Image from 'next/image'
|
||
import { useState, useRef } from 'react'
|
||
import { toast } from 'sonner'
|
||
|
||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card } from '@/components/ui/card'
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||
import { Input } from '@/components/ui/input'
|
||
import {
|
||
formatPhoneInput,
|
||
formatSalary,
|
||
formatNameInput,
|
||
isValidEmail,
|
||
isValidPhone,
|
||
isValidBirthDate,
|
||
isValidSalary,
|
||
} from '@/lib/input-masks'
|
||
|
||
interface EmployeeInlineFormProps {
|
||
onSave: (employeeData: {
|
||
firstName: string
|
||
lastName: string
|
||
middleName?: string
|
||
birthDate?: string
|
||
phone: string
|
||
email?: string
|
||
position: string
|
||
salary?: number
|
||
avatar?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
passportPhoto?: string
|
||
hireDate: string
|
||
}) => void
|
||
onCancel: () => void
|
||
isLoading?: boolean
|
||
}
|
||
|
||
interface ValidationErrors {
|
||
[key: string]: string
|
||
}
|
||
|
||
export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: EmployeeInlineFormProps) {
|
||
const [formData, setFormData] = useState({
|
||
firstName: '',
|
||
lastName: '',
|
||
middleName: '',
|
||
birthDate: '',
|
||
phone: '',
|
||
telegram: '',
|
||
whatsapp: '',
|
||
email: '',
|
||
position: '',
|
||
salary: 0,
|
||
avatar: '',
|
||
passportPhoto: '',
|
||
})
|
||
|
||
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]: 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 || '',
|
||
}))
|
||
}
|
||
|
||
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()}`)
|
||
endpoint = '/api/upload-avatar'
|
||
} else {
|
||
// Для паспорта используем специальный API для документов сотрудников
|
||
formDataUpload.append('documentType', 'passport')
|
||
endpoint = '/api/upload-employee-document'
|
||
}
|
||
|
||
const response = await fetch(endpoint, {
|
||
method: 'POST',
|
||
body: formDataUpload,
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json()
|
||
throw new Error(errorData.error || `Ошибка загрузки ${type === 'avatar' ? 'аватара' : 'паспорта'}`)
|
||
}
|
||
|
||
const result = await response.json()
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || 'Неизвестная ошибка при загрузке')
|
||
}
|
||
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[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' ? 'фото' : 'паспорта'}`
|
||
toast.error(errorMessage)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
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)
|
||
|
||
// Дебаг: показываем все ошибки в консоли
|
||
if (Object.keys(newErrors).filter((key) => newErrors[key]).length > 0) {
|
||
console.warn('Ошибки валидации:', newErrors)
|
||
}
|
||
|
||
return Object.keys(newErrors).filter((key) => newErrors[key]).length === 0
|
||
}
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
if (!validateForm()) {
|
||
toast.error('Пожалуйста, исправьте ошибки в форме')
|
||
return
|
||
}
|
||
|
||
// Подготавливаем данные для отправки
|
||
const employeeData = {
|
||
firstName: formData.firstName,
|
||
lastName: formData.lastName,
|
||
middleName: formData.middleName || undefined,
|
||
birthDate: formData.birthDate || undefined,
|
||
phone: formData.phone,
|
||
email: formData.email || undefined,
|
||
position: formData.position,
|
||
salary: formData.salary || undefined,
|
||
avatar: formData.avatar || undefined,
|
||
telegram: formData.telegram || undefined,
|
||
whatsapp: formData.whatsapp || undefined,
|
||
passportPhoto: formData.passportPhoto || undefined,
|
||
hireDate: new Date().toISOString().split('T')[0],
|
||
}
|
||
|
||
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()
|
||
return `${first}${last}`
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<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>
|
||
</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>
|
||
</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>
|
||
|
||
<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>
|
||
|
||
{/* Табель работы - точно как в карточке но пустой */}
|
||
<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>
|
||
|
||
{/* Превью паспорта */}
|
||
<Dialog open={showPassportPreview} onOpenChange={setShowPassportPreview}>
|
||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden glass-card">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="flex justify-center">
|
||
{formData.passportPhoto && formData.passportPhoto.trim() !== '' && (
|
||
<Image
|
||
src={formData.passportPhoto}
|
||
alt="Паспорт"
|
||
width={600}
|
||
height={800}
|
||
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
||
/>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
)
|
||
}
|