
Модуляризация 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>
512 lines
21 KiB
TypeScript
512 lines
21 KiB
TypeScript
// =============================================================================
|
||
// 🧑💼 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>
|
||
)
|
||
}) |