1252 lines
83 KiB
TypeScript
1252 lines
83 KiB
TypeScript
|
|
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
};
|