Files
mkd/components/hr/EmployeeFormModal.tsx
2026-02-04 00:17:04 +05:00

1189 lines
76 KiB
TypeScript
Executable File
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.
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Employee, District, Position } from '../../types';
import { X, User, Phone, Calendar, MapPin, Briefcase, MessageCircle, Plus, Trash2, Upload, Image as ImageIcon, Receipt, FileCheck, CreditCard, Users } from 'lucide-react';
import { backendApi, authFetch } from '../../services/apiClient';
interface EmployeeFormModalProps {
employee?: Employee | null;
onClose: () => void;
onSave: (employee: Employee) => void;
}
export const EmployeeFormModal: React.FC<EmployeeFormModalProps> = ({ employee, onClose, onSave }) => {
const isEditMode = !!employee;
const [formData, setFormData] = useState({
name: employee?.name || '',
position: employee?.position || '',
phone: employee?.phone || '',
status: employee?.status || 'active' as 'active' | 'vacation' | 'inactive',
salary: employee?.salary || 0,
assignedDistrictIds: (employee?.assignedDistrictIds?.length ? employee.assignedDistrictIds : (employee?.assignedDistrictId ? [employee.assignedDistrictId] : [])) as string[],
managerId: employee?.managerId || '',
birthDate: employee?.birthDate || '',
photoUrl: employee?.photoUrl || '',
registrationDate: employee?.registrationDate || '',
messengerLogins: employee?.messengerLogins || [] as Array<{ messenger: 'Max' | 'Telegram'; login: string }>,
hrData: {
passportData: employee?.hrData?.passportData || undefined,
laborBook: employee?.hrData?.laborBook || undefined,
accountingData: employee?.hrData?.accountingData || undefined,
contracts: employee?.hrData?.contracts || [],
},
});
const [availableEmployees, setAvailableEmployees] = useState<Employee[]>([]);
const [loadingEmployees, setLoadingEmployees] = useState(false);
const [districts, setDistricts] = useState<District[]>([]);
const [loadingDistricts, setLoadingDistricts] = useState(false);
const [positions, setPositions] = useState<Position[]>([]);
const [loadingPositions, setLoadingPositions] = useState(false);
// Загружаем справочник должностей из API
useEffect(() => {
const loadPositions = async () => {
try {
setLoadingPositions(true);
const list = await backendApi.getPositions();
setPositions(list);
} catch (error) {
console.error('Error loading positions:', error);
setPositions([]);
} finally {
setLoadingPositions(false);
}
};
loadPositions();
}, []);
// Загружаем список участков из API
useEffect(() => {
const loadDistricts = async () => {
try {
setLoadingDistricts(true);
const list = await backendApi.getDistricts();
setDistricts(list);
} catch (error) {
console.error('Error loading districts:', error);
setDistricts([]);
} finally {
setLoadingDistricts(false);
}
};
loadDistricts();
}, []);
// Загружаем список сотрудников для выбора руководителя
useEffect(() => {
const loadEmployees = async () => {
try {
setLoadingEmployees(true);
const employees = await backendApi.getEmployees();
const filtered = employees.filter(emp => emp.id !== employee?.id);
setAvailableEmployees(filtered);
} catch (error) {
console.error('Error loading employees:', error);
} finally {
setLoadingEmployees(false);
}
};
loadEmployees();
}, [employee?.id]);
// Руководители: только активные сотрудники (не уволенные) с руководящей должностью по справочнику.
// Сравниваем должности с приведением к одному регистру и без лишних пробелов.
// Если в справочнике нет руководящих должностей — показываем всех активных (чтобы форма работала до настройки справочника).
const managerialPositionNamesLower = useMemo(
() => new Set(
positions
.filter(p => p.isManagerial)
.map(p => (p.name || '').trim().toLowerCase())
),
[positions]
);
const hasAnyManagerialInReference = managerialPositionNamesLower.size > 0;
const eligibleManagers = useMemo(
() => availableEmployees.filter(emp => {
if (emp.status !== 'active') return false;
if (!hasAnyManagerialInReference) return true; // fallback: все активные, если справочник не настроен
const empPos = (emp.position || '').trim().toLowerCase();
return empPos !== '' && managerialPositionNamesLower.has(empPos);
}),
[availableEmployees, managerialPositionNamesLower, hasAnyManagerialInReference]
);
const [customPosition, setCustomPosition] = useState('');
const [showCustomPosition, setShowCustomPosition] = useState(false);
// При загрузке должностей: если у сотрудника должность не из справочника — показываем поле «Другое»
useEffect(() => {
if (employee?.position && positions.length > 0) {
const inList = positions.some(p => p.name === employee!.position);
setShowCustomPosition(!inList);
if (!inList) setCustomPosition(employee.position);
}
}, [employee?.position, positions]);
const [photoFile, setPhotoFile] = useState<File | null>(null);
const [photoPreview, setPhotoPreview] = useState<string | null>(employee?.photoUrl || null);
const [isUploadingPhoto, setIsUploadingPhoto] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [newMessenger, setNewMessenger] = useState<{ messenger: 'Max' | 'Telegram'; login: string }>({
messenger: 'Telegram',
login: ''
});
const handlePhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// Проверяем тип файла
if (!file.type.startsWith('image/')) {
alert('Пожалуйста, выберите изображение');
return;
}
// Проверяем размер файла (5MB)
if (file.size > 5 * 1024 * 1024) {
alert('Размер файла не должен превышать 5MB');
return;
}
setPhotoFile(file);
// Создаем превью
const reader = new FileReader();
reader.onloadend = () => {
setPhotoPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const handleRemovePhoto = () => {
setPhotoFile(null);
setPhotoPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Используем customPosition если выбрано "Другое"
const finalPosition = showCustomPosition && customPosition ? customPosition : formData.position;
if (!formData.name || !finalPosition || !formData.phone || !formData.salary) {
alert('Заполните обязательные поля: ФИО, должность, телефон и оклад');
return;
}
setIsUploadingPhoto(true);
let finalPhotoUrl = formData.photoUrl;
let employeeId = employee?.id;
// Если это новый сотрудник, сначала создаем его
if (!employeeId) {
try {
// В режиме разработки используем относительные пути для работы через прокси Vite
// В продакшене используем полный URL из переменной окружения
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/employees' : `${apiBaseUrl}/employees`;
const newEmployeeData = {
name: formData.name.trim(),
position: finalPosition.trim(),
phone: formData.phone.trim(),
status: formData.status,
salary: Number(formData.salary),
assignedDistrictIds: formData.assignedDistrictIds && formData.assignedDistrictIds.length > 0 ? formData.assignedDistrictIds : [],
managerId: formData.managerId && formData.managerId.trim() ? formData.managerId.trim() : null,
birthDate: formData.birthDate && formData.birthDate.trim() ? formData.birthDate.trim() : null,
registrationDate: formData.registrationDate && formData.registrationDate.trim() ? formData.registrationDate.trim() : null,
messengerLogins: formData.messengerLogins.length > 0 ? formData.messengerLogins : undefined,
hrData: {
...(formData.hrData.passportData && { passportData: formData.hrData.passportData }),
...(formData.hrData.laborBook && { laborBook: formData.hrData.laborBook }),
...(formData.hrData.accountingData && Object.keys(formData.hrData.accountingData).length > 0 && { accountingData: formData.hrData.accountingData }),
...(formData.hrData.contracts && formData.hrData.contracts.length > 0 && { contracts: formData.hrData.contracts }),
},
};
const createResponse = await authFetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newEmployeeData)
});
if (!createResponse.ok) {
let errorMessage = 'Ошибка при создании сотрудника';
try {
const errorData = await createResponse.json();
errorMessage = errorData.error || errorMessage;
} catch (e) {
// Если не удалось распарсить JSON, пытаемся прочитать текст ответа
try {
const text = await createResponse.text();
if (text) {
errorMessage = text;
}
} catch (textError) {
// Если и текст не удалось прочитать, используем статус
errorMessage = `Ошибка ${createResponse.status}: ${createResponse.statusText}`;
}
}
throw new Error(errorMessage);
}
const createdEmployee = await createResponse.json();
employeeId = createdEmployee.id;
} catch (error) {
console.error('Error creating employee:', error);
const errorMessage = error instanceof Error ? error.message : 'Ошибка при создании сотрудника';
alert(errorMessage);
setIsUploadingPhoto(false);
return;
}
}
// Загружаем фото, если оно было выбрано
if (photoFile && employeeId) {
try {
const formDataPhoto = new FormData();
formDataPhoto.append('photo', photoFile);
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const photoUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/employees/${employeeId}/photo` : `${apiBaseUrl}/employees/${employeeId}/photo`;
const response = await authFetch(photoUrl, {
method: 'POST',
body: formDataPhoto
});
if (!response.ok) {
throw new Error('Ошибка при загрузке фото');
}
const result = await response.json();
finalPhotoUrl = result.photoUrl;
} catch (error) {
console.error('Error uploading photo:', error);
alert('Ошибка при загрузке фото. Продолжить без фото?');
}
}
setIsUploadingPhoto(false);
// Если это был новый сотрудник, получаем его полные данные из API
if (!employee && employeeId) {
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const employeeUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/employees/${employeeId}` : `${apiBaseUrl}/employees/${employeeId}`;
const response = await authFetch(employeeUrl);
if (response.ok) {
const fullEmployee = await response.json();
window.dispatchEvent(new CustomEvent('mkd-employees-changed'));
onSave(fullEmployee);
return;
}
} catch (error) {
console.error('Error fetching created employee:', error);
}
}
// Для существующего сотрудника обновляем данные через API
if (employeeId) {
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const updateUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/employees/${employeeId}` : `${apiBaseUrl}/employees/${employeeId}`;
const updateData = {
name: formData.name.trim(),
position: finalPosition.trim(),
phone: formData.phone.trim(),
status: formData.status,
salary: Number(formData.salary),
assignedDistrictIds: formData.assignedDistrictIds && formData.assignedDistrictIds.length > 0 ? formData.assignedDistrictIds : [],
managerId: formData.managerId && formData.managerId.trim() ? formData.managerId.trim() : null,
birthDate: formData.birthDate && formData.birthDate.trim() ? formData.birthDate.trim() : null,
photoUrl: finalPhotoUrl || undefined,
registrationDate: formData.registrationDate && formData.registrationDate.trim() ? formData.registrationDate.trim() : null,
messengerLogins: formData.messengerLogins.length > 0 ? formData.messengerLogins : undefined,
hrData: {
...(formData.hrData.passportData && { passportData: formData.hrData.passportData }),
...(formData.hrData.laborBook && { laborBook: formData.hrData.laborBook }),
...(formData.hrData.accountingData && Object.keys(formData.hrData.accountingData).length > 0 && { accountingData: formData.hrData.accountingData }),
...(formData.hrData.contracts && formData.hrData.contracts.length > 0 && { contracts: formData.hrData.contracts }),
},
};
const updateResponse = await authFetch(updateUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (!updateResponse.ok) {
throw new Error('Ошибка при обновлении сотрудника');
}
const updatedEmployee = await updateResponse.json();
window.dispatchEvent(new CustomEvent('mkd-employees-changed'));
onSave(updatedEmployee);
} catch (error) {
console.error('Error updating employee:', error);
alert('Ошибка при обновлении сотрудника');
}
} else {
// Для нового сотрудника (fallback)
const employeeData: Employee = {
id: employeeId!,
name: formData.name,
position: finalPosition,
phone: formData.phone,
status: formData.status,
salary: formData.salary,
assignedDistrictIds: formData.assignedDistrictIds || [],
assignedDistrictId: formData.assignedDistrictIds?.[0] || undefined,
managerId: formData.managerId || undefined,
birthDate: formData.birthDate || undefined,
photoUrl: finalPhotoUrl || undefined,
registrationDate: formData.registrationDate || undefined,
messengerLogins: formData.messengerLogins.length > 0 ? formData.messengerLogins : undefined,
hrData: formData.hrData,
};
window.dispatchEvent(new CustomEvent('mkd-employees-changed'));
onSave(employeeData);
}
};
const addMessenger = () => {
if (newMessenger.login.trim()) {
setFormData({
...formData,
messengerLogins: [...formData.messengerLogins, newMessenger]
});
setNewMessenger({ messenger: 'Telegram', login: '' });
}
};
const removeMessenger = (index: number) => {
setFormData({
...formData,
messengerLogins: formData.messengerLogins.filter((_, i) => i !== index)
});
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in"
onClick={onClose}
>
<div
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl animate-slide-up"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 bg-white border-b border-slate-200 p-6 rounded-t-2xl z-10">
<div className="flex justify-between items-center">
<h3 className="text-2xl font-bold text-slate-800">
{isEditMode ? 'Редактирование сотрудника' : 'Добавление сотрудника'}
</h3>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-500"/>
</button>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Основная информация */}
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<User className="w-4 h-4"/> Основная информация
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
ФИО *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Иванов Иван Иванович"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Briefcase className="w-3 h-3"/> Должность *
</label>
{loadingPositions ? (
<div className="w-full p-3 border border-slate-200 rounded-xl text-sm text-slate-500 bg-slate-50">
Загрузка справочника должностей...
</div>
) : (
<>
<select
value={showCustomPosition ? '__OTHER__' : formData.position}
onChange={(e) => {
const selectedValue = e.target.value;
if (selectedValue === '__OTHER__') {
setShowCustomPosition(true);
setCustomPosition('');
setFormData({ ...formData, position: '' });
} else if (selectedValue === '') {
setShowCustomPosition(false);
setCustomPosition('');
setFormData({ ...formData, position: '' });
} else {
setShowCustomPosition(false);
setCustomPosition('');
setFormData({ ...formData, position: selectedValue });
}
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required={!showCustomPosition}
>
<option value="">Выберите должность</option>
{positions.map((pos) => (
<option key={pos.id} value={pos.name}>
{pos.name}
</option>
))}
<option value="__OTHER__">Другое (ввести вручную)</option>
</select>
{showCustomPosition && (
<input
type="text"
value={customPosition}
onChange={(e) => {
setCustomPosition(e.target.value);
setFormData({ ...formData, position: e.target.value });
}}
placeholder="Введите должность"
className="w-full mt-2 p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
)}
</>
)}
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Phone className="w-3 h-3"/> Телефон *
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
placeholder="+7 900 123-45-67"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Оклад () *
</label>
<input
type="number"
value={formData.salary}
onChange={(e) => setFormData({ ...formData, salary: parseFloat(e.target.value) || 0 })}
placeholder="45000 или 45250.50"
min="0"
step="0.01"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<MapPin className="w-3 h-3"/> Участки (можно несколько)
</label>
<div className="w-full p-3 border border-slate-200 rounded-xl text-sm focus-within:ring-2 focus-within:ring-primary-500 outline-none max-h-40 overflow-y-auto space-y-2" style={{ minHeight: '80px' }}>
{loadingDistricts ? (
<p className="text-slate-500">Загрузка участков...</p>
) : districts.length === 0 ? (
<p className="text-slate-500">Нет участков</p>
) : (
districts.map(district => (
<label key={district.id} className="flex items-center gap-2 cursor-pointer hover:bg-slate-50 rounded px-1 py-0.5">
<input
type="checkbox"
checked={formData.assignedDistrictIds.includes(district.id)}
onChange={(e) => {
const next = e.target.checked
? [...formData.assignedDistrictIds, district.id]
: formData.assignedDistrictIds.filter(id => id !== district.id);
setFormData({ ...formData, assignedDistrictIds: next });
}}
className="rounded border-slate-300 text-primary-600 focus:ring-primary-500"
/>
<span>{district.name}</span>
</label>
))
)}
</div>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Users className="w-3 h-3"/> Руководитель
</label>
<select
value={formData.managerId}
onChange={(e) => setFormData({ ...formData, managerId: e.target.value })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
disabled={loadingEmployees}
>
<option value="">Не выбран</option>
{loadingEmployees ? (
<option value="" disabled>Загрузка...</option>
) : (
eligibleManagers.map(emp => (
<option key={emp.id} value={emp.id}>
{emp.name} {emp.position}
</option>
))
)}
{!loadingEmployees && eligibleManagers.length === 0 && (
<option value="" disabled>Нет сотрудников с руководящей должностью (только активные)</option>
)}
</select>
<p className="text-[10px] text-slate-500 mt-1">Только активные сотрудники с руководящей должностью из справочника</p>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Статус
</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'active' | 'vacation' | 'inactive' })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="active">В строю</option>
<option value="vacation">В отпуске</option>
<option value="inactive">Неактивен</option>
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата рождения
</label>
<input
type="date"
value={formData.birthDate}
onChange={(e) => setFormData({ ...formData, birthDate: e.target.value })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата регистрации
</label>
<input
type="date"
value={formData.registrationDate}
onChange={(e) => setFormData({ ...formData, registrationDate: e.target.value })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div className="md:col-span-2">
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<ImageIcon className="w-3 h-3"/> Фото сотрудника
</label>
<div className="space-y-3">
{photoPreview && (
<div className="relative inline-block">
<img
src={photoPreview.startsWith('data:') || photoPreview.startsWith('http')
? photoPreview
: `${import.meta.env.VITE_API_BASE_URL?.replace('/api', '') || 'http://localhost:4000'}${photoPreview}`}
alt="Превью фото"
className="w-32 h-32 rounded-xl object-cover border-2 border-slate-200"
/>
<button
type="button"
onClick={handleRemovePhoto}
className="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
>
<X className="w-3 h-3"/>
</button>
</div>
)}
<div className="flex items-center gap-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoChange}
className="hidden"
id="photo-upload"
/>
<label
htmlFor="photo-upload"
className="flex items-center gap-2 px-4 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm font-bold text-slate-700 hover:bg-slate-100 cursor-pointer transition-colors"
>
<Upload className="w-4 h-4"/>
{photoFile ? 'Изменить фото' : 'Загрузить фото'}
</label>
{isUploadingPhoto && (
<span className="text-sm text-slate-500">Загрузка...</span>
)}
</div>
<p className="text-xs text-slate-400">
Поддерживаются форматы: JPG, PNG, GIF, WEBP. Максимальный размер: 5MB
</p>
</div>
</div>
</div>
</div>
{/* HR данные */}
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<FileCheck className="w-4 h-4"/> HR данные
</h4>
{/* Паспортные данные */}
<div className="mb-6 p-4 bg-slate-50 rounded-xl border border-slate-100">
<h5 className="text-xs font-black text-slate-600 uppercase tracking-wider mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4"/> Паспортные данные
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Серия</label>
<input
type="text"
value={formData.hrData.passportData?.series || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
passportData: {
...formData.hrData.passportData,
series: e.target.value,
number: formData.hrData.passportData?.number || '',
issuedBy: formData.hrData.passportData?.issuedBy || '',
issuedDate: formData.hrData.passportData?.issuedDate || '',
registrationAddress: formData.hrData.passportData?.registrationAddress || '',
}
}
})}
placeholder="1234"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Номер</label>
<input
type="text"
value={formData.hrData.passportData?.number || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
passportData: {
...formData.hrData.passportData,
series: formData.hrData.passportData?.series || '',
number: e.target.value,
issuedBy: formData.hrData.passportData?.issuedBy || '',
issuedDate: formData.hrData.passportData?.issuedDate || '',
registrationAddress: formData.hrData.passportData?.registrationAddress || '',
}
}
})}
placeholder="567890"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Выдан</label>
<input
type="text"
value={formData.hrData.passportData?.issuedBy || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
passportData: {
...formData.hrData.passportData,
series: formData.hrData.passportData?.series || '',
number: formData.hrData.passportData?.number || '',
issuedBy: e.target.value,
issuedDate: formData.hrData.passportData?.issuedDate || '',
registrationAddress: formData.hrData.passportData?.registrationAddress || '',
}
}
})}
placeholder="УФМС России"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Дата выдачи</label>
<input
type="date"
value={formData.hrData.passportData?.issuedDate || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
passportData: {
...formData.hrData.passportData,
series: formData.hrData.passportData?.series || '',
number: formData.hrData.passportData?.number || '',
issuedBy: formData.hrData.passportData?.issuedBy || '',
issuedDate: e.target.value,
registrationAddress: formData.hrData.passportData?.registrationAddress || '',
}
}
})}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div className="md:col-span-2">
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Адрес регистрации</label>
<input
type="text"
value={formData.hrData.passportData?.registrationAddress || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
passportData: {
...formData.hrData.passportData,
series: formData.hrData.passportData?.series || '',
number: formData.hrData.passportData?.number || '',
issuedBy: formData.hrData.passportData?.issuedBy || '',
issuedDate: formData.hrData.passportData?.issuedDate || '',
registrationAddress: e.target.value,
}
}
})}
placeholder="г. Москва, ул. Примерная, д. 1, кв. 1"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
</div>
{/* Бухгалтерская информация */}
<div className="mb-6 p-4 bg-slate-50 rounded-xl border border-slate-100">
<h5 className="text-xs font-black text-slate-600 uppercase tracking-wider mb-3 flex items-center gap-2">
<Receipt className="w-4 h-4"/> Бухгалтерская информация
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">ИНН</label>
<input
type="text"
value={formData.hrData.accountingData?.inn || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
inn: e.target.value,
}
}
})}
placeholder="123456789012"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">СНИЛС</label>
<input
type="text"
value={formData.hrData.accountingData?.snils || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
snils: e.target.value,
}
}
})}
placeholder="123-456-789 01"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Название банка</label>
<input
type="text"
value={formData.hrData.accountingData?.bankName || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
bankName: e.target.value,
}
}
})}
placeholder="ПАО Сбербанк"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Расчетный счет</label>
<input
type="text"
value={formData.hrData.accountingData?.bankAccount || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
bankAccount: e.target.value,
}
}
})}
placeholder="40817810099910004312"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Корреспондентский счет</label>
<input
type="text"
value={formData.hrData.accountingData?.correspondentAccount || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
correspondentAccount: e.target.value,
}
}
})}
placeholder="30101810400000000225"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">БИК</label>
<input
type="text"
value={formData.hrData.accountingData?.bik || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
bik: e.target.value,
}
}
})}
placeholder="044525225"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">КПП</label>
<input
type="text"
value={formData.hrData.accountingData?.taxId || ''}
onChange={(e) => setFormData({
...formData,
hrData: {
...formData.hrData,
accountingData: {
...formData.hrData.accountingData,
taxId: e.target.value,
}
}
})}
placeholder="773101001"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
</div>
{/* Характеристики договора */}
<div className="mb-6 p-4 bg-slate-50 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-3">
<h5 className="text-xs font-black text-slate-600 uppercase tracking-wider flex items-center gap-2">
<FileCheck className="w-4 h-4"/> Характеристики договора
</h5>
<button
type="button"
onClick={() => setFormData({
...formData,
hrData: {
...formData.hrData,
contracts: [...(formData.hrData.contracts || []), {
contractType: 'Трудовой договор',
startDate: new Date().toISOString().split('T')[0],
}]
}
})}
className="p-1.5 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
<Plus className="w-4 h-4"/>
</button>
</div>
<div className="space-y-4">
{(formData.hrData.contracts || []).map((contract, idx) => (
<div key={idx} className="bg-white p-4 rounded-lg border border-slate-200">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-bold text-slate-600">Договор #{idx + 1}</span>
<button
type="button"
onClick={() => setFormData({
...formData,
hrData: {
...formData.hrData,
contracts: formData.hrData.contracts?.filter((_, i) => i !== idx) || []
}
})}
className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
>
<Trash2 className="w-4 h-4"/>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Тип договора *</label>
<select
value={contract.contractType}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], contractType: e.target.value };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
>
<option value="Трудовой договор">Трудовой договор</option>
<option value="Срочный трудовой договор">Срочный трудовой договор</option>
<option value="Договор ГПХ">Договор ГПХ</option>
<option value="Договор подряда">Договор подряда</option>
<option value="Договор оказания услуг">Договор оказания услуг</option>
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Номер договора</label>
<input
type="text"
value={contract.contractNumber || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], contractNumber: e.target.value };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
placeholder="ТД-2024-001"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Дата начала *</label>
<input
type="date"
value={contract.startDate}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], startDate: e.target.value };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Дата окончания</label>
<input
type="date"
value={contract.endDate || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], endDate: e.target.value || undefined };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Испытательный срок (дней)</label>
<input
type="number"
value={contract.probationPeriodDays || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], probationPeriodDays: e.target.value ? parseInt(e.target.value) : undefined };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
placeholder="90"
min="0"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">График работы</label>
<select
value={contract.workSchedule || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], workSchedule: e.target.value || undefined };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Не указан</option>
<option value="Полный рабочий день">Полный рабочий день</option>
<option value="Неполный рабочий день">Неполный рабочий день</option>
<option value="Сменный график">Сменный график</option>
<option value="Гибкий график">Гибкий график</option>
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Режим работы</label>
<select
value={contract.workMode || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], workMode: e.target.value || undefined };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Не указан</option>
<option value="Офис">Офис</option>
<option value="Удаленно">Удаленно</option>
<option value="Гибрид">Гибрид</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">Дополнительные условия</label>
<textarea
value={contract.contractTerms || ''}
onChange={(e) => {
const newContracts = [...(formData.hrData.contracts || [])];
newContracts[idx] = { ...newContracts[idx], contractTerms: e.target.value || undefined };
setFormData({
...formData,
hrData: { ...formData.hrData, contracts: newContracts }
});
}}
placeholder="Дополнительные условия договора..."
rows={3}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
</div>
))}
{(!formData.hrData.contracts || formData.hrData.contracts.length === 0) && (
<div className="text-center py-4 text-sm text-slate-400">
Нет договоров. Нажмите "+" чтобы добавить.
</div>
)}
</div>
</div>
</div>
{/* Мессенджеры */}
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<MessageCircle className="w-4 h-4"/> Мессенджеры
</h4>
<div className="space-y-3">
{formData.messengerLogins.map((msg, idx) => (
<div key={idx} className="flex items-center gap-2 p-3 bg-slate-50 rounded-xl border border-slate-100">
<div className="flex-1">
<span className="text-xs font-bold text-slate-600 uppercase">{msg.messenger}</span>
<span className="ml-2 text-sm text-slate-800">{msg.login}</span>
</div>
<button
type="button"
onClick={() => removeMessenger(idx)}
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4"/>
</button>
</div>
))}
<div className="flex gap-2">
<select
value={newMessenger.messenger}
onChange={(e) => setNewMessenger({ ...newMessenger, messenger: e.target.value as 'Max' | 'Telegram' })}
className="p-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="Telegram">Telegram</option>
<option value="Max">Max</option>
</select>
<input
type="text"
value={newMessenger.login}
onChange={(e) => setNewMessenger({ ...newMessenger, login: e.target.value })}
placeholder="Логин или номер телефона"
className="flex-1 p-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
<button
type="button"
onClick={addMessenger}
className="p-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
<Plus className="w-4 h-4"/>
</button>
</div>
</div>
</div>
{/* Кнопки */}
<div className="flex gap-3 pt-4 border-t border-slate-200">
<button
type="button"
onClick={onClose}
className="flex-1 py-3 bg-slate-50 text-slate-700 rounded-xl text-xs font-black uppercase hover:bg-slate-100 transition-all"
>
Отмена
</button>
<button
type="submit"
disabled={isUploadingPhoto}
className="flex-1 py-3 bg-primary-600 text-white rounded-xl text-xs font-black uppercase shadow-lg shadow-primary-500/20 hover:bg-primary-700 active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUploadingPhoto ? 'Сохранение...' : (isEditMode ? 'Сохранить' : 'Добавить')}
</button>
</div>
</form>
</div>
</div>
);
};