Files
mkd/components/hr/EmployeeCardModal.tsx

1252 lines
83 KiB
TypeScript
Raw Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect } from 'react';
import { Employee, EmployeeVacation, EmployeeSickLeave, EmployeeTermination, User, UserRole, District } from '../../types';
import { authFetch, backendApi } from '../../services/apiClient';
import {
X, User as UserIcon, Phone, Calendar, MapPin, Briefcase, MessageCircle,
FileText, CreditCard, Building2, Camera, Lock, Eye, EyeOff,
Mail, Download, Plus, Edit, Receipt, FileCheck, Banknote,
Umbrella, Heart, LogOut, Trash2, AlertCircle
} from 'lucide-react';
import { EmployeeFormModal } from './EmployeeFormModal';
import { WorkCalendarModal, AbsenceType } from './WorkCalendarModal';
interface EmployeeCardModalProps {
employee: Employee;
onClose: () => void;
onUpdate?: (employee: Employee) => void;
currentUser?: User; // Текущий пользователь для проверки прав доступа
}
export const EmployeeCardModal: React.FC<EmployeeCardModalProps> = ({ employee, onClose, onUpdate, currentUser }) => {
const [showHrData, setShowHrData] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentEmployee, setCurrentEmployee] = useState(employee);
const [vacations, setVacations] = useState<EmployeeVacation[]>([]);
const [sickLeaves, setSickLeaves] = useState<EmployeeSickLeave[]>([]);
const [terminations, setTerminations] = useState<EmployeeTermination[]>([]);
const [loading, setLoading] = useState(false);
const [isWorkCalendarModalOpen, setIsWorkCalendarModalOpen] = useState(false);
const [workCalendarModalType, setWorkCalendarModalType] = useState<AbsenceType | undefined>(undefined);
const [isTerminationModalOpen, setIsTerminationModalOpen] = useState(false);
const [terminationDate, setTerminationDate] = useState('');
const [terminationReason, setTerminationReason] = useState('');
const [terminationInitiatedBy, setTerminationInitiatedBy] = useState('');
const [terminationLoading, setTerminationLoading] = useState(false);
const [terminationError, setTerminationError] = useState('');
const [districts, setDistricts] = useState<District[]>([]);
const activeUserRole = currentUser?.role || 'OTHER';
useEffect(() => {
backendApi.getDistricts().then(setDistricts).catch(() => setDistricts([]));
}, []);
const canViewHrData = activeUserRole === 'HR_MANAGER' || activeUserRole === 'FINANCIER' || activeUserRole === 'DIRECTOR';
// Проверка прав для создания отгулов/пропусков:
// 1. Сотрудник может ставить отгулы себе
// 2. Руководитель может ставить отгулы себе и своим подчиненным
// 3. HR_MANAGER и DIRECTOR могут ставить отгулы всем
const canCreateAbsence = (): boolean => {
if (!currentUser) return false;
const isSelf = currentUser.id === currentEmployee.id;
const isHrOrDirector = activeUserRole === 'HR_MANAGER' || activeUserRole === 'DIRECTOR';
// Если это сам сотрудник или HR/Директор - разрешаем
if (isSelf || isHrOrDirector) {
return true;
}
// Для руководителей нужно проверить, является ли текущий пользователь руководителем этого сотрудника
// Это будет проверяться на backend, но на frontend мы можем показать кнопку, если роль позволяет быть руководителем
const canBeManager = ['DIRECTOR', 'ENGINEER', 'MASTER'].includes(activeUserRole);
return canBeManager; // Показываем кнопку, но backend проверит фактическую иерархию
};
useEffect(() => {
if (currentEmployee.hrData) {
setVacations(currentEmployee.hrData.vacations || []);
setSickLeaves(currentEmployee.hrData.sickLeaves || []);
setTerminations(currentEmployee.hrData.terminations || []);
}
}, [currentEmployee]);
const getDistrictName = (districtId: string) => {
const district = districts.find(d => d.id === districtId);
return district?.name || districtId || 'Не указан';
};
const assignedDistrictIds = currentEmployee.assignedDistrictIds?.length
? currentEmployee.assignedDistrictIds
: (currentEmployee.assignedDistrictId ? [currentEmployee.assignedDistrictId] : []);
const districtNamesDisplay = assignedDistrictIds.length
? assignedDistrictIds.map(id => getDistrictName(id)).join(', ')
: 'Не указаны';
const formatDate = (dateString?: string) => {
if (!dateString) return 'Не указана';
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
};
const getMessengerIcon = (messenger: 'Max' | 'Telegram') => {
return MessageCircle; // Можно заменить на специфичные иконки
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'bg-emerald-50 text-emerald-600';
case 'vacation': return 'bg-amber-50 text-amber-600';
case 'inactive': return 'bg-red-50 text-red-600';
default: return 'bg-slate-50 text-slate-600';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'active': return 'В строю';
case 'vacation': return 'В отпуске';
case 'inactive': return 'Неактивен';
default: return status;
}
};
const getCertificateStatusColor = (status: string) => {
switch (status) {
case 'requested': return 'bg-amber-50 text-amber-600';
case 'issued': return 'bg-blue-50 text-blue-600';
case 'ready': return 'bg-emerald-50 text-emerald-600';
default: return 'bg-slate-50 text-slate-600';
}
};
const getCertificateStatusLabel = (status: string) => {
switch (status) {
case 'requested': return 'Запрошена';
case 'issued': return 'Выдана';
case 'ready': return 'Готова';
default: return status;
}
};
const handleUpdate = (updatedEmployee: Employee) => {
setCurrentEmployee(updatedEmployee);
setIsEditing(false);
if (onUpdate) {
onUpdate(updatedEmployee);
}
};
const fetchEmployeeData = async () => {
try {
setLoading(true);
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/employees' : `${apiBaseUrl}/employees`;
const response = await authFetch(apiUrl);
if (response.ok) {
const employees = await response.json();
const updated = employees.find((e: Employee) => e.id === currentEmployee.id);
if (updated) {
setCurrentEmployee(updated);
if (updated.hrData) {
setVacations(updated.hrData.vacations || []);
setSickLeaves(updated.hrData.sickLeaves || []);
setTerminations(updated.hrData.terminations || []);
}
}
}
} catch (error) {
console.error('Error fetching employee data:', error);
} finally {
setLoading(false);
}
};
const handleAddVacation = () => {
setWorkCalendarModalType('vacation');
setIsWorkCalendarModalOpen(true);
};
const handleAddSickLeave = () => {
setWorkCalendarModalType('sick_leave');
setIsWorkCalendarModalOpen(true);
};
const handleAddAbsence = (type: 'day_off' | 'absence' | 'late' | 'early_leave') => {
setWorkCalendarModalType(type);
setIsWorkCalendarModalOpen(true);
};
const handleTerminateEmployee = () => {
setIsTerminationModalOpen(true);
// Устанавливаем текущего пользователя как инициатора по умолчанию
if (currentUser) {
setTerminationInitiatedBy(currentUser.name || 'HR Manager');
} else {
setTerminationInitiatedBy('HR Manager');
}
};
const handleTerminationSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setTerminationError('');
// Валидация
if (!terminationDate) {
setTerminationError('Укажите дату увольнения');
return;
}
if (!terminationReason.trim()) {
setTerminationError('Укажите причину увольнения');
return;
}
if (!terminationInitiatedBy.trim()) {
setTerminationError('Укажите, кто инициировал увольнение');
return;
}
setTerminationLoading(true);
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiUrl = (import.meta.env.DEV || !apiBaseUrl)
? `/api/employees/${currentEmployee.id}/terminations`
: `${apiBaseUrl}/employees/${currentEmployee.id}/terminations`;
const response = await authFetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
terminationDate,
reason: terminationReason,
initiatedBy: terminationInitiatedBy,
status: 'initiated'
})
});
if (response.ok) {
await fetchEmployeeData();
// Закрываем модальное окно и сбрасываем поля
setIsTerminationModalOpen(false);
setTerminationDate('');
setTerminationReason('');
setTerminationInitiatedBy(currentUser?.name || 'HR Manager');
setTerminationError('');
} else {
const errorData = await response.json().catch(() => ({}));
setTerminationError(errorData.error || 'Ошибка при оформлении увольнения');
}
} catch (error) {
console.error('Error terminating employee:', error);
setTerminationError('Ошибка при оформлении увольнения');
} finally {
setTerminationLoading(false);
}
};
const handleTerminationCancel = () => {
setIsTerminationModalOpen(false);
setTerminationDate('');
setTerminationReason('');
setTerminationInitiatedBy(currentUser?.name || 'HR Manager');
setTerminationError('');
};
const getVacationStatusColor = (status: string) => {
switch (status) {
case 'planned': return 'bg-slate-50 text-slate-600';
case 'approved': return 'bg-blue-50 text-blue-600';
case 'active': return 'bg-emerald-50 text-emerald-600';
case 'completed': return 'bg-slate-50 text-slate-600';
case 'canceled': return 'bg-red-50 text-red-600';
default: return 'bg-slate-50 text-slate-600';
}
};
const getVacationStatusLabel = (status: string) => {
switch (status) {
case 'planned': return 'Запланирован';
case 'approved': return 'Утвержден';
case 'active': return 'Активен';
case 'completed': return 'Завершен';
case 'canceled': return 'Отменен';
default: return status;
}
};
const getSickLeaveStatusColor = (status: string) => {
switch (status) {
case 'active': return 'bg-amber-50 text-amber-600';
case 'closed': return 'bg-emerald-50 text-emerald-600';
case 'canceled': return 'bg-red-50 text-red-600';
default: return 'bg-slate-50 text-slate-600';
}
};
const getSickLeaveStatusLabel = (status: string) => {
switch (status) {
case 'active': return 'Активен';
case 'closed': return 'Закрыт';
case 'canceled': return 'Отменен';
default: return status;
}
};
const getTerminationStatusColor = (status: string) => {
switch (status) {
case 'initiated': return 'bg-slate-50 text-slate-600';
case 'in_progress': return 'bg-blue-50 text-blue-600';
case 'completed': return 'bg-emerald-50 text-emerald-600';
case 'canceled': return 'bg-red-50 text-red-600';
default: return 'bg-slate-50 text-slate-600';
}
};
const getTerminationStatusLabel = (status: string) => {
switch (status) {
case 'initiated': return 'Инициировано';
case 'in_progress': return 'В процессе';
case 'completed': return 'Завершено';
case 'canceled': return 'Отменено';
default: return status;
}
};
if (isEditing) {
return (
<EmployeeFormModal
employee={currentEmployee}
onClose={() => setIsEditing(false)}
onSave={handleUpdate}
/>
);
}
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-4xl 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-start">
<div className="flex items-center gap-4">
{currentEmployee.photoUrl ? (
<img
src={currentEmployee.photoUrl.startsWith('http')
? currentEmployee.photoUrl
: `${import.meta.env.VITE_API_BASE_URL?.replace('/api', '') || 'http://localhost:4000'}${currentEmployee.photoUrl}`}
alt={currentEmployee.name}
className="w-16 h-16 rounded-2xl object-cover border-2 border-slate-200"
onError={(e) => {
// Если фото не загрузилось, показываем инициалы
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const parent = target.parentElement;
if (parent) {
const fallback = document.createElement('div');
fallback.className = 'w-16 h-16 rounded-2xl bg-slate-100 flex items-center justify-center text-2xl font-black text-slate-400';
fallback.textContent = currentEmployee.name.split(' ').map(n => n[0]).join('');
parent.appendChild(fallback);
}
}}
/>
) : (
<div className="w-16 h-16 rounded-2xl bg-slate-100 flex items-center justify-center text-2xl font-black text-slate-400">
{currentEmployee.name.split(' ').map(n => n[0]).join('')}
</div>
)}
<div>
<h3 className="text-2xl font-bold text-slate-800">{currentEmployee.name}</h3>
<p className="text-sm text-slate-500 mt-1">{currentEmployee.position}</p>
<span className={`inline-block mt-2 text-xs font-black px-2 py-1 rounded-full uppercase tracking-tighter ${getStatusColor(currentEmployee.status)}`}>
{getStatusLabel(currentEmployee.status)}
</span>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setIsEditing(true)}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
title="Редактировать"
>
<Edit className="w-5 h-5 text-slate-500"/>
</button>
<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>
</div>
{/* Content */}
<div 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">
<UserIcon className="w-4 h-4"/> Основная информация
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">ФИО</span>
<span className="text-base font-bold text-slate-800">{currentEmployee.name}</span>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата рождения
</span>
<span className="text-base font-bold text-slate-800">{formatDate(currentEmployee.birthDate)}</span>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1 flex items-center gap-1">
<Phone className="w-3 h-3"/> Телефон
</span>
<a
href={`tel:${currentEmployee.phone}`}
className="text-base font-bold text-primary-600 hover:text-primary-700"
>
{currentEmployee.phone}
</a>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1 flex items-center gap-1">
<Briefcase className="w-3 h-3"/> Должность
</span>
<span className="text-base font-bold text-slate-800">{currentEmployee.position}</span>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1 flex items-center gap-1">
<MapPin className="w-3 h-3"/> {assignedDistrictIds.length > 1 ? 'Участки' : 'Участок'}
</span>
<span className="text-base font-bold text-slate-800">{districtNamesDisplay}</span>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата регистрации
</span>
<span className="text-base font-bold text-slate-800">{formatDate(currentEmployee.registrationDate)}</span>
</div>
</div>
</div>
{/* Мессенджеры */}
{currentEmployee.messengerLogins && currentEmployee.messengerLogins.length > 0 && (
<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="grid grid-cols-1 md:grid-cols-2 gap-3">
{currentEmployee.messengerLogins.map((msg, idx) => {
const Icon = getMessengerIcon(msg.messenger);
return (
<div key={idx} className="bg-slate-50 p-4 rounded-xl border border-slate-100 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary-50 flex items-center justify-center">
<Icon className="w-5 h-5 text-primary-600"/>
</div>
<div className="flex-1">
<span className="text-[10px] text-slate-400 font-bold uppercase block">{msg.messenger}</span>
<span className="text-sm font-bold text-slate-800">{msg.login}</span>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Фото */}
{currentEmployee.photoUrl && (
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<Camera className="w-4 h-4"/> Фото
</h4>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<img
src={currentEmployee.photoUrl.startsWith('http')
? currentEmployee.photoUrl
: `${import.meta.env.VITE_API_BASE_URL?.replace('/api', '') || 'http://localhost:4000'}${currentEmployee.photoUrl}`}
alt={currentEmployee.name}
className="w-32 h-32 rounded-xl object-cover border-2 border-slate-200"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
</div>
</div>
)}
{/* HR и Бухгалтерские данные */}
{canViewHrData && currentEmployee.hrData && (
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider flex items-center gap-2">
<Lock className="w-4 h-4"/> Данные для HR и Бухгалтерии
</h4>
<button
onClick={() => setShowHrData(!showHrData)}
className="flex items-center gap-2 text-xs font-bold text-slate-600 hover:text-slate-800 transition-colors"
>
{showHrData ? (
<>
<EyeOff className="w-4 h-4"/> Скрыть
</>
) : (
<>
<Eye className="w-4 h-4"/> Показать
</>
)}
</button>
</div>
{showHrData && (
<div className="space-y-4">
{/* Паспортные данные */}
{currentEmployee.hrData.passportData && (
<div className="bg-slate-50 p-4 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-3">
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Серия и номер</span>
<span className="text-sm font-bold text-slate-800">
{currentEmployee.hrData.passportData.series} {currentEmployee.hrData.passportData.number}
</span>
</div>
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Выдан</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.passportData.issuedBy}</span>
</div>
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Дата выдачи</span>
<span className="text-sm font-bold text-slate-800">{formatDate(currentEmployee.hrData.passportData.issuedDate)}</span>
</div>
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Адрес регистрации</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.passportData.registrationAddress}</span>
</div>
</div>
</div>
)}
{/* Трудовая книжка */}
{currentEmployee.hrData.laborBook && (
<div className="bg-slate-50 p-4 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">
<FileText className="w-4 h-4"/> Трудовая книжка
</h5>
<div className="space-y-3">
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Номер</span>
<span className="text-sm font-bold text-slate-800">
{currentEmployee.hrData.laborBook.series && `${currentEmployee.hrData.laborBook.series} `}
{currentEmployee.hrData.laborBook.number}
</span>
</div>
{currentEmployee.hrData.laborBook.entries && currentEmployee.hrData.laborBook.entries.length > 0 && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-2">Записи</span>
<div className="space-y-2">
{currentEmployee.hrData.laborBook.entries.map((entry, idx) => (
<div key={idx} className="bg-white p-3 rounded-lg border border-slate-200">
<div className="text-xs font-bold text-slate-600">{formatDate(entry.date)}</div>
<div className="text-sm font-bold text-slate-800 mt-1">{entry.organization}</div>
<div className="text-xs text-slate-500 mt-1">{entry.position}</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Заказ справок */}
{currentEmployee.hrData.certificates && currentEmployee.hrData.certificates.length > 0 && (
<div className="bg-slate-50 p-4 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">
<FileText className="w-4 h-4"/> Заказ справок
</h5>
<button 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-2">
{currentEmployee.hrData.certificates.map((cert, idx) => (
<div key={idx} className="bg-white p-3 rounded-lg border border-slate-200 flex items-center justify-between">
<div className="flex-1">
<div className="text-sm font-bold text-slate-800">{cert.type}</div>
<div className="text-xs text-slate-500 mt-1">
Запрошена: {formatDate(cert.requestedDate)}
{cert.issuedDate && ` • Выдана: ${formatDate(cert.issuedDate)}`}
</div>
</div>
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${getCertificateStatusColor(cert.status)}`}>
{getCertificateStatusLabel(cert.status)}
</span>
</div>
))}
</div>
</div>
)}
{/* Прочие документы */}
{currentEmployee.hrData.otherDocuments && currentEmployee.hrData.otherDocuments.length > 0 && (
<div className="bg-slate-50 p-4 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">
<FileText className="w-4 h-4"/> Прочие документы
</h5>
<div className="space-y-2">
{currentEmployee.hrData.otherDocuments.map((doc, idx) => (
<div key={idx} className="bg-white p-3 rounded-lg border border-slate-200 flex items-center justify-between">
<div className="flex-1">
<div className="text-sm font-bold text-slate-800">{doc.name}</div>
<div className="text-xs text-slate-500 mt-1">
{doc.type} {formatDate(doc.date)}
</div>
</div>
{doc.fileUrl && (
<a
href={doc.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="p-2 bg-primary-50 text-primary-600 rounded-lg hover:bg-primary-100 transition-colors"
>
<Download className="w-4 h-4"/>
</a>
)}
</div>
))}
</div>
</div>
)}
{/* Бухгалтерская информация */}
{currentEmployee.hrData.accountingData && (
<div className="bg-slate-50 p-4 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-3">
{currentEmployee.hrData.accountingData.inn && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">ИНН</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.inn}</span>
</div>
)}
{currentEmployee.hrData.accountingData.snils && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">СНИЛС</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.snils}</span>
</div>
)}
{currentEmployee.hrData.accountingData.bankName && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Банк</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.bankName}</span>
</div>
)}
{currentEmployee.hrData.accountingData.bankAccount && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Расчетный счет</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.bankAccount}</span>
</div>
)}
{currentEmployee.hrData.accountingData.correspondentAccount && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Корреспондентский счет</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.correspondentAccount}</span>
</div>
)}
{currentEmployee.hrData.accountingData.bik && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">БИК</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.bik}</span>
</div>
)}
{currentEmployee.hrData.accountingData.taxId && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">КПП</span>
<span className="text-sm font-bold text-slate-800">{currentEmployee.hrData.accountingData.taxId}</span>
</div>
)}
</div>
</div>
)}
{/* Характеристики договора */}
{currentEmployee.hrData.contracts && currentEmployee.hrData.contracts.length > 0 && (
<div className="bg-slate-50 p-4 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">
<FileCheck className="w-4 h-4"/> Характеристики договора
</h5>
<div className="space-y-3">
{currentEmployee.hrData.contracts.map((contract, idx) => (
<div key={idx} className="bg-white p-4 rounded-lg border border-slate-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Тип договора</span>
<span className="text-sm font-bold text-slate-800">{contract.contractType}</span>
</div>
{contract.contractNumber && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Номер договора</span>
<span className="text-sm font-bold text-slate-800">{contract.contractNumber}</span>
</div>
)}
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Дата начала</span>
<span className="text-sm font-bold text-slate-800">{formatDate(contract.startDate)}</span>
</div>
{contract.endDate && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Дата окончания</span>
<span className="text-sm font-bold text-slate-800">{formatDate(contract.endDate)}</span>
</div>
)}
{contract.probationPeriodDays && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Испытательный срок</span>
<span className="text-sm font-bold text-slate-800">{contract.probationPeriodDays} дней</span>
</div>
)}
{contract.workSchedule && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">График работы</span>
<span className="text-sm font-bold text-slate-800">{contract.workSchedule}</span>
</div>
)}
{contract.workMode && (
<div>
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Режим работы</span>
<span className="text-sm font-bold text-slate-800">{contract.workMode}</span>
</div>
)}
{contract.contractTerms && (
<div className="md:col-span-2">
<span className="text-[10px] text-slate-400 font-bold uppercase block mb-1">Дополнительные условия</span>
<span className="text-sm font-bold text-slate-800">{contract.contractTerms}</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)}
{/* Рабочий календарь */}
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider flex items-center gap-2">
<Calendar className="w-4 h-4"/> Рабочий календарь
</h4>
{canViewHrData && (
<button
onClick={() => {
setWorkCalendarModalType(undefined);
setIsWorkCalendarModalOpen(true);
}}
className="px-3 py-1.5 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-xs font-bold flex items-center gap-2"
title="Добавить запись"
>
<Plus className="w-4 h-4"/> Добавить
</button>
)}
</div>
{/* Отпуска */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h5 className="text-xs font-bold text-slate-600 flex items-center gap-2">
<Umbrella className="w-3 h-3"/> Отпуска
</h5>
{canViewHrData && (
<button
onClick={handleAddVacation}
className="p-1 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
title="Добавить отпуск"
>
<Plus className="w-3 h-3"/>
</button>
)}
</div>
{vacations.length > 0 ? (
<div className="space-y-2">
{vacations.map((vacation) => (
<div key={vacation.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="text-sm font-bold text-slate-800">
{formatDate(vacation.startDate)} - {formatDate(vacation.endDate)}
</div>
<div className="text-xs text-slate-500 mt-1">
{vacation.daysCount} дней {vacation.vacationType === 'annual' ? 'Ежегодный' : vacation.vacationType === 'unpaid' ? 'Без сохранения зарплаты' : vacation.vacationType}
{vacation.approvedBy && ` • Утвержден: ${vacation.approvedBy}`}
</div>
</div>
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${getVacationStatusColor(vacation.status)}`}>
{getVacationStatusLabel(vacation.status)}
</span>
</div>
</div>
))}
</div>
) : (
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-sm text-slate-500 text-center">
Нет записей об отпусках
</div>
)}
</div>
{/* Больничные */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h5 className="text-xs font-bold text-slate-600 flex items-center gap-2">
<Heart className="w-3 h-3"/> Больничные
</h5>
{canViewHrData && (
<button
onClick={handleAddSickLeave}
className="p-1 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
title="Добавить больничный"
>
<Plus className="w-3 h-3"/>
</button>
)}
</div>
{sickLeaves.length > 0 ? (
<div className="space-y-2">
{sickLeaves.map((sickLeave) => (
<div key={sickLeave.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="text-sm font-bold text-slate-800">
{formatDate(sickLeave.startDate)}
{sickLeave.endDate && ` - ${formatDate(sickLeave.endDate)}`}
{sickLeave.daysCount && ` (${sickLeave.daysCount} дней)`}
</div>
<div className="text-xs text-slate-500 mt-1">
{sickLeave.sickLeaveNumber && `${sickLeave.sickLeaveNumber}`}
{sickLeave.medicalInstitution && `${sickLeave.medicalInstitution}`}
{sickLeave.diagnosis && `Диагноз: ${sickLeave.diagnosis}`}
</div>
</div>
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${getSickLeaveStatusColor(sickLeave.status)}`}>
{getSickLeaveStatusLabel(sickLeave.status)}
</span>
</div>
{sickLeave.fileUrl && (
<a
href={sickLeave.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1 text-xs text-primary-600 hover:text-primary-700"
>
<Download className="w-3 h-3"/> Скачать больничный лист
</a>
)}
</div>
))}
</div>
) : (
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-sm text-slate-500 text-center">
Нет записей о больничных
</div>
)}
</div>
{/* Отгулы и прогуски */}
{canViewHrData && (
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h5 className="text-xs font-bold text-slate-600 flex items-center gap-2">
<Calendar className="w-3 h-3"/> Отгулы и пропуски
</h5>
{canCreateAbsence() && (
<div className="flex gap-1">
<button
onClick={() => handleAddAbsence('day_off')}
className="px-2 py-1 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-[10px] font-bold"
title="Отгул"
>
Отгул
</button>
<button
onClick={() => handleAddAbsence('absence')}
className="px-2 py-1 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors text-[10px] font-bold"
title="Прогул"
>
Прогул
</button>
<button
onClick={() => handleAddAbsence('late')}
className="px-2 py-1 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-[10px] font-bold"
title="Опоздание"
>
Опозд.
</button>
<button
onClick={() => handleAddAbsence('early_leave')}
className="px-2 py-1 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors text-[10px] font-bold"
title="Ранний уход"
>
Уход
</button>
</div>
)}
</div>
{currentEmployee.hrData?.absences && currentEmployee.hrData.absences.length > 0 ? (
<div className="space-y-2">
{currentEmployee.hrData.absences.map((absence) => {
const typeLabels: Record<string, { label: string; color: string }> = {
day_off: { label: 'Отгул', color: 'bg-amber-50 text-amber-600' },
absence: { label: 'Прогул', color: 'bg-orange-50 text-orange-600' },
late: { label: 'Опоздание', color: 'bg-purple-50 text-purple-600' },
early_leave: { label: 'Ранний уход', color: 'bg-indigo-50 text-indigo-600' },
};
const typeInfo = typeLabels[absence.absenceType] || { label: absence.absenceType, color: 'bg-slate-50 text-slate-600' };
const statusInfo = getVacationStatusColor(absence.status);
return (
<div key={absence.id} className="bg-slate-50 p-3 rounded-xl border border-slate-100">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="text-sm font-bold text-slate-800">
{formatDate(absence.startDate)}
{absence.endDate && absence.endDate !== absence.startDate && (
<> - {formatDate(absence.endDate)}</>
)}
{absence.startTime && (
<> в {absence.startTime}</>
)}
</div>
<div className="text-xs text-slate-500 mt-1">
{absence.daysCount} {absence.daysCount === 1 ? 'день' : absence.daysCount < 5 ? 'дня' : 'дней'}
{absence.reason && `${absence.reason}`}
</div>
</div>
<div className="flex flex-col items-end gap-1">
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${typeInfo.color}`}>
{typeInfo.label}
</span>
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${statusInfo}`}>
{absence.status === 'pending' ? 'На согласовании' : absence.status === 'approved' ? 'Утверждено' : absence.status === 'rejected' ? 'Отклонено' : 'Отменено'}
</span>
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100 text-xs text-slate-500 text-center">
Нет записей
</div>
)}
</div>
)}
</div>
{/* Увольнения */}
{canViewHrData && (
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider flex items-center gap-2">
<LogOut className="w-4 h-4"/> Увольнения
</h4>
{currentEmployee.status !== 'inactive' && (
<button
onClick={handleTerminateEmployee}
className="px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-xs font-bold"
title="Оформить увольнение"
>
Уволить
</button>
)}
</div>
{terminations.length > 0 ? (
<div className="space-y-3">
{terminations.map((termination) => (
<div key={termination.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm font-bold text-slate-800">
Дата увольнения: {formatDate(termination.terminationDate)}
</div>
<div className="text-xs text-slate-500 mt-1">
Причина: {termination.reason} Инициировано: {termination.initiatedBy}
</div>
</div>
<span className={`text-[9px] font-black px-2 py-1 rounded-full uppercase tracking-tighter ${getTerminationStatusColor(termination.status)}`}>
{getTerminationStatusLabel(termination.status)}
</span>
</div>
{/* Договор на увольнение */}
{termination.terminationContractNumber && (
<div className="mt-3 pt-3 border-t border-slate-200">
<div className="text-xs font-bold text-slate-600 mb-2">Договор на увольнение</div>
<div className="text-xs text-slate-700">
{termination.terminationContractNumber}
{termination.terminationContractDate && ` от ${formatDate(termination.terminationContractDate)}`}
</div>
{termination.terminationContractFileUrl && (
<a
href={termination.terminationContractFileUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs text-primary-600 hover:text-primary-700"
>
<Download className="w-3 h-3"/> Скачать договор
</a>
)}
</div>
)}
{/* Расчеты */}
{(termination.finalSettlementAmount || termination.compensationAmount || termination.severancePay) && (
<div className="mt-3 pt-3 border-t border-slate-200">
<div className="text-xs font-bold text-slate-600 mb-2">Расчеты</div>
<div className="grid grid-cols-2 gap-2 text-xs">
{termination.unusedVacationDays && (
<div>
<span className="text-slate-500">Неиспользованные дни отпуска:</span>
<span className="font-bold text-slate-800 ml-1">{termination.unusedVacationDays}</span>
</div>
)}
{termination.compensationAmount && (
<div>
<span className="text-slate-500">Компенсация за отпуск:</span>
<span className="font-bold text-slate-800 ml-1">{termination.compensationAmount.toLocaleString('ru-RU')} </span>
</div>
)}
{termination.severancePay && (
<div>
<span className="text-slate-500">Выходное пособие:</span>
<span className="font-bold text-slate-800 ml-1">{termination.severancePay.toLocaleString('ru-RU')} </span>
</div>
)}
{termination.otherPayments && (
<div>
<span className="text-slate-500">Прочие выплаты:</span>
<span className="font-bold text-slate-800 ml-1">{termination.otherPayments.toLocaleString('ru-RU')} </span>
</div>
)}
{termination.deductions && (
<div>
<span className="text-slate-500">Удержания:</span>
<span className="font-bold text-red-600 ml-1">{termination.deductions.toLocaleString('ru-RU')} </span>
</div>
)}
{termination.finalSettlementAmount && (
<div className="col-span-2 pt-2 border-t border-slate-200">
<span className="text-slate-600 font-bold">Итого к выплате:</span>
<span className="font-bold text-emerald-600 ml-2 text-base">{termination.finalSettlementAmount.toLocaleString('ru-RU')} </span>
</div>
)}
</div>
{termination.settlementDocumentNumber && (
<div className="mt-2 text-xs text-slate-700">
Документ расчета: {termination.settlementDocumentNumber}
{termination.settlementDocumentDate && ` от ${formatDate(termination.settlementDocumentDate)}`}
</div>
)}
{termination.settlementDocumentFileUrl && (
<a
href={termination.settlementDocumentFileUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs text-primary-600 hover:text-primary-700"
>
<Download className="w-3 h-3"/> Скачать документ расчета
</a>
)}
</div>
)}
</div>
))}
</div>
) : (
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-sm text-slate-500 text-center">
Нет записей об увольнениях
</div>
)}
</div>
)}
{/* Кнопки действий */}
<div className="flex gap-3 pt-4 border-t border-slate-200">
<a
href={`tel:${currentEmployee.phone}`}
className="flex-1 py-3 bg-slate-50 text-slate-700 rounded-xl text-xs font-black uppercase flex items-center justify-center gap-2 hover:bg-slate-100 transition-all"
>
<Phone className="w-4 h-4"/> Позвонить
</a>
{currentEmployee.messengerLogins && currentEmployee.messengerLogins.length > 0 && (
<button className="flex-1 py-3 bg-primary-600 text-white rounded-xl text-xs font-black uppercase flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 active:scale-95 transition-all">
<MessageCircle className="w-4 h-4"/> Написать
</button>
)}
</div>
</div>
</div>
{/* Модальное окно рабочего календаря */}
{isWorkCalendarModalOpen && (
<WorkCalendarModal
employee={currentEmployee}
onClose={() => {
setIsWorkCalendarModalOpen(false);
setWorkCalendarModalType(undefined);
}}
onSave={async () => {
await fetchEmployeeData();
}}
type={workCalendarModalType}
currentUser={currentUser}
/>
)}
{/* Модальное окно увольнения */}
{isTerminationModalOpen && (
<div
className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in"
onClick={handleTerminationCancel}
>
<div
className="bg-white rounded-2xl w-full max-w-md shadow-2xl animate-slide-up"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="p-6 border-b border-slate-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-red-50 flex items-center justify-center">
<LogOut className="w-5 h-5 text-red-600"/>
</div>
<div>
<h3 className="text-lg font-bold text-slate-800">Оформление увольнения</h3>
<p className="text-xs text-slate-500 mt-0.5">{currentEmployee.name}</p>
</div>
</div>
<button
onClick={handleTerminationCancel}
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={handleTerminationSubmit} className="p-6 space-y-4">
{terminationError && (
<div className="bg-red-50 border border-red-200 rounded-xl p-3 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0"/>
<p className="text-sm text-red-600">{terminationError}</p>
</div>
)}
{/* Дата увольнения */}
<div>
<label className="block text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<Calendar className="w-4 h-4"/> Дата увольнения <span className="text-red-500">*</span>
</label>
<input
type="date"
value={terminationDate}
onChange={(e) => setTerminationDate(e.target.value)}
required
className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Причина увольнения */}
<div>
<label className="block text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<FileText className="w-4 h-4"/> Причина увольнения <span className="text-red-500">*</span>
</label>
<textarea
value={terminationReason}
onChange={(e) => setTerminationReason(e.target.value)}
required
rows={3}
placeholder="Укажите причину увольнения..."
className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent resize-none"
/>
</div>
{/* Кто инициировал */}
<div>
<label className="block text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<UserIcon className="w-4 h-4"/> Кто инициировал увольнение <span className="text-red-500">*</span>
</label>
<input
type="text"
value={terminationInitiatedBy}
onChange={(e) => setTerminationInitiatedBy(e.target.value)}
required
placeholder="ФИО инициатора"
className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Предупреждение */}
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
<p className="text-xs text-amber-800">
<strong>Внимание:</strong> После оформления увольнения будут автоматически созданы договор на увольнение и расчетные документы.
</p>
</div>
{/* Кнопки */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={handleTerminationCancel}
disabled={terminationLoading}
className="flex-1 px-4 py-3 bg-slate-100 text-slate-700 rounded-xl font-bold hover:bg-slate-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Отмена
</button>
<button
type="submit"
disabled={terminationLoading}
className="flex-1 px-4 py-3 bg-red-600 text-white rounded-xl font-bold hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{terminationLoading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Оформление...
</>
) : (
<>
<LogOut className="w-4 h-4"/> Оформить увольнение
</>
)}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};