import React, { useState, useEffect, useRef, useCallback } from 'react'; import { MOCK_DISTRICTS } from '../../constants'; import { Employee, User, District } from '../../types'; import { Search, Filter, Phone, MessageCircle, MoreVertical, MapPin, Briefcase, Mail, User as UserIcon, Calendar, FileText, Eye, X, Plus } from 'lucide-react'; import { EmployeeCardModal } from './EmployeeCardModal'; import { backendApi, authFetch } from '../../services/apiClient'; import { readCache, saveCache } from '../../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; interface EmployeeRegistryProps { onEmployeeCreated?: () => void; refreshTrigger?: string; currentUser?: User; // Текущий пользователь для проверки прав доступа onOpenCreateModal?: () => void; // Функция для открытия модального окна создания сотрудника } export const EmployeeRegistry: React.FC = ({ onEmployeeCreated, refreshTrigger, currentUser, onOpenCreateModal }) => { const CACHE_EMP = 'mkd_hr_employee_registry'; const CACHE_DIST = 'mkd_hr_districts'; const cachedEmp = readCache(CACHE_EMP, []); const cachedDist = readCache(CACHE_DIST, []); const [search, setSearch] = useState(''); const [selectedEmployee, setSelectedEmployee] = useState(null); const [employees, setEmployees] = useState(cachedEmp); const [districts, setDistricts] = useState(cachedDist); const [loading, setLoading] = useState(cachedEmp.length === 0); // Фильтры const [showFilters, setShowFilters] = useState(false); const [statusFilter, setStatusFilter] = useState<('active' | 'vacation' | 'inactive')[]>(['active', 'vacation']); // По умолчанию скрываем неактивных const [districtFilter, setDistrictFilter] = useState(null); const filtersRef = useRef(null); useEffect(() => { fetchEmployees(); fetchDistricts(); }, [refreshTrigger]); useEffect(() => { const onRefresh = () => { fetchEmployees(); fetchDistricts(); }; window.addEventListener(REFRESH_EVENTS.employees, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.employees, onRefresh); }, []); useEffect(() => { const interval = setInterval(() => { fetchEmployees(false); fetchDistricts(); }, 10 * 1000); return () => clearInterval(interval); }, []); // Закрытие панели фильтров при клике вне её useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (filtersRef.current && !filtersRef.current.contains(event.target as Node)) { setShowFilters(false); } }; if (showFilters) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showFilters]); const fetchEmployees = async (showSpinner = true) => { try { if (showSpinner && cachedEmp.length === 0) setLoading(true); const response = await authFetch('/api/employees'); if (response.ok) { const data = await response.json(); setEmployees(data); saveCache(CACHE_EMP, data); } else setEmployees([]); } catch (error) { console.error('Error fetching employees:', error); setEmployees([]); } finally { setLoading(false); } }; const fetchDistricts = async () => { try { const data = await backendApi.getDistricts(); setDistricts(data); saveCache(CACHE_DIST, data); } catch (error) { console.warn('Failed to fetch districts from API, using MOCK_DISTRICTS:', error); setDistricts(MOCK_DISTRICTS); } }; const handleEmployeeUpdate = async (updatedEmployee: Employee) => { try { const response = await authFetch(`/api/employees/${updatedEmployee.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedEmployee) }); if (response.ok) { window.dispatchEvent(new CustomEvent('mkd-employees-changed')); setEmployees(employees.map(emp => emp.id === updatedEmployee.id ? updatedEmployee : emp )); setSelectedEmployee(updatedEmployee); } else { alert('Ошибка при обновлении сотрудника'); } } catch (error) { console.error('Error updating employee:', error); alert('Ошибка при обновлении сотрудника'); } }; const filtered = employees.filter(e => { // Поиск по имени и должности const matchesSearch = e.name.toLowerCase().includes(search.toLowerCase()) || e.position.toLowerCase().includes(search.toLowerCase()); // Фильтр по статусу const matchesStatus = statusFilter.includes(e.status); // Фильтр по участку const matchesDistrict = !districtFilter || e.assignedDistrictId === districtFilter; return matchesSearch && matchesStatus && matchesDistrict; }); const getDistrictName = (districtId: string) => { const district = districts.find(d => d.id === districtId) || MOCK_DISTRICTS.find(d => d.id === districtId); return district?.name || 'Не указан'; }; const toggleStatusFilter = (status: 'active' | 'vacation' | 'inactive') => { setStatusFilter(prev => prev.includes(status) ? prev.filter(s => s !== status) : [...prev, status] ); }; const getActiveFiltersCount = () => { let count = 0; if (statusFilter.length !== 3) count++; // Если не все статусы выбраны if (districtFilter) count++; return count; }; if (loading) { return (
Загрузка сотрудников...
); } return (
{selectedEmployee && ( setSelectedEmployee(null)} onUpdate={handleEmployeeUpdate} currentUser={currentUser} /> )}
setSearch(e.target.value)} className="w-full pl-11 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm" />
{onOpenCreateModal && ( )}
{showFilters && (

Фильтры

{/* Фильтр по статусу */}
{[ { value: 'active' as const, label: 'В строю', color: 'emerald' }, { value: 'vacation' as const, label: 'В отпуске', color: 'amber' }, { value: 'inactive' as const, label: 'Неактивен (уволен)', color: 'red' } ].map(status => ( ))}
{/* Фильтр по участку */}
{/* Кнопка сброса фильтров */} {(statusFilter.length !== 3 || districtFilter) && ( )}
)}
{/* Активные фильтры (чипсы) */} {(statusFilter.length !== 3 || districtFilter) && (
{statusFilter.length !== 3 && (
{statusFilter.length === 1 ? statusFilter[0] === 'active' ? 'В строю' : statusFilter[0] === 'vacation' ? 'В отпуске' : 'Неактивен' : `${statusFilter.length} статуса` }
)} {districtFilter && (
{getDistrictName(districtFilter)}
)}
)} {filtered.length === 0 ? (

Сотрудники не найдены

{search &&

Попробуйте изменить поисковый запрос

}
) : (
{filtered.map(emp => (
setSelectedEmployee(emp)} >
{emp.photoUrl ? ( {emp.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-14 h-14 rounded-2xl bg-slate-100 flex items-center justify-center text-xl font-black text-slate-400 group-hover:bg-primary-50 group-hover:text-primary-600 transition-colors'; fallback.textContent = emp.name.split(' ').map(n => n[0]).join(''); parent.appendChild(fallback); } }} /> ) : (
{emp.name.split(' ').map(n => n[0]).join('')}
)}

{emp.name}

{emp.position}

{emp.status === 'active' ? 'В строю' : emp.status === 'vacation' ? 'В отпуске' : 'Неактивен'}
{getDistrictName(emp.assignedDistrictId)}
Оклад: {emp.salary.toLocaleString()}₽
e.stopPropagation()} className="flex-1 py-2.5 bg-slate-50 text-slate-700 rounded-xl text-[10px] font-black uppercase flex items-center justify-center gap-2 hover:bg-slate-100 transition-all" > Позвонить {emp.messengerLogins && emp.messengerLogins.length > 0 && ( )}
))}
)}
); };