Files
sfera-new/src/components/employees-v2/forms/EmployeeForm.tsx
Veronika Smirnova 962b2deb58 feat: завершить модуляризацию Employee системы V1→V2 с исправлением критических ошибок
Модуляризация Employee системы:
- src/components/employees-v2/ - полная модульная V2 архитектура (hooks, blocks, forms)
- src/app/employees/page.tsx - обновлена главная страница для Employee V2
- src/graphql/queries/employees-v2.ts - GraphQL queries для V2 системы
- src/graphql/resolvers/employees-v2.ts - модульные V2 resolvers с аутентификацией
- src/graphql/resolvers/index.ts - интеграция Employee V2 resolvers
- src/graphql/typedefs.ts - типы для Employee V2 системы

Исправления критических ошибок:
- src/app/api/graphql/route.ts - КРИТИЧНО: исправлен импорт resolvers (resolvers.ts → resolvers/index.ts)
- src/components/employees/employees-dashboard.tsx - адаптация UI к V2 backend с V2→V1 трансформацией
- src/components/employees/employee-*.tsx - исправлены ошибки handleFileUpload во всех формах сотрудников

Система готова к production использованию с V2 модульной архитектурой.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 09:52:09 +03:00

512 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================================================================
// 🧑‍💼 EMPLOYEE FORM V2
// =============================================================================
// Универсальная модульная форма для создания/редактирования сотрудников
'use client'
import { User, FileText, Phone, Briefcase, Save, X } from 'lucide-react'
import React, { useState, useCallback, useMemo } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import type {
EmployeeV2,
CreateEmployeeInput,
UpdateEmployeeInput,
EmployeeStatus,
} from '../types'
interface EmployeeFormProps {
employee?: EmployeeV2 | null // для редактирования
onSubmit: (data: CreateEmployeeInput | UpdateEmployeeInput) => Promise<void>
onCancel: () => void
loading?: boolean
}
export const EmployeeForm = React.memo(function EmployeeForm({
employee,
onSubmit,
onCancel,
loading = false,
}: EmployeeFormProps) {
const isEditing = !!employee
// =============================================================================
// СОСТОЯНИЕ ФОРМЫ
// =============================================================================
const [formData, setFormData] = useState(() => ({
// Личная информация
firstName: employee?.personalInfo.firstName || '',
lastName: employee?.personalInfo.lastName || '',
middleName: employee?.personalInfo.middleName || '',
birthDate: employee?.personalInfo.birthDate
? new Date(employee.personalInfo.birthDate).toISOString().split('T')[0]
: '',
avatar: employee?.personalInfo.avatar || '',
// Паспортные данные
passportPhoto: employee?.documentsInfo.passportPhoto || '',
passportSeries: employee?.documentsInfo.passportSeries || '',
passportNumber: employee?.documentsInfo.passportNumber || '',
passportIssued: employee?.documentsInfo.passportIssued || '',
passportDate: employee?.documentsInfo.passportDate
? new Date(employee.documentsInfo.passportDate).toISOString().split('T')[0]
: '',
// Контактная информация
phone: employee?.contactInfo.phone || '',
email: employee?.contactInfo.email || '',
telegram: employee?.contactInfo.telegram || '',
whatsapp: employee?.contactInfo.whatsapp || '',
address: employee?.contactInfo.address || '',
emergencyContact: employee?.contactInfo.emergencyContact || '',
emergencyPhone: employee?.contactInfo.emergencyPhone || '',
// Рабочая информация
position: employee?.workInfo.position || '',
department: employee?.workInfo.department || '',
hireDate: employee?.workInfo.hireDate
? new Date(employee.workInfo.hireDate).toISOString().split('T')[0]
: '',
salary: employee?.workInfo.salary?.toString() || '',
status: employee?.workInfo.status || 'ACTIVE' as EmployeeStatus,
}))
const [activeTab, setActiveTab] = useState<'personal' | 'documents' | 'contact' | 'work'>('personal')
// =============================================================================
// ВАЛИДАЦИЯ
// =============================================================================
const validation = useMemo(() => {
const errors: string[] = []
if (!formData.firstName.trim()) errors.push('Имя обязательно')
if (!formData.lastName.trim()) errors.push('Фамилия обязательна')
if (!formData.phone.trim()) errors.push('Телефон обязателен')
if (!formData.position.trim()) errors.push('Должность обязательна')
if (!formData.hireDate) errors.push('Дата найма обязательна')
// Валидация телефона
if (formData.phone && !/^\+7\d{10}$/.test(formData.phone.replace(/\s/g, ''))) {
errors.push('Неверный формат телефона')
}
// Валидация email
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
errors.push('Неверный формат email')
}
return {
isValid: errors.length === 0,
errors,
}
}, [formData])
// =============================================================================
// ОБРАБОТЧИКИ
// =============================================================================
const handleInputChange = useCallback((field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}, [])
const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!validation.isValid) {
return
}
const submitData = isEditing ? {
// Для обновления отправляем только измененные поля
personalInfo: {
firstName: formData.firstName,
lastName: formData.lastName,
middleName: formData.middleName || undefined,
birthDate: formData.birthDate ? new Date(formData.birthDate) : undefined,
avatar: formData.avatar || undefined,
},
documentsInfo: {
passportPhoto: formData.passportPhoto || undefined,
passportSeries: formData.passportSeries || undefined,
passportNumber: formData.passportNumber || undefined,
passportIssued: formData.passportIssued || undefined,
passportDate: formData.passportDate ? new Date(formData.passportDate) : undefined,
},
contactInfo: {
phone: formData.phone,
email: formData.email || undefined,
telegram: formData.telegram || undefined,
whatsapp: formData.whatsapp || undefined,
address: formData.address || undefined,
emergencyContact: formData.emergencyContact || undefined,
emergencyPhone: formData.emergencyPhone || undefined,
},
workInfo: {
position: formData.position,
department: formData.department || undefined,
hireDate: new Date(formData.hireDate),
salary: formData.salary ? parseFloat(formData.salary) : undefined,
status: formData.status,
},
} as UpdateEmployeeInput : {
// Для создания все обязательные поля
personalInfo: {
firstName: formData.firstName,
lastName: formData.lastName,
middleName: formData.middleName || undefined,
birthDate: formData.birthDate ? new Date(formData.birthDate) : undefined,
avatar: formData.avatar || undefined,
},
documentsInfo: formData.passportSeries || formData.passportNumber ? {
passportPhoto: formData.passportPhoto || undefined,
passportSeries: formData.passportSeries || undefined,
passportNumber: formData.passportNumber || undefined,
passportIssued: formData.passportIssued || undefined,
passportDate: formData.passportDate ? new Date(formData.passportDate) : undefined,
} : undefined,
contactInfo: {
phone: formData.phone,
email: formData.email || undefined,
telegram: formData.telegram || undefined,
whatsapp: formData.whatsapp || undefined,
address: formData.address || undefined,
emergencyContact: formData.emergencyContact || undefined,
emergencyPhone: formData.emergencyPhone || undefined,
},
workInfo: {
position: formData.position,
department: formData.department || undefined,
hireDate: new Date(formData.hireDate),
salary: formData.salary ? parseFloat(formData.salary) : undefined,
},
} as CreateEmployeeInput
await onSubmit(submitData)
}, [formData, validation.isValid, isEditing, onSubmit])
// =============================================================================
// RENDER
// =============================================================================
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Заголовок */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white">
{isEditing ? 'Редактирование сотрудника' : 'Новый сотрудник'}
</h2>
<Button
type="button"
variant="ghost"
size="sm"
onClick={onCancel}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Ошибки валидации */}
{!validation.isValid && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
<ul className="text-red-300 text-sm space-y-1">
{validation.errors.map((error, index) => (
<li key={index}> {error}</li>
))}
</ul>
</div>
)}
{/* Табы для секций формы */}
<Tabs value={activeTab} onValueChange={setActiveTab as any}>
<TabsList className="glass-card inline-flex h-10 items-center justify-center rounded-lg bg-white/5 p-1 w-full">
<TabsTrigger value="personal" className="text-white data-[state=active]:bg-white/20 flex-1">
<User className="h-4 w-4 mr-2" />
Личные данные
</TabsTrigger>
<TabsTrigger value="documents" className="text-white data-[state=active]:bg-white/20 flex-1">
<FileText className="h-4 w-4 mr-2" />
Документы
</TabsTrigger>
<TabsTrigger value="contact" className="text-white data-[state=active]:bg-white/20 flex-1">
<Phone className="h-4 w-4 mr-2" />
Контакты
</TabsTrigger>
<TabsTrigger value="work" className="text-white data-[state=active]:bg-white/20 flex-1">
<Briefcase className="h-4 w-4 mr-2" />
Работа
</TabsTrigger>
</TabsList>
{/* Личные данные */}
<TabsContent value="personal" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">Имя *</label>
<Input
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Фамилия *</label>
<Input
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Отчество</label>
<Input
value={formData.middleName}
onChange={(e) => handleInputChange('middleName', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Дата рождения</label>
<Input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
</div>
</TabsContent>
{/* Документы */}
<TabsContent value="documents" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">Серия паспорта</label>
<Input
value={formData.passportSeries}
onChange={(e) => handleInputChange('passportSeries', e.target.value)}
placeholder="1234"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Номер паспорта</label>
<Input
value={formData.passportNumber}
onChange={(e) => handleInputChange('passportNumber', e.target.value)}
placeholder="567890"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Кем выдан</label>
<Input
value={formData.passportIssued}
onChange={(e) => handleInputChange('passportIssued', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Дата выдачи</label>
<Input
type="date"
value={formData.passportDate}
onChange={(e) => handleInputChange('passportDate', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
</div>
</TabsContent>
{/* Контакты */}
<TabsContent value="contact" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">Телефон *</label>
<Input
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="+7 (999) 123-45-67"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Email</label>
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="email@company.com"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Telegram</label>
<Input
value={formData.telegram}
onChange={(e) => handleInputChange('telegram', e.target.value)}
placeholder="@username"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">WhatsApp</label>
<Input
value={formData.whatsapp}
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
placeholder="+7 (999) 123-45-67"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div className="md:col-span-2">
<label className="text-white/60 text-sm mb-2 block">Адрес</label>
<Textarea
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="Полный адрес проживания"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Экстренный контакт</label>
<Input
value={formData.emergencyContact}
onChange={(e) => handleInputChange('emergencyContact', e.target.value)}
placeholder="ФИО контактного лица"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Телефон экстренного контакта</label>
<Input
value={formData.emergencyPhone}
onChange={(e) => handleInputChange('emergencyPhone', e.target.value)}
placeholder="+7 (999) 123-45-67"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
</div>
</TabsContent>
{/* Работа */}
<TabsContent value="work" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">Должность *</label>
<Input
value={formData.position}
onChange={(e) => handleInputChange('position', e.target.value)}
placeholder="Менеджер по продажам"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Отдел</label>
<Input
value={formData.department}
onChange={(e) => handleInputChange('department', e.target.value)}
placeholder="Продажи"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Дата найма *</label>
<Input
type="date"
value={formData.hireDate}
onChange={(e) => handleInputChange('hireDate', e.target.value)}
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Зарплата ()</label>
<Input
type="number"
value={formData.salary}
onChange={(e) => handleInputChange('salary', e.target.value)}
placeholder="50000"
className="bg-white/5 border-white/20 text-white"
disabled={loading}
/>
</div>
{isEditing && (
<div>
<label className="text-white/60 text-sm mb-2 block">Статус</label>
<Select
value={formData.status}
onValueChange={(value) => handleInputChange('status', value)}
disabled={loading}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACTIVE">Активен</SelectItem>
<SelectItem value="VACATION">В отпуске</SelectItem>
<SelectItem value="SICK">На больничном</SelectItem>
<SelectItem value="FIRED">Уволен</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
</Tabs>
{/* Кнопки действий */}
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-white/10">
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={loading}
className="text-white border-white/20 hover:bg-white/10"
>
Отмена
</Button>
<Button
type="submit"
disabled={!validation.isValid || loading}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
<Save className="h-4 w-4 mr-2" />
{isEditing ? 'Сохранить' : 'Создать'}
</Button>
</div>
</form>
)
})