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 = ({ employee, onClose, onUpdate, currentUser }) => { const [showHrData, setShowHrData] = useState(false); const [isEditing, setIsEditing] = useState(false); const [currentEmployee, setCurrentEmployee] = useState(employee); const [vacations, setVacations] = useState([]); const [sickLeaves, setSickLeaves] = useState([]); const [terminations, setTerminations] = useState([]); const [loading, setLoading] = useState(false); const [isWorkCalendarModalOpen, setIsWorkCalendarModalOpen] = useState(false); const [workCalendarModalType, setWorkCalendarModalType] = useState(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([]); 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 ( setIsEditing(false)} onSave={handleUpdate} /> ); } return (
e.stopPropagation()} > {/* Header */}
{currentEmployee.photoUrl ? ( {currentEmployee.name} { // Если фото не загрузилось, показываем инициалы 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); } }} /> ) : (
{currentEmployee.name.split(' ').map(n => n[0]).join('')}
)}

{currentEmployee.name}

{currentEmployee.position}

{getStatusLabel(currentEmployee.status)}
{/* Content */}
{/* Основная информация */}

Основная информация

ФИО {currentEmployee.name}
Дата рождения {formatDate(currentEmployee.birthDate)}
Должность {currentEmployee.position}
{assignedDistrictIds.length > 1 ? 'Участки' : 'Участок'} {districtNamesDisplay}
Дата регистрации {formatDate(currentEmployee.registrationDate)}
{/* Мессенджеры */} {currentEmployee.messengerLogins && currentEmployee.messengerLogins.length > 0 && (

Мессенджеры

{currentEmployee.messengerLogins.map((msg, idx) => { const Icon = getMessengerIcon(msg.messenger); return (
{msg.messenger} {msg.login}
); })}
)} {/* Фото */} {currentEmployee.photoUrl && (

Фото

{currentEmployee.name} { const target = e.target as HTMLImageElement; target.style.display = 'none'; }} />
)} {/* HR и Бухгалтерские данные */} {canViewHrData && currentEmployee.hrData && (

Данные для HR и Бухгалтерии

{showHrData && (
{/* Паспортные данные */} {currentEmployee.hrData.passportData && (
Паспортные данные
Серия и номер {currentEmployee.hrData.passportData.series} {currentEmployee.hrData.passportData.number}
Выдан {currentEmployee.hrData.passportData.issuedBy}
Дата выдачи {formatDate(currentEmployee.hrData.passportData.issuedDate)}
Адрес регистрации {currentEmployee.hrData.passportData.registrationAddress}
)} {/* Трудовая книжка */} {currentEmployee.hrData.laborBook && (
Трудовая книжка
Номер {currentEmployee.hrData.laborBook.series && `${currentEmployee.hrData.laborBook.series} `} {currentEmployee.hrData.laborBook.number}
{currentEmployee.hrData.laborBook.entries && currentEmployee.hrData.laborBook.entries.length > 0 && (
Записи
{currentEmployee.hrData.laborBook.entries.map((entry, idx) => (
{formatDate(entry.date)}
{entry.organization}
{entry.position}
))}
)}
)} {/* Заказ справок */} {currentEmployee.hrData.certificates && currentEmployee.hrData.certificates.length > 0 && (
Заказ справок
{currentEmployee.hrData.certificates.map((cert, idx) => (
{cert.type}
Запрошена: {formatDate(cert.requestedDate)} {cert.issuedDate && ` • Выдана: ${formatDate(cert.issuedDate)}`}
{getCertificateStatusLabel(cert.status)}
))}
)} {/* Прочие документы */} {currentEmployee.hrData.otherDocuments && currentEmployee.hrData.otherDocuments.length > 0 && (
Прочие документы
{currentEmployee.hrData.otherDocuments.map((doc, idx) => (
{doc.name}
{doc.type} • {formatDate(doc.date)}
{doc.fileUrl && ( )}
))}
)} {/* Бухгалтерская информация */} {currentEmployee.hrData.accountingData && (
Бухгалтерская информация
{currentEmployee.hrData.accountingData.inn && (
ИНН {currentEmployee.hrData.accountingData.inn}
)} {currentEmployee.hrData.accountingData.snils && (
СНИЛС {currentEmployee.hrData.accountingData.snils}
)} {currentEmployee.hrData.accountingData.bankName && (
Банк {currentEmployee.hrData.accountingData.bankName}
)} {currentEmployee.hrData.accountingData.bankAccount && (
Расчетный счет {currentEmployee.hrData.accountingData.bankAccount}
)} {currentEmployee.hrData.accountingData.correspondentAccount && (
Корреспондентский счет {currentEmployee.hrData.accountingData.correspondentAccount}
)} {currentEmployee.hrData.accountingData.bik && (
БИК {currentEmployee.hrData.accountingData.bik}
)} {currentEmployee.hrData.accountingData.taxId && (
КПП {currentEmployee.hrData.accountingData.taxId}
)}
)} {/* Характеристики договора */} {currentEmployee.hrData.contracts && currentEmployee.hrData.contracts.length > 0 && (
Характеристики договора
{currentEmployee.hrData.contracts.map((contract, idx) => (
Тип договора {contract.contractType}
{contract.contractNumber && (
Номер договора {contract.contractNumber}
)}
Дата начала {formatDate(contract.startDate)}
{contract.endDate && (
Дата окончания {formatDate(contract.endDate)}
)} {contract.probationPeriodDays && (
Испытательный срок {contract.probationPeriodDays} дней
)} {contract.workSchedule && (
График работы {contract.workSchedule}
)} {contract.workMode && (
Режим работы {contract.workMode}
)} {contract.contractTerms && (
Дополнительные условия {contract.contractTerms}
)}
))}
)}
)}
)} {/* Рабочий календарь */}

Рабочий календарь

{canViewHrData && ( )}
{/* Отпуска */}
Отпуска
{canViewHrData && ( )}
{vacations.length > 0 ? (
{vacations.map((vacation) => (
{formatDate(vacation.startDate)} - {formatDate(vacation.endDate)}
{vacation.daysCount} дней • {vacation.vacationType === 'annual' ? 'Ежегодный' : vacation.vacationType === 'unpaid' ? 'Без сохранения зарплаты' : vacation.vacationType} {vacation.approvedBy && ` • Утвержден: ${vacation.approvedBy}`}
{getVacationStatusLabel(vacation.status)}
))}
) : (
Нет записей об отпусках
)}
{/* Больничные */}
Больничные
{canViewHrData && ( )}
{sickLeaves.length > 0 ? (
{sickLeaves.map((sickLeave) => (
{formatDate(sickLeave.startDate)} {sickLeave.endDate && ` - ${formatDate(sickLeave.endDate)}`} {sickLeave.daysCount && ` (${sickLeave.daysCount} дней)`}
{sickLeave.sickLeaveNumber && `№ ${sickLeave.sickLeaveNumber} • `} {sickLeave.medicalInstitution && `${sickLeave.medicalInstitution} • `} {sickLeave.diagnosis && `Диагноз: ${sickLeave.diagnosis}`}
{getSickLeaveStatusLabel(sickLeave.status)}
{sickLeave.fileUrl && ( Скачать больничный лист )}
))}
) : (
Нет записей о больничных
)}
{/* Отгулы и прогуски */} {canViewHrData && (
Отгулы и пропуски
{canCreateAbsence() && (
)}
{currentEmployee.hrData?.absences && currentEmployee.hrData.absences.length > 0 ? (
{currentEmployee.hrData.absences.map((absence) => { const typeLabels: Record = { 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 (
{formatDate(absence.startDate)} {absence.endDate && absence.endDate !== absence.startDate && ( <> - {formatDate(absence.endDate)} )} {absence.startTime && ( <> в {absence.startTime} )}
{absence.daysCount} {absence.daysCount === 1 ? 'день' : absence.daysCount < 5 ? 'дня' : 'дней'} {absence.reason && ` • ${absence.reason}`}
{typeInfo.label} {absence.status === 'pending' ? 'На согласовании' : absence.status === 'approved' ? 'Утверждено' : absence.status === 'rejected' ? 'Отклонено' : 'Отменено'}
); })}
) : (
Нет записей
)}
)}
{/* Увольнения */} {canViewHrData && (

Увольнения

{currentEmployee.status !== 'inactive' && ( )}
{terminations.length > 0 ? (
{terminations.map((termination) => (
Дата увольнения: {formatDate(termination.terminationDate)}
Причина: {termination.reason} • Инициировано: {termination.initiatedBy}
{getTerminationStatusLabel(termination.status)}
{/* Договор на увольнение */} {termination.terminationContractNumber && (
Договор на увольнение
№ {termination.terminationContractNumber} {termination.terminationContractDate && ` от ${formatDate(termination.terminationContractDate)}`}
{termination.terminationContractFileUrl && ( Скачать договор )}
)} {/* Расчеты */} {(termination.finalSettlementAmount || termination.compensationAmount || termination.severancePay) && (
Расчеты
{termination.unusedVacationDays && (
Неиспользованные дни отпуска: {termination.unusedVacationDays}
)} {termination.compensationAmount && (
Компенсация за отпуск: {termination.compensationAmount.toLocaleString('ru-RU')} ₽
)} {termination.severancePay && (
Выходное пособие: {termination.severancePay.toLocaleString('ru-RU')} ₽
)} {termination.otherPayments && (
Прочие выплаты: {termination.otherPayments.toLocaleString('ru-RU')} ₽
)} {termination.deductions && (
Удержания: {termination.deductions.toLocaleString('ru-RU')} ₽
)} {termination.finalSettlementAmount && (
Итого к выплате: {termination.finalSettlementAmount.toLocaleString('ru-RU')} ₽
)}
{termination.settlementDocumentNumber && (
Документ расчета: № {termination.settlementDocumentNumber} {termination.settlementDocumentDate && ` от ${formatDate(termination.settlementDocumentDate)}`}
)} {termination.settlementDocumentFileUrl && ( Скачать документ расчета )}
)}
))}
) : (
Нет записей об увольнениях
)}
)} {/* Кнопки действий */}
Позвонить {currentEmployee.messengerLogins && currentEmployee.messengerLogins.length > 0 && ( )}
{/* Модальное окно рабочего календаря */} {isWorkCalendarModalOpen && ( { setIsWorkCalendarModalOpen(false); setWorkCalendarModalType(undefined); }} onSave={async () => { await fetchEmployeeData(); }} type={workCalendarModalType} currentUser={currentUser} /> )} {/* Модальное окно увольнения */} {isTerminationModalOpen && (
e.stopPropagation()} > {/* Header */}

Оформление увольнения

{currentEmployee.name}

{/* Form */}
{terminationError && (

{terminationError}

)} {/* Дата увольнения */}
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" />
{/* Причина увольнения */}