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

1252 lines
83 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } 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>
);
};