import React, { useState, useEffect } from 'react'; import { Users, ChevronDown, ChevronRight, User, Phone, Loader2, AlertCircle, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react'; import { apiClient } from '../../services/apiClient'; interface OrgEmployee { id: string; name: string; position: string; phone: string; status: 'active' | 'vacation' | 'inactive'; photoUrl?: string; managerId?: string; subordinates: OrgEmployee[]; subordinateCount?: number; } export const OrganizationalStructure: React.FC = () => { const [structure, setStructure] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [zoom, setZoom] = useState(100); const [selectedEmployee, setSelectedEmployee] = useState(null); useEffect(() => { loadStructure(); }, []); const loadStructure = async () => { try { setLoading(true); setError(null); const data = await apiClient.get('/employees/organizational-structure'); setStructure(data); // По умолчанию разворачиваем все узлы const allIds = new Set(); const collectIds = (employees: OrgEmployee[]) => { employees.forEach(emp => { allIds.add(emp.id); if (emp.subordinates.length > 0) { collectIds(emp.subordinates); } }); }; collectIds(data); setExpandedNodes(allIds); } catch (err: any) { console.error('Error loading organizational structure:', err); setError(err.message || 'Не удалось загрузить организационную структуру'); } finally { setLoading(false); } }; const toggleNode = (id: string) => { const newExpanded = new Set(expandedNodes); if (newExpanded.has(id)) { newExpanded.delete(id); } else { newExpanded.add(id); } setExpandedNodes(newExpanded); }; const expandAll = () => { const allIds = new Set(); const collectIds = (employees: OrgEmployee[]) => { employees.forEach(emp => { allIds.add(emp.id); if (emp.subordinates.length > 0) { collectIds(emp.subordinates); } }); }; collectIds(structure); setExpandedNodes(allIds); }; const collapseAll = () => { setExpandedNodes(new Set()); }; const renderEmployeeCard = (employee: OrgEmployee, level: number = 0) => { const isExpanded = expandedNodes.has(employee.id); const hasSubordinates = employee.subordinates.length > 0; const indent = level * 60; return (
{/* Вертикальная линия для связи с родителем */} {level > 0 && (
)} {/* Кнопка разворачивания */} {hasSubordinates && ( )} {!hasSubordinates &&
} {/* Карточка сотрудника */}
setSelectedEmployee(employee)} >
{/* Фото */}
{employee.photoUrl ? ( {employee.name} { (e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(employee.name)}&background=6366f1&color=fff&size=128`; }} /> ) : (
{employee.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
)}
{/* Информация */}

{employee.name}

{employee.position}

{employee.phone}
{hasSubordinates && (
{employee.subordinateCount || employee.subordinates.length} подчиненных
)}
{/* Статус */}
{employee.status === 'active' ? 'Активен' : employee.status === 'vacation' ? 'Отпуск' : 'Неактивен'}
{/* Горизонтальная линия для связи с подчиненными */} {hasSubordinates && isExpanded && level >= 0 && (
)} {/* Подчиненные */} {hasSubordinates && isExpanded && (
{employee.subordinates.map((sub, idx) => (
{renderEmployeeCard(sub, level + 1)}
))}
)}
); }; if (loading) { return (

Загрузка организационной структуры...

); } if (error) { return (

{error}

); } if (structure.length === 0) { return (

Организационная структура пуста

Добавьте сотрудников и укажите их руководителей

); } return (
{/* Панель управления */}

Организационная структура

({structure.reduce((acc, emp) => { const count = (e: OrgEmployee) => 1 + e.subordinates.reduce((sum, s) => sum + count(s), 0); return acc + count(emp); }, 0)} сотрудников)
{/* Управление масштабом */}
{zoom}%
{/* Управление разворачиванием */}
{/* Структура */}
{structure.map(employee => (
{renderEmployeeCard(employee, 0)}
))}
{/* Детали выбранного сотрудника */} {selectedEmployee && (

Информация о сотруднике

ФИО

{selectedEmployee.name}

Должность

{selectedEmployee.position}

Телефон

{selectedEmployee.phone}

Подчиненные

{selectedEmployee.subordinateCount || selectedEmployee.subordinates.length} человек

)}
); };