diff --git a/src/app/api/graphql/route.ts b/src/app/api/graphql/route.ts index b2bfff5..e51f253 100644 --- a/src/app/api/graphql/route.ts +++ b/src/app/api/graphql/route.ts @@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken' import { NextRequest } from 'next/server' import { Context } from '@/graphql/context' -import { resolvers } from '@/graphql/resolvers' +import { resolvers } from '@/graphql/resolvers/index' import { typeDefs } from '@/graphql/typedefs' import { prisma } from '@/lib/prisma' diff --git a/src/app/employees/page.tsx b/src/app/employees/page.tsx index a865d99..f05360f 100644 --- a/src/app/employees/page.tsx +++ b/src/app/employees/page.tsx @@ -1,34 +1,12 @@ 'use client' -import { lazy, Suspense } from 'react' - import { AuthGuard } from '@/components/auth-guard' - -// Ленивая загрузка дашборда сотрудников -const EmployeesDashboard = lazy(() => - import('@/components/employees/employees-dashboard').then((mod) => ({ - default: mod.EmployeesDashboard, - })), -) - -// Компонент загрузки для сотрудников -function LoadingFallback() { - return ( -
-
-
-

Загрузка управления сотрудниками...

-
-
- ) -} +import { EmployeesDashboard } from '@/components/employees/employees-dashboard' export default function EmployeesPage() { return ( - }> - - + ) } diff --git a/src/components/employees-v2/EmployeeManagement.tsx b/src/components/employees-v2/EmployeeManagement.tsx new file mode 100644 index 0000000..6c1f7b9 --- /dev/null +++ b/src/components/employees-v2/EmployeeManagement.tsx @@ -0,0 +1,192 @@ +// ============================================================================= +// 🧑‍💼 EMPLOYEE MANAGEMENT V2 +// ============================================================================= +// Главный модульный компонент управления сотрудниками V2 +// Следует паттерну MODULAR_ARCHITECTURE_PATTERN.md + +'use client' + +import { FileText, Plus, Users } from 'lucide-react' +import { useCallback, useState } from 'react' + +import { Sidebar } from '@/components/dashboard/sidebar' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' + +import { EmployeeFiltersBlock } from './blocks/EmployeeFiltersBlock' +import { EmployeeListBlock } from './blocks/EmployeeListBlock' +import { EmployeeStatsBlock } from './blocks/EmployeeStatsBlock' +import { EmployeeForm } from './forms/EmployeeForm' +import { useEmployeeCRUD, useEmployeeData, useEmployeeFilters } from './hooks' +import type { CreateEmployeeInput, EmployeeV2, UpdateEmployeeInput } from './types' + +// ============================================================================= +// ГЛАВНЫЙ КОМПОНЕНТ +// ============================================================================= + +export const EmployeeManagement = () => { + // Локальное состояние UI + const [activeTab, setActiveTab] = useState<'employees' | 'reports'>('employees') + const [showCreateForm, setShowCreateForm] = useState(false) + const [editingEmployee, setEditingEmployee] = useState(null) + + // Хуки для бизнес-логики + const filters = useEmployeeFilters() + const employeeData = useEmployeeData(filters.filters) + const employeeCRUD = useEmployeeCRUD() + + // ============================================================================= + // ОБРАБОТЧИКИ ДЕЙСТВИЙ + // ============================================================================= + + const handleCreateEmployee = useCallback(async (data: CreateEmployeeInput) => { + const result = await employeeCRUD.createEmployee(data) + if (result.success) { + setShowCreateForm(false) + employeeData.refetch() + } + }, [employeeCRUD, employeeData]) + + const handleUpdateEmployee = useCallback(async (data: UpdateEmployeeInput) => { + if (!editingEmployee) return + + const result = await employeeCRUD.updateEmployee(editingEmployee.id, data) + if (result.success) { + setEditingEmployee(null) + employeeData.refetch() + } + }, [editingEmployee, employeeCRUD, employeeData]) + + const handleDeleteEmployee = useCallback(async (id: string) => { + const success = await employeeCRUD.deleteEmployee(id) + if (success) { + employeeData.refetch() + } + }, [employeeCRUD, employeeData]) + + const handleEditEmployee = useCallback((employee: EmployeeV2) => { + setEditingEmployee(employee) + setShowCreateForm(false) + }, []) + + const handleCloseForm = useCallback(() => { + setShowCreateForm(false) + setEditingEmployee(null) + }, []) + + // ============================================================================= + // RENDER + // ============================================================================= + + if (employeeData.loading && !employeeData.employees.length) { + return ( +
+ +
+
+
+
Загрузка сотрудников...
+
+
+
+
+ ) + } + + return ( +
+ +
+
+ + {/* Заголовок и управление */} +
+

Управление сотрудниками

+ +
+ +
+
+ + {/* Табы */} + + + + + Сотрудники + + + + Отчеты + + + + {/* Контент табов */} + + + {/* Статистика */} + + + {/* Фильтры и поиск */} + + + {/* Список сотрудников */} + + + + +
+

Отчеты и аналитика

+

Модуль отчетов будет реализован в следующей итерации

+
+
+
+ + {/* Формы создания/редактирования */} + {(showCreateForm || editingEmployee) && ( +
+
+ +
+
+ )} +
+
+
+ ) +} + +// ============================================================================= +// DISPLAY NAME +// ============================================================================= + +EmployeeManagement.displayName = 'EmployeeManagement' \ No newline at end of file diff --git a/src/components/employees-v2/blocks/EmployeeFiltersBlock.tsx b/src/components/employees-v2/blocks/EmployeeFiltersBlock.tsx new file mode 100644 index 0000000..651b3e5 --- /dev/null +++ b/src/components/employees-v2/blocks/EmployeeFiltersBlock.tsx @@ -0,0 +1,233 @@ +// ============================================================================= +// 🧑‍💼 EMPLOYEE FILTERS BLOCK V2 +// ============================================================================= +// Блок фильтров и поиска сотрудников + +'use client' + +import { Search, Filter, X, Calendar } from 'lucide-react' +import React, { useCallback } from 'react' + +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +import type { EmployeesFilterInput, EmployeeStatus, EmployeeSortField } from '../types' + +interface EmployeeFiltersBlockProps { + filters: EmployeesFilterInput + updateFilter: ( + key: K, + value: EmployeesFilterInput[K] + ) => void + resetFilters: () => void + hasActiveFilters: boolean + loading?: boolean +} + +export const EmployeeFiltersBlock = React.memo(function EmployeeFiltersBlock({ + filters, + updateFilter, + resetFilters, + hasActiveFilters, + loading = false, +}: EmployeeFiltersBlockProps) { + + // ============================================================================= + // ОБРАБОТЧИКИ + // ============================================================================= + + const handleSearchChange = useCallback((value: string) => { + updateFilter('search', value || undefined) + }, [updateFilter]) + + const handleStatusChange = useCallback((value: string) => { + if (value === 'all') { + updateFilter('status', undefined) + } else { + updateFilter('status', [value as EmployeeStatus]) + } + }, [updateFilter]) + + const handleDepartmentChange = useCallback((value: string) => { + updateFilter('department', value || undefined) + }, [updateFilter]) + + const handleSortChange = useCallback((value: string) => { + const [sortBy, sortOrder] = value.split(':') as [EmployeeSortField, 'asc' | 'desc'] + updateFilter('sortBy', sortBy) + updateFilter('sortOrder', sortOrder) + }, [updateFilter]) + + const handleDateFromChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value + updateFilter('dateFrom', value ? new Date(value) : undefined) + }, [updateFilter]) + + const handleDateToChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value + updateFilter('dateTo', value ? new Date(value) : undefined) + }, [updateFilter]) + + // ============================================================================= + // RENDER + // ============================================================================= + + return ( + +
+ + {/* Основной поиск */} +
+
+ + handleSearchChange(e.target.value)} + className="pl-10 bg-white/5 border-white/20 text-white placeholder:text-white/40" + disabled={loading} + /> +
+ + {hasActiveFilters && ( + + )} +
+ + {/* Дополнительные фильтры */} +
+ + {/* Статус */} +
+ + +
+ + {/* Отдел */} +
+ + handleDepartmentChange(e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder:text-white/40" + disabled={loading} + /> +
+ + {/* Сортировка */} +
+ + +
+ + {/* Размер страницы */} +
+ + +
+
+ + {/* Фильтры по датам найма */} +
+
+ + +
+ +
+ + +
+
+
+
+ ) +}) \ No newline at end of file diff --git a/src/components/employees-v2/blocks/EmployeeListBlock.tsx b/src/components/employees-v2/blocks/EmployeeListBlock.tsx new file mode 100644 index 0000000..804b017 --- /dev/null +++ b/src/components/employees-v2/blocks/EmployeeListBlock.tsx @@ -0,0 +1,277 @@ +// ============================================================================= +// 🧑‍💼 EMPLOYEE LIST BLOCK V2 +// ============================================================================= +// Модульный блок списка сотрудников с пагинацией + +'use client' + +import { Edit, Trash2, Calendar, Phone, Mail, Users } from 'lucide-react' +import React, { useCallback } from 'react' + +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' + +import type { EmployeeV2, PaginationInfo } from '../types' + +interface EmployeeListBlockProps { + employees: EmployeeV2[] + pagination: PaginationInfo | null + loading?: boolean + onEdit?: (employee: EmployeeV2) => void + onDelete?: (id: string) => void + onViewSchedule?: (employee: EmployeeV2) => void + onPageChange?: (page: number) => void +} + +export const EmployeeListBlock = React.memo(function EmployeeListBlock({ + employees, + pagination, + loading = false, + onEdit, + onDelete, + onViewSchedule, + onPageChange, +}: EmployeeListBlockProps) { + + // ============================================================================= + // ОБРАБОТЧИКИ + // ============================================================================= + + const handleEdit = useCallback((employee: EmployeeV2) => { + onEdit?.(employee) + }, [onEdit]) + + const handleDelete = useCallback((id: string) => { + if (confirm('Вы уверены что хотите удалить сотрудника?')) { + onDelete?.(id) + } + }, [onDelete]) + + const handleViewSchedule = useCallback((employee: EmployeeV2) => { + onViewSchedule?.(employee) + }, [onViewSchedule]) + + const handlePageChange = useCallback((page: number) => { + onPageChange?.(page) + }, [onPageChange]) + + // ============================================================================= + // RENDER HELPERS + // ============================================================================= + + const getStatusBadge = (status: string) => { + const variants = { + ACTIVE: 'bg-green-500/20 text-green-300 border-green-500/30', + VACATION: 'bg-blue-500/20 text-blue-300 border-blue-500/30', + SICK: 'bg-orange-500/20 text-orange-300 border-orange-500/30', + FIRED: 'bg-red-500/20 text-red-300 border-red-500/30', + } + + const labels = { + ACTIVE: 'Активен', + VACATION: 'В отпуске', + SICK: 'На больничном', + FIRED: 'Уволен', + } + + return ( + + {labels[status as keyof typeof labels] || status} + + ) + } + + const formatSalary = (salary?: number) => { + if (!salary) return '-' + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + maximumFractionDigits: 0, + }).format(salary) + } + + const formatDate = (date: Date) => { + return new Date(date).toLocaleDateString('ru-RU') + } + + // ============================================================================= + // RENDER + // ============================================================================= + + if (loading && employees.length === 0) { + return ( + +
+
Загружаем сотрудников...
+
+
+ ) + } + + if (employees.length === 0) { + return ( + +
+ +

Сотрудники не найдены

+

Попробуйте изменить параметры поиска или добавьте первого сотрудника

+
+
+ ) + } + + return ( + + + {/* Заголовок списка */} +
+

+ Сотрудники {pagination && `(${pagination.total})`} +

+ {loading && ( +
Обновление...
+ )} +
+ + {/* Список сотрудников */} +
+ {employees.map((employee) => ( +
+ + {/* Основная информация */} +
+ {/* Аватар */} +
+ {employee.personalInfo.firstName[0]}{employee.personalInfo.lastName[0]} +
+ + {/* Данные */} +
+
+ {employee.personalInfo.fullName} +
+
+ {employee.workInfo.position} + {employee.workInfo.department && ` • ${employee.workInfo.department}`} +
+
+
+ + {employee.contactInfo.phone} +
+ {employee.contactInfo.email && ( +
+ + {employee.contactInfo.email} +
+ )} +
+
+
+ + {/* Статус и действия */} +
+ + {/* Статус */} + {getStatusBadge(employee.workInfo.status)} + + {/* Зарплата */} + {employee.workInfo.salary && ( +
+ {formatSalary(employee.workInfo.salary)} +
+ )} + + {/* Дата найма */} +
+ с {formatDate(employee.workInfo.hireDate)} +
+ + {/* Действия */} +
+ + + +
+
+
+ ))} +
+ + {/* Пагинация */} + {pagination && pagination.totalPages > 1 && ( +
+ + +
+ {Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => { + const pageNum = pagination.page <= 3 + ? i + 1 + : pagination.page + i - 2 + + if (pageNum > pagination.totalPages) return null + + return ( + + ) + })} +
+ + +
+ )} +
+ ) +}) \ No newline at end of file diff --git a/src/components/employees-v2/blocks/EmployeeStatsBlock.tsx b/src/components/employees-v2/blocks/EmployeeStatsBlock.tsx new file mode 100644 index 0000000..7820154 --- /dev/null +++ b/src/components/employees-v2/blocks/EmployeeStatsBlock.tsx @@ -0,0 +1,115 @@ +// ============================================================================= +// 🧑‍💼 EMPLOYEE STATS BLOCK V2 +// ============================================================================= +// Блок статистики сотрудников + +'use client' + +import { Users, UserCheck, Plane, HeartPulse, UserX, DollarSign } from 'lucide-react' +import React from 'react' + +import { Card } from '@/components/ui/card' + +import type { EmployeeStats } from '../types' + +interface EmployeeStatsBlockProps { + stats: EmployeeStats | null + loading?: boolean +} + +export const EmployeeStatsBlock = React.memo(function EmployeeStatsBlock({ + stats, + loading = false, +}: EmployeeStatsBlockProps) { + + if (loading || !stats) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + +
+
+
+
+
+ ))} +
+ ) + } + + const formatSalary = (salary?: number) => { + if (!salary) return '-' + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + maximumFractionDigits: 0, + }).format(salary) + } + + const statItems = [ + { + icon: Users, + label: 'Всего', + value: stats.total, + color: 'from-blue-500 to-blue-600', + }, + { + icon: UserCheck, + label: 'Активных', + value: stats.active, + color: 'from-green-500 to-green-600', + }, + { + icon: Plane, + label: 'В отпуске', + value: stats.vacation, + color: 'from-blue-500 to-blue-600', + }, + { + icon: HeartPulse, + label: 'На больничном', + value: stats.sick, + color: 'from-orange-500 to-orange-600', + }, + { + icon: UserX, + label: 'Уволенных', + value: stats.fired, + color: 'from-red-500 to-red-600', + }, + { + icon: DollarSign, + label: 'Средняя ЗП', + value: formatSalary(stats.averageSalary), + color: 'from-purple-500 to-purple-600', + isText: true, + }, + ] + + return ( +
+ {statItems.map((item, index) => { + const Icon = item.icon + + return ( + +
+
+ +
+ +
+

+ {item.label} +

+

+ {item.isText ? item.value : item.value.toLocaleString('ru-RU')} +

+
+
+
+ ) + })} +
+ ) +}) \ No newline at end of file diff --git a/src/components/employees-v2/blocks/index.ts b/src/components/employees-v2/blocks/index.ts new file mode 100644 index 0000000..233df6f --- /dev/null +++ b/src/components/employees-v2/blocks/index.ts @@ -0,0 +1,8 @@ +// ============================================================================= +// 🧑‍💼 EMPLOYEE V2 BLOCKS EXPORTS +// ============================================================================= +// Централизованный экспорт всех блоков Employee V2 + +export { EmployeeListBlock } from './EmployeeListBlock' +export { EmployeeStatsBlock } from './EmployeeStatsBlock' +export { EmployeeFiltersBlock } from './EmployeeFiltersBlock' \ No newline at end of file diff --git a/src/components/employees-v2/forms/EmployeeForm.tsx b/src/components/employees-v2/forms/EmployeeForm.tsx new file mode 100644 index 0000000..587acda --- /dev/null +++ b/src/components/employees-v2/forms/EmployeeForm.tsx @@ -0,0 +1,512 @@ +// ============================================================================= +// 🧑‍💼 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 + 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 ( +
+ + {/* Заголовок */} +
+

+ {isEditing ? 'Редактирование сотрудника' : 'Новый сотрудник'} +

+ +
+ + {/* Ошибки валидации */} + {!validation.isValid && ( +
+
    + {validation.errors.map((error, index) => ( +
  • • {error}
  • + ))} +
+
+ )} + + {/* Табы для секций формы */} + + + + + Личные данные + + + + Документы + + + + Контакты + + + + Работа + + + + {/* Личные данные */} + +
+
+ + handleInputChange('firstName', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('lastName', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('middleName', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('birthDate', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+
+ + {/* Документы */} + +
+
+ + handleInputChange('passportSeries', e.target.value)} + placeholder="1234" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('passportNumber', e.target.value)} + placeholder="567890" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('passportIssued', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('passportDate', e.target.value)} + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+
+ + {/* Контакты */} + +
+
+ + handleInputChange('phone', e.target.value)} + placeholder="+7 (999) 123-45-67" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('email', e.target.value)} + placeholder="email@company.com" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('telegram', e.target.value)} + placeholder="@username" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ + handleInputChange('whatsapp', e.target.value)} + placeholder="+7 (999) 123-45-67" + className="bg-white/5 border-white/20 text-white" + disabled={loading} + /> +
+
+ +