1189 lines
76 KiB
TypeScript
Executable File
1189 lines
76 KiB
TypeScript
Executable File
|
||
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>
|
||
);
|
||
};
|