Files
mkd/components/hr/EmployeeFormModal.tsx

1189 lines
76 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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>
);
};