Темный режим

3.6. Управление командами

Модуль "Управление командами" в системе HRoom предоставляет инструменты для организации и управления структурой компании, отделами, командами и сотрудниками. Этот модуль является фундаментальной частью системы, так как он определяет иерархию и взаимосвязи между различными группами сотрудников, необходимые для корректной работы аналитических и опросных функций платформы.

Основные компоненты модуля

  • Управление отделами - создание и редактирование структурных подразделений компании
  • Управление сотрудниками - добавление, редактирование и архивирование данных о сотрудниках
  • Импорт данных - инструменты для массового импорта информации о сотрудниках из CSV
  • Фильтрация и поиск - инструменты для быстрого доступа к нужной информации
  • Структура организации - управление иерархией и взаимосвязями между отделами

Архитектура модуля управления командами

Модуль управления командами построен на основе следующей структуры файлов и компонентов:


src/pages/Teams/
├── components/             # Компоненты пользовательского интерфейса
│   ├── Employees/          # Компоненты для работы с сотрудниками
│   │   ├── EmployeeTable.tsx # Таблица сотрудников
│   │   ├── EmployeeForm.tsx  # Форма создания/редактирования сотрудника
│   │   └── BulkActions.tsx   # Массовые действия с сотрудниками
│   └── EmptyStates/        # Компоненты для отображения пустых состояний
│       ├── DepartmentsEmpty.tsx # Пустое состояние для отделов
│       └── EmployeesEmpty.tsx   # Пустое состояние для сотрудников
├── DepartmentForm.tsx      # Форма создания/редактирования отдела
├── DepartmentList.tsx      # Список отделов
├── EmployeeList.tsx        # Страница списка сотрудников
├── CSVImportModal.tsx      # Модальное окно импорта данных из CSV
└── index.tsx               # Главный компонент страницы Teams

src/components/teams/       # Общие компоненты для работы с командами
├── EmployeeFilter.tsx      # Компонент фильтрации сотрудников
└── Pagination.tsx          # Компонент пагинации для списков

src/services/teamsApi.ts    # API-сервис для работы с данными команд и сотрудников
src/services/teams/          # Дополнительные сервисы
│   ├── types.ts             # Типы данных
│   ├── utils.ts             # Утилиты
│   └── departmentService.ts # Сервис для работы с отделами
                

Управление отделами

Компонент DepartmentList предоставляет интерфейс для управления отделами компании. Он позволяет просматривать существующие отделы, создавать новые, редактировать и удалять их.

Функциональность управления отделами

  • Просмотр списка всех отделов компании
  • Создание новых отделов
  • Редактирование существующих отделов
  • Удаление отделов (с проверкой наличия связанных сотрудников)
  • Просмотр статистики по отделам (количество сотрудников, команд)
  • Навигация к списку сотрудников конкретного отдела

Компонент списка отделов

Компонент DepartmentList отображает список отделов в виде карточек с ключевой информацией и действиями:

// Пример компонента DepartmentList из src/pages/Teams/DepartmentList.tsx
export const DepartmentList = () => {
  const [departments, setDepartments] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [isCreateFormVisible, setIsCreateFormVisible] = useState(false);
  const [editingDepartment, setEditingDepartment] = useState(null);
  
  // Загрузка списка отделов
  useEffect(() => {
    const fetchDepartments = async () => {
      try {
        setIsLoading(true);
        const data = await departmentService.getDepartments();
        setDepartments(data);
        setError(null);
      } catch (err) {
        console.error('Error fetching departments:', err);
        setError('Произошла ошибка при загрузке списка отделов');
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchDepartments();
  }, []);
  
  // Обработчик создания нового отдела
  const handleCreateDepartment = async (departmentData) => {
    try {
      const newDepartment = await departmentService.createDepartment(departmentData);
      setDepartments(prev => [...prev, newDepartment]);
      setIsCreateFormVisible(false);
    } catch (err) {
      console.error('Error creating department:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик обновления отдела
  const handleUpdateDepartment = async (id, departmentData) => {
    try {
      const updatedDepartment = await departmentService.updateDepartment(id, departmentData);
      setDepartments(prev => 
        prev.map(dept => dept.id === id ? updatedDepartment : dept)
      );
      setEditingDepartment(null);
    } catch (err) {
      console.error('Error updating department:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик удаления отдела
  const handleDeleteDepartment = async (id) => {
    try {
      await departmentService.deleteDepartment(id);
      setDepartments(prev => prev.filter(dept => dept.id !== id));
    } catch (err) {
      console.error('Error deleting department:', err);
      // Обработка ошибки
    }
  };
  
  if (isLoading) {
    return <div className="loading-state">Загрузка отделов...</div>;
  }
  
  if (error) {
    return (
      <div className="error-state">
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>Повторить</button>
      </div>
    );
  }
  
  if (departments.length === 0 && !isCreateFormVisible) {
    return <DepartmentsEmpty onCreateClick={() => setIsCreateFormVisible(true)} />;
  }
  
  return (
    <div className="departments-container">
      <div className="departments-header">
        <h1>Отделы компании</h1>
        <button 
          className="create-department-button"
          onClick={() => setIsCreateFormVisible(true)}
        >
          Создать отдел
        </button>
      </div>
      
      {isCreateFormVisible && (
        <DepartmentForm 
          onSubmit={handleCreateDepartment}
          onCancel={() => setIsCreateFormVisible(false)}
        />
      )}
      
      {editingDepartment && (
        <DepartmentForm 
          department={editingDepartment}
          isEditing={true}
          onSubmit={(data) => handleUpdateDepartment(editingDepartment.id, data)}
          onCancel={() => setEditingDepartment(null)}
        />
      )}
      
      <div className="departments-grid">
        {departments.map(department => (
          <div key={department.id} className="department-card">
            <div className="department-header">
              <h3>{department.name}</h3>
              <div className="department-actions">
                <button 
                  className="edit-button"
                  onClick={() => setEditingDepartment(department)}
                >
                  Редактировать
                </button>
                <button 
                  className="delete-button"
                  onClick={() => handleDeleteDepartment(department.id)}
                >
                  Удалить
                </button>
              </div>
            </div>
            
            <div className="department-info">
              <p>{department.description || 'Нет описания'}</p>
            </div>
            
            <div className="department-stats">
              <div className="stat-item">
                <span className="stat-label">Сотрудников:</span>
                <span className="stat-value">{department.employeesCount || 0}</span>
              </div>
              <div className="stat-item">
                <span className="stat-label">Команд:</span>
                <span className="stat-value">{department.teamsCount || 0}</span>
              </div>
            </div>
            
            <div className="department-footer">
              <a 
                href={`/teams/employees?departmentId=${department.id}`} 
                className="view-employees-link"
              >
                Просмотреть сотрудников
              </a>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

Форма отдела

Компонент DepartmentForm предоставляет интерфейс для создания и редактирования отделов:

// Пример компонента DepartmentForm из src/pages/Teams/DepartmentForm.tsx
export const DepartmentForm = ({ 
  department, 
  isEditing = false, 
  onSubmit, 
  onCancel 
}) => {
  const [formData, setFormData] = useState({
    name: department?.name || '',
    description: department?.description || '',
    parentDepartmentId: department?.parentDepartmentId || null
  });
  
  const [departments, setDepartments] = useState([]);
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  
  // Загрузка списка отделов для выбора родительского отдела
  useEffect(() => {
    const fetchDepartments = async () => {
      try {
        setIsLoading(true);
        
        const data = await departmentService.getDepartments();
        // Фильтруем текущий отдел из списка возможных родителей
        const filteredDepartments = department 
          ? data.filter(dept => dept.id !== department.id) 
          : data;
        setDepartments(filteredDepartments);
      } catch (err) {
        console.error('Error fetching departments:', err);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchDepartments();
  }, [department]);
  
  // Обработчик изменения полей формы
  const handleChange = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Сброс ошибки для измененного поля
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };
  
  // Обработчик отправки формы
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Валидация данных
    const validationErrors = {};
    
    if (!formData.name.trim()) {
      validationErrors.name = 'Название отдела обязательно';
    }
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    // Вызов функции обработки отправки
    onSubmit(formData);
  };
  
  return (
    <div className="department-form-container">
      <h2>{isEditing ? 'Редактирование отдела' : 'Создание нового отдела'}</h2>
      
      <form onSubmit={handleSubmit} className="department-form">
        <div className="form-group">
          <label htmlFor="name">Название отдела *</label>
          <input 
            type="text"
            id="name"
            value={formData.name}
            onChange={(e) => handleChange('name', e.target.value)}
            className={errors.name ? 'error' : ''}
          />
          {errors.name && <div className="error-message">{errors.name}</div>}
        </div>
        
        <div className="form-group">
          <label htmlFor="description">Описание</label>
          <textarea 
            id="description"
            value={formData.description}
            onChange={(e) => handleChange('description', e.target.value)}
            rows={4}
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="parentDepartmentId">Родительский отдел</label>
          <select 
            id="parentDepartmentId"
            value={formData.parentDepartmentId || ''}
            onChange={(e) => handleChange('parentDepartmentId', e.target.value || null)}
          >
            <option value="">Нет (корневой отдел)</option>
            {departments.map(dept => (
              <option key={dept.id} value={dept.id}>
                {dept.name}
              </option>
            ))}
          </select>
        </div>
        
        <div className="form-actions">
          <button 
            type="button" 
            className="cancel-button"
            onClick={onCancel}
          >
            Отмена
          </button>
          
          <button 
            type="submit" 
            className="submit-button"
          >
            {isEditing ? 'Сохранить изменения' : 'Создать отдел'}
          </button>
        </div>
      </form>
    </div>
  );
};

Управление сотрудниками

Компонент EmployeeList предоставляет интерфейс для управления сотрудниками. Он позволяет просматривать список сотрудников, добавлять новых, редактировать и архивировать существующих, а также выполнять массовые операции.

Функциональность управления сотрудниками

  • Просмотр списка сотрудников с возможностью фильтрации и сортировки
  • Добавление новых сотрудников через форму
  • Редактирование данных существующих сотрудников
  • Архивирование/деактивация сотрудников
  • Массовый импорт сотрудников из CSV-файла
  • Массовые операции с выбранными сотрудниками
  • Поиск сотрудников по различным параметрам

Компонент списка сотрудников

Компонент EmployeeList отображает список сотрудников и предоставляет инструменты для управления ими:

// Пример компонента EmployeeList из src/pages/Teams/EmployeeList.tsx
import React, { useState, useEffect } from 'react';
import { Employee } from '../../types';
import { EmployeeFilter } from '../components/EmployeeFilter';
import { BulkActions } from '../components/Employees/BulkActions';
import { EmployeeTable } from '../components/Employees/EmployeeTable';
import { Pagination } from '../components/Pagination';
import { CSVImportModal } from '../components/CSVImportModal';

export const EmployeeList = () => {
  const [employees, setEmployees] = useState([]);
  const [filteredEmployees, setFilteredEmployees] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [filters, setFilters] = useState({
    departmentId: '',
    status: 'active',
    search: ''
  });
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);
  const [isFormVisible, setIsFormVisible] = useState(false);
  const [editingEmployee, setEditingEmployee] = useState(null);
  const [selectedEmployees, setSelectedEmployees] = useState([]);
  const [isImportModalOpen, setIsImportModalOpen] = useState(false);
  
  const pageSize = 10; // Количество сотрудников на странице
  
  // Загрузка списка сотрудников с учетом фильтров и пагинации
  useEffect(() => {
    const fetchEmployees = async () => {
      try {
        setIsLoading(true);
        
        const queryParams = {
          page,
          pageSize,
          ...filters
        };
        
        const response = await teamsApi.getEmployees(queryParams);
        
        setEmployees(response.employees);
        setFilteredEmployees(response.employees);
        setTotalPages(Math.ceil(response.total / pageSize));
        setError(null);
      } catch (err) {
        console.error('Error fetching employees:', err);
        setError('Произошла ошибка при загрузке списка сотрудников');
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchEmployees();
  }, [page, filters]);
  
  // Обработчик изменения фильтров
  const handleFilterChange = (filterName, value) => {
    setFilters(prev => ({
      ...prev,
      [filterName]: value
    }));
    setPage(1); // Сброс на первую страницу при изменении фильтров
  };
  
  // Обработчик создания нового сотрудника
  const handleCreateEmployee = async (employeeData) => {
    try {
      const newEmployee = await teamsApi.createEmployee(employeeData);
      
      // Обновление списка сотрудников только если новый сотрудник соответствует текущим фильтрам
      if (
        (!filters.departmentId || employeeData.departmentId === filters.departmentId) &&
        (!filters.status || employeeData.status === filters.status)
      ) {
        setEmployees(prev => [...prev, newEmployee]);
        setFilteredEmployees(prev => [...prev, newEmployee]);
      }
      
      setIsFormVisible(false);
    } catch (err) {
      console.error('Error creating employee:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик обновления сотрудника
  const handleUpdateEmployee = async (id, employeeData) => {
    try {
      const updatedEmployee = await teamsApi.updateEmployee(id, employeeData);
      
      // Обновление списка сотрудников
      setEmployees(prev => 
        prev.map(emp => emp.id === id ? updatedEmployee : emp)
      );
      setFilteredEmployees(prev => 
        prev.map(emp => emp.id === id ? updatedEmployee : emp)
      );
      
      setEditingEmployee(null);
    } catch (err) {
      console.error('Error updating employee:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик изменения статуса сотрудника (активный/архивный)
  const handleStatusChange = async (id, newStatus) => {
    try {
      await teamsApi.updateEmployeeStatus(id, newStatus);
      
      // Обновление списка сотрудников
      if (filters.status === 'all' || filters.status === newStatus) {
        setEmployees(prev => 
          prev.map(emp => emp.id === id ? { ...emp, status: newStatus } : emp)
        );
        setFilteredEmployees(prev => 
          prev.map(emp => emp.id === id ? { ...emp, status: newStatus } : emp)
        );
      } else {
        // Если текущий фильтр не включает новый статус, удаляем сотрудника из списка
        setEmployees(prev => prev.filter(emp => emp.id !== id));
        setFilteredEmployees(prev => prev.filter(emp => emp.id !== id));
      }
    } catch (err) {
      console.error('Error updating employee status:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик массового импорта сотрудников
  const handleBulkImport = async (importedEmployees) => {
    try {
      const results = await teamsApi.bulkImportEmployees(importedEmployees);
      
      // Обновление списка сотрудников, если они соответствуют текущим фильтрам
      if (results.success) {
        // Перезагрузка списка сотрудников для обновления данных
        const queryParams = {
          page,
          pageSize,
          ...filters
        };
        
        const response = await teamsApi.getEmployees(queryParams);
        
        setEmployees(response.employees);
        setFilteredEmployees(response.employees);
        setTotalPages(Math.ceil(response.total / pageSize));
      }
      
      setIsImportModalOpen(false);
    } catch (err) {
      console.error('Error importing employees:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик выбора/отмены выбора сотрудника
  const handleEmployeeSelection = (employeeId) => {
    setSelectedEmployees(prev => {
      if (prev.includes(employeeId)) {
        return prev.filter(id => id !== employeeId);
      } else {
        return [...prev, employeeId];
      }
    });
  };
  
  // Обработчик выбора/отмены выбора всех сотрудников на странице
  const handleSelectAllEmployees = () => {
    if (selectedEmployees.length === filteredEmployees.length) {
      // Если все выбраны, отменяем выбор
      setSelectedEmployees([]);
    } else {
      // Иначе выбираем всех
      setSelectedEmployees(filteredEmployees.map(emp => emp.id));
    }
  };
  
  // Обработчик массовой операции с выбранными сотрудниками
  const handleBulkAction = async (action) => {
    if (selectedEmployees.length === 0) return;
    
    try {
      switch (action) {
        case 'archive':
          await teamsApi.bulkUpdateEmployeeStatus(selectedEmployees, 'archived');
          break;
        case 'activate':
          await teamsApi.bulkUpdateEmployeeStatus(selectedEmployees, 'active');
          break;
        case 'changeDepartment':
          // Логика изменения отдела для выбранных сотрудников
          break;
        default:
          return;
      }
      
      // Перезагрузка списка сотрудников для обновления данных
      const queryParams = {
        page,
        pageSize,
        ...filters
      };
      
      const response = await teamsApi.getEmployees(queryParams);
      
      setEmployees(response.employees);
      setFilteredEmployees(response.employees);
      
      // Сброс выбранных сотрудников
      setSelectedEmployees([]);
    } catch (err) {
      console.error('Error performing bulk action:', err);
      // Обработка ошибки
    }
  };
  
  if (isLoading && employees.length === 0) {
    return <div className="loading-state">Загрузка сотрудников...</div>;
  }
  
  if (error) {
    return (
      <div className="error-state">
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>Повторить</button>
      </div>
    );
  }
  
  if (employees.length === 0 && !isFormVisible && filters.departmentId === '' && filters.status === 'active' && filters.search === '') {
    return <EmployeesEmpty onCreateClick={() => setIsFormVisible(true)} onImportClick={() => setIsImportModalOpen(true)} />;
  }
  
  return (
    <div className="employees-container">
      <div className="employees-header">
        <h1>Сотрудники</h1>
        
        <div className="header-actions">
          <button 
            className="create-employee-button"
            onClick={() => setIsFormVisible(true)}
          >
            Добавить сотрудника
          </button>
          
          <button 
            className="import-button"
            onClick={() => setIsImportModalOpen(true)}
          >
            Импорт из CSV
          </button>
        </div>
      </div>
      
      <EmployeeFilter 
        filters={filters}
        onFilterChange={handleFilterChange}
      />
      
      {selectedEmployees.length > 0 && (
        <BulkActions 
          selectedCount={selectedEmployees.length}
          onBulkAction={handleBulkAction}
        />
      )}
      
      <EmployeeTable 
        employees={filteredEmployees}
        selectedEmployees={selectedEmployees}
        onSelectEmployee={handleEmployeeSelection}
        onSelectAll={handleSelectAllEmployees}
        onEdit={setEditingEmployee}
        onStatusChange={handleStatusChange}
      />
      
      {totalPages > 1 && (
        <Pagination 
          currentPage={page}
          totalPages={totalPages}
          onPageChange={setPage}
        />
      )}
      
      {isFormVisible && (
        <EmployeeForm 
          onSubmit={handleCreateEmployee}
          onCancel={() => setIsFormVisible(false)}
        />
      )}
      
      {editingEmployee && (
        <EmployeeForm 
          employee={editingEmployee}
          isEditing={true}
          onSubmit={(data) => handleUpdateEmployee(editingEmployee.id, data)}
          onCancel={() => setEditingEmployee(null)}
        />
      )}
      
      {isImportModalOpen && (
        <CSVImportModal 
          onImport={handleBulkImport}
          onClose={() => setIsImportModalOpen(false)}
        />
      )}
    </div>
  );
};

Таблица сотрудников

Компонент EmployeeTable отображает данные сотрудников в табличном виде с возможностью выбора и выполнения действий:

// Пример компонента EmployeeTable из src/pages/Teams/components/Employees/EmployeeTable.tsx
import React from 'react';
import { Employee } from '../../types';

export const EmployeeTable = ({ 
  employees, 
  selectedEmployees, 
  onSelectEmployee, 
  onSelectAll, 
  onEdit, 
  onStatusChange 
}) => {
  // Проверка, выбраны ли все сотрудники
  const allSelected = employees.length > 0 && selectedEmployees.length === employees.length;
  
  // Форматирование даты
  const formatDate = (dateString) => {
    if (!dateString) return 'Н/Д';
    
    const date = new Date(dateString);
    return new Intl.DateTimeFormat('ru-RU').format(date);
  };
  
  // Получение статуса сотрудника в читаемом виде
  const getStatusLabel = (status) => {
    switch (status) {
      case 'active': return 'Активный';
      case 'archived': return 'Архивный';
      default: return status;
    }
  };
  
  return (
    <div className="employee-table-container">
      {employees.length === 0 ? (
        <div className="no-employees-found">
          <p>Сотрудники не найдены. Попробуйте изменить параметры фильтрации.</p>
        </div>
      ) : (
        <table className="employee-table">
          <thead>
            <tr>
              <th className="checkbox-column">
                <input 
                  type="checkbox" 
                  checked={allSelected}
                  onChange={onSelectAll}
                />
              </th>
              <th>ФИО</th>
              <th>Email</th>
              <th>Должность</th>
              <th>Отдел</th>
              <th>Статус</th>
              <th>Дата найма</th>
              <th className="actions-column">Действия</th>
            </tr>
          </thead>
          <tbody>
            {employees.map(employee => (
              <tr key={employee.id}>
                <td>
                  <input 
                    type="checkbox"
                    checked={selectedEmployees.includes(employee.id)}
                    onChange={() => onSelectEmployee(employee.id)}
                  />
                </td>
                <td>
                  <div className="employee-name">
                    {employee.firstName} {employee.lastName}
                  </div>
                </td>
                <td>{employee.email}</td>
                <td>{employee.position || 'Не указана'}</td>
                <td>{employee.departmentName || 'Не указан'}</td>
                <td>
                  <span className={`status-badge ${employee.status}`}>
                    {getStatusLabel(employee.status)}
                  </span>
                </td>
                <td>{formatDate(employee.hireDate)}</td>
                <td>
                  <div className="employee-actions">
                    <button 
                      className="edit-button"
                      onClick={() => onEdit(employee)}
                    >
                      Редактировать
                    </button>
                    
                    {employee.status === 'active' ? (
                      <button 
                        className="archive-button"
                        onClick={() => onStatusChange(employee.id, 'archived')}
                      >
                        Архивировать
                      </button>
                    ) : (
                      <button 
                        className="activate-button"
                        onClick={() => onStatusChange(employee.id, 'active')}
                      >
                        Активировать
                      </button>
                    )}
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
};

Форма сотрудника

Компонент EmployeeForm предоставляет интерфейс для создания и редактирования данных сотрудника:

// Пример компонента EmployeeForm из src/pages/Teams/components/Employees/EmployeeForm.tsx
export const EmployeeForm = ({ 
  employee, 
  isEditing = false, 
  onSubmit, 
  onCancel 
}) => {
  const [formData, setFormData] = useState({
    firstName: employee?.firstName || '',
    lastName: employee?.lastName || '',
    email: employee?.email || '',
    position: employee?.position || '',
    departmentId: employee?.departmentId || '',
    hireDate: employee?.hireDate || '',
    phone: employee?.phone || '',
    status: employee?.status || 'active'
  });
  
  const [departments, setDepartments] = useState([]);
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  
  // Загрузка списка отделов для выбора
  useEffect(() => {
    const fetchDepartments = async () => {
      try {
        setIsLoading(true);
        const data = await departmentService.getDepartments();
        setDepartments(data);
      } catch (err) {
        console.error('Error fetching departments:', err);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchDepartments();
  }, []);
  
  // Обработчик изменения полей формы
  const handleChange = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Сброс ошибки для измененного поля
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };
  
  // Валидация email
  const validateEmail = (email) => {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return re.test(String(email).toLowerCase());
  };
  
  // Обработчик отправки формы
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Валидация данных
    const validationErrors = {};
    
    if (!formData.firstName.trim()) {
      validationErrors.firstName = 'Имя обязательно';
    }
    
    if (!formData.lastName.trim()) {
      validationErrors.lastName = 'Фамилия обязательна';
    }
    
    if (!formData.email.trim()) {
      validationErrors.email = 'Email обязателен';
    } else if (!validateEmail(formData.email)) {
      validationErrors.email = 'Некорректный формат email';
    }
    
    if (!formData.departmentId) {
      validationErrors.departmentId = 'Отдел обязателен';
    }
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    // Вызов функции обработки отправки
    onSubmit(formData);
  };
  
  if (isLoading && departments.length === 0) {
    return <div className="loading-state">Загрузка данных...</div>;
  }
  
  return (
    <div className="employee-form-container">
      <div className="form-header">
        <h2>{isEditing ? 'Редактирование сотрудника' : 'Добавление нового сотрудника'}</h2>
        <button className="close-button" onClick={onCancel}>×</button>
      </div>
      
      <form onSubmit={handleSubmit} className="employee-form">
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="firstName">Имя *</label>
            <input 
              type="text"
              id="firstName"
              value={formData.firstName}
              onChange={(e) => handleChange('firstName', e.target.value)}
              className={errors.firstName ? 'error' : ''}
            />
            {errors.firstName && <div className="error-message">{errors.firstName}</div>}
          </div>
          
          <div className="form-group">
            <label htmlFor="lastName">Фамилия *</label>
            <input 
              type="text"
              id="lastName"
              value={formData.lastName}
              onChange={(e) => handleChange('lastName', e.target.value)}
              className={errors.lastName ? 'error' : ''}
            />
            {errors.lastName && <div className="error-message">{errors.lastName}</div>}
          </div>
        </div>
        
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="email">Email *</label>
            <input 
              type="email"
              id="email"
              value={formData.email}
              onChange={(e) => handleChange('email', e.target.value)}
              className={errors.email ? 'error' : ''}
            />
            {errors.email && <div className="error-message">{errors.email}</div>}
          </div>
          
          <div className="form-group">
            <label htmlFor="phone">Телефон</label>
            <input 
              type="tel"
              id="phone"
              value={formData.phone}
              onChange={(e) => handleChange('phone', e.target.value)}
            />
          </div>
        </div>
        
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="position">Должность</label>
            <input 
              type="text"
              id="position"
              value={formData.position}
              onChange={(e) => handleChange('position', e.target.value)}
            />
          </div>
          
          <div className="form-group">
            <label htmlFor="departmentId">Отдел *</label>
            <select 
              id="departmentId"
              value={formData.departmentId}
              onChange={(e) => handleChange('departmentId', e.target.value)}
              className={errors.departmentId ? 'error' : ''}
            >
              <option value="">Выберите отдел</option>
              {departments.map(dept => (
                <option key={dept.id} value={dept.id}>
                  {dept.name}
                </option>
              ))}
            </select>
            {errors.departmentId && <div className="error-message">{errors.departmentId}</div>}
          </div>
        </div>
        
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="hireDate">Дата найма</label>
            <input 
              type="date"
              id="hireDate"
              value={formData.hireDate}
              onChange={(e) => handleChange('hireDate', e.target.value)}
            />
          </div>
          
          <div className="form-group">
            <label htmlFor="status">Статус</label>
            <select 
              id="status"
              value={formData.status}
              onChange={(e) => handleChange('status', e.target.value)}
            >
              <option value="active">Активный</option>
              <option value="archived">Архивный</option>
            </select>
          </div>
        </div>
        
        <div className="form-actions">
          <button 
            type="button" 
            className="cancel-button"
            onClick={onCancel}
          >
            Отмена
          </button>
          
          <button 
            type="submit" 
            className="submit-button"
          >
            {isEditing ? 'Сохранить изменения' : 'Добавить сотрудника'}
          </button>
        </div>
      </form>
    </div>
  );
};

Массовый импорт данных

Компонент CSVImportModal предоставляет интерфейс для массового импорта сотрудников из CSV-файла:

// Пример компонента CSVImportModal из src/pages/Teams/CSVImportModal.tsx
export const CSVImportModal = ({ onImport, onClose }) => {
  const [file, setFile] = useState(null);
  const [previewData, setPreviewData] = useState(null);
  const [mappings, setMappings] = useState({});
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  
  // Обязательные поля для импорта
  const requiredFields = ['firstName', 'lastName', 'email', 'departmentId'];
  
  // Доступные поля для маппинга
  const availableFields = [
    { id: 'firstName', name: 'Имя' },
    { id: 'lastName', name: 'Фамилия' },
    { id: 'email', name: 'Email' },
    { id: 'departmentId', name: 'ID отдела' },
    { id: 'position', name: 'Должность' },
    { id: 'phone', name: 'Телефон' },
    { id: 'hireDate', name: 'Дата найма' }
  ];
  
  // Обработчик выбора файла
  const handleFileChange = (e) => {
    const selectedFile = e.target.files[0];
    setFile(selectedFile);
    
    if (selectedFile) {
      const reader = new FileReader();
      
      reader.onload = (event) => {
        try {
          // Парсинг CSV-файла
          const csvData = parseCSVData(event.target.result);
          
          if (csvData.length === 0) {
            setErrors({ file: 'Файл не содержит данных' });
            return;
          }
          
          // Получение заголовков из первой строки
          const headers = csvData[0];
          
          // Автоматическое сопоставление заголовков с полями
          const initialMappings = {};
          headers.forEach((header, index) => {
            const matchedField = availableFields.find(field => 
              field.name.toLowerCase() === header.toLowerCase() ||
              field.id.toLowerCase() === header.toLowerCase()
            );
            
            if (matchedField) {
              initialMappings[index] = matchedField.id;
            }
          });
          
          setMappings(initialMappings);
          setPreviewData(csvData);
        } catch (err) {
          setErrors({ file: 'Ошибка при чтении файла: ' + err.message });
        }
      };
      
      reader.readAsText(selectedFile);
    }
  };
  
  // Функция для парсинга CSV-данных
  const parseCSVData = (csvText) => {
    const lines = csvText.split('\n');
    return lines.map(line => {
      // Простой парсер CSV, учитывающий запятые внутри кавычек
      const result = [];
      let inQuotes = false;
      let currentValue = '';
      
      for (let i = 0; i < line.length; i++) {
        const char = line[i];
        
        if (char === '"') {
          inQuotes = !inQuotes;
        } else if (char === ',' && !inQuotes) {
          result.push(currentValue.trim());
          currentValue = '';
        } else {
          currentValue += char;
        }
      }
      
      result.push(currentValue.trim());
      return result;
    }).filter(line => line.some(cell => cell.length > 0));
  };
  
  // Обработчик изменения маппинга
  const handleMappingChange = (columnIndex, fieldId) => {
    setMappings(prev => ({
      ...prev,
      [columnIndex]: fieldId
    }));
  };
  
  // Валидация данных перед импортом
  const validateMappings = () => {
    const validationErrors = {};
    
    // Проверяем, что все обязательные поля замаплены
    requiredFields.forEach(field => {
      if (!Object.values(mappings).includes(field)) {
        validationErrors[field] = `Поле "${availableFields.find(f => f.id === field).name}" обязательно`;
      }
    });
    
    return validationErrors;
  };
  
  // Обработчик импорта данных
  const handleImport = async () => {
    // Валидация маппинга
    const validationErrors = validateMappings();
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    try {
      setIsLoading(true);
      
      // Преобразование CSV-данных в формат для импорта
      const employees = [];
      
      // Пропускаем заголовки (первую строку)
      for (let i = 1; i < previewData.length; i++) {
        const row = previewData[i];
        const employee = {};
        
        // Маппинг данных из CSV в объект сотрудника
        Object.entries(mappings).forEach(([columnIndex, fieldId]) => {
          if (row[columnIndex] !== undefined) {
            employee[fieldId] = row[columnIndex];
          }
        });
        
        employees.push(employee);
      }
      
      // Вызываем функцию импорта
      await onImport(employees);
    } catch (err) {
      console.error('Error importing employees:', err);
      setErrors({ import: 'Ошибка при импорте сотрудников: ' + err.message });
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <div className="modal-overlay">
      <div className="import-modal">
        <div className="modal-header">
          <h2>Импорт сотрудников из CSV</h2>
          <button className="close-button" onClick={onClose}>×</button>
        </div>
        
        <div className="modal-body">
          {!previewData ? (
            <div className="file-upload-section">
              <p>Выберите CSV-файл со списком сотрудников для импорта.</p>
              <input 
                type="file" 
                accept=".csv" 
                onChange={handleFileChange}
              />
              {errors.file && <div className="error-message">{errors.file}</div>}
              
              <div className="file-format-info">
                <h4>Формат файла</h4>
                <p>
                  CSV-файл должен содержать заголовки в первой строке и данные сотрудников в последующих строках.
                  Обязательные поля: Имя, Фамилия, Email, ID отдела.
                </p>
                <p>Пример CSV-файла:</p>
                <pre>
                  Имя,Фамилия,Email,ID отдела,Должность,Телефон,Дата найма
                  Иван,Иванов,ivan@example.com,dept-1,Инженер,+7 900 123-45-67,2023-01-15
                  Мария,Петрова,maria@example.com,dept-2,Дизайнер,+7 900 765-43-21,2023-02-01
                </pre>
              </div>
            </div>
          ) : (
            <div className="import-mapping-section">
              <h3>Предварительный просмотр и настройка импорта</h3>
              
              <div className="field-mapping">
                <h4>Сопоставление полей</h4>
                <p>
                  Сопоставьте столбцы CSV-файла с полями системы.
                  Обязательные поля отмечены звездочкой (*).
                </p>
                
                <table className="mapping-table">
                  <thead>
                    <tr>
                      <th>Столбец в файле</th>
                      <th>Поле в системе</th>
                    </tr>
                  </thead>
                  <tbody>
                    {previewData[0].map((header, index) => (
                      <tr key={index}>
                        <td>{header}</td>
                        <td>
                          <select 
                            value={mappings[index] || ''}
                            onChange={(e) => handleMappingChange(index, e.target.value)}
                            className={errors[mappings[index]] ? 'error' : ''}
                          >
                            <option value="">Не импортировать</option>
                            {availableFields.map(field => (
                              <option key={field.id} value={field.id}>
                                {field.name} {requiredFields.includes(field.id) ? '*' : ''}
                              </option>
                            ))}
                          </select>
                          {mappings[index] && errors[mappings[index]] && (
                            <div className="error-message">{errors[mappings[index]]}</div>
                          )}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              
              <div className="data-preview">
                <h4>Предварительный просмотр данных</h4>
                <p>Показаны первые 5 строк из файла (без учета заголовков).</p>
                
                <table className="preview-table">
                  <thead>
                    <tr>
                      {previewData[0].map((header, index) => (
                        <th key={index}>{header}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody>
                    {previewData.slice(1, 6).map((row, rowIndex) => (
                      <tr key={rowIndex}>
                        {row.map((cell, cellIndex) => (
                          <td key={cellIndex}>{cell}</td>
                        ))}
                      </tr>
                    ))}
                  </tbody>
                </table>
                
                <p className="total-rows">
                  Всего строк для импорта: {previewData.length - 1}
                </p>
              </div>
              
              <button 
                className="import-button"
                onClick={handleImport}
                disabled={isLoading}
              >
                {isLoading ? 'Импорт...' : 'Импортировать'}
              </button>
            </div>
          )}
        </div>
        
        <div className="modal-footer">
          <button 
            className="cancel-button"
            onClick={onClose}
            disabled={isLoading}
          >
            Отмена
          </button>
          
          <button 
            className="confirm-button"
            onClick={handleImport}
            disabled={isLoading}
          >
            Подтвердить
          </button>
        </div>
      </div>
    </div>
  );
};

Фильтрация сотрудников

Компонент EmployeeFilter предоставляет интерфейс для фильтрации списка сотрудников по различным параметрам:

// Пример компонента EmployeeFilter из src/components/teams/EmployeeFilter.tsx
export const EmployeeFilter = ({ filters, onFilterChange }) => {
  const [departments, setDepartments] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  
  // Загрузка списка отделов для фильтрации
  useEffect(() => {
    if (isLoading) {
      const fetchDepartments = async () => {
        try {
          setIsLoading(true);
          const data = await departmentService.getDepartments();
          setDepartments(data);
        } catch (err) {
          console.error('Error fetching departments:', err);
        } finally {
          setIsLoading(false);
        }
      };
      
      fetchDepartments();
    }
  }, []);
  
  return (
    <div className="employee-filters">
      <div className="filter-row">
        <div className="filter-group">
          <label htmlFor="search">Поиск:</label>
          <input 
            type="text"
            id="search"
            placeholder="Имя, фамилия, email..."
            value={filters.search}
            onChange={(e) => onFilterChange('search', e.target.value)}
          />
        </div>
        
        <div className="filter-group">
          <label htmlFor="departmentId">Отдел:</label>
          <select 
            id="departmentId"
            value={filters.departmentId}
            onChange={(e) => onFilterChange('departmentId', e.target.value)}
          >
            <option value="">Все отделы</option>
            {departments.map(dept => (
              <option key={dept.id} value={dept.id}>
                {dept.name}
              </option>
            ))}
          </select>
        </div>
        
        <div className="filter-group">
          <label htmlFor="status">Статус:</label>
          <select 
            id="status"
            value={filters.status}
            onChange={(e) => onFilterChange('status', e.target.value)}
          >
            <option value="active">Активные</option>
            <option value="archived">Архивные</option>
            <option value="all">Все</option>
          </select>
        </div>
        
        <button 
          className="reset-filters-button"
          onClick={() => {
            onFilterChange('departmentId', '');
            onFilterChange('status', 'active');
            onFilterChange('search', '');
          }}
        >
          Сбросить фильтры
        </button>
      </div>
    </div>
  );
};

Массовые действия с сотрудниками

Компонент BulkActions предоставляет интерфейс для выполнения массовых операций с выбранными сотрудниками:

// Пример компонента BulkActions из src/pages/Teams/components/Employees/BulkActions.tsx
export const BulkActions = ({ selectedCount, onBulkAction }) => {
  const [isDepartmentModalOpen, setIsDepartmentModalOpen] = useState(false);
  const [selectedDepartmentId, setSelectedDepartmentId] = useState('');
  const [departments, setDepartments] = useState([]);
  
  // Загрузка списка отделов для выбора при массовом изменении отдела
  useEffect(() => {
    if (isDepartmentModalOpen) {
      const fetchDepartments = async () => {
        try {
          const data = await departmentService.getDepartments();
          setDepartments(data);
        } catch (err) {
          console.error('Error fetching departments:', err);
        }
      };
      
      fetchDepartments();
    }
  }, [isDepartmentModalOpen]);
  
  // Обработчик подтверждения изменения отдела
  const handleConfirmDepartmentChange = () => {
    if (selectedDepartmentId) {
      onBulkAction('changeDepartment', { departmentId: selectedDepartmentId });
      setIsDepartmentModalOpen(false);
      setSelectedDepartmentId('');
    }
  };
  
  return (
    <div className="bulk-actions-panel">
      <div className="selected-count">
        Выбрано сотрудников: {selectedCount}
      </div>
      
      <div className="bulk-actions">
        <button 
          className="action-button"
          onClick={() => onBulkAction('archive')}
        >
          Архивировать выбранных
        </button>
        
        <button 
          className="action-button"
          onClick={() => onBulkAction('activate')}
        >
          Активировать выбранных
        </button>
        
        <button 
          className="action-button"
          onClick={() => setIsDepartmentModalOpen(true)}
        >
          Изменить отдел
        </button>
      </div>
      
      {isDepartmentModalOpen && (
        <div className="modal-overlay">
          <div className="change-department-modal">
            <div className="modal-header">
              <h3>Изменение отдела для выбранных сотрудников</h3>
              <button 
                className="close-button"
                onClick={() => setIsDepartmentModalOpen(false)}
              >
                ×
              </button>
            </div>
            
            <div className="modal-body">
              <p>
                Выберите новый отдел для {selectedCount} выбранных сотрудников:
              </p>
              
              <select 
                value={selectedDepartmentId}
                onChange={(e) => setSelectedDepartmentId(e.target.value)}
              >
                <option value="">Выберите отдел</option>
                {departments.map(dept => (
                  <option key={dept.id} value={dept.id}>
                    {dept.name}
                  </option>
                ))}
              </select>
            </div>
            
            <div className="modal-footer">
              <button 
                className="cancel-button"
                onClick={() => setIsDepartmentModalOpen(false)}
              >
                Отмена
              </button>
              
              <button 
                className="confirm-button"
                onClick={handleConfirmDepartmentChange}
                disabled={!selectedDepartmentId}
              >
                Подтвердить
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

Пустые состояния

Модуль управления командами включает компоненты для отображения пустых состояний при отсутствии данных:

Пустое состояние для отделов

// Пример компонента DepartmentsEmpty из src/pages/Teams/components/EmptyStates/DepartmentsEmpty.tsx
export const DepartmentsEmpty = ({ onCreateClick }) => {
  return (
    <div className="empty-state-container">
      <div className="empty-state-icon">
        {/* Иконка или иллюстрация */}
      </div>
      
      <h2>У вас пока нет отделов</h2>
      
      <p>
        Отделы позволяют организовать структуру вашей компании и группировать сотрудников.
        Создайте первый отдел, чтобы начать работу с системой.
      </p>
      
      <button 
        className="create-department-button"
        onClick={onCreateClick}
      >
        Создать первый отдел
      </button>
    </div>
  );
};

Пустое состояние для сотрудников

// Пример компонента EmployeesEmpty из src/pages/Teams/components/EmptyStates/EmployeesEmpty.tsx
export const EmployeesEmpty = ({ onCreateClick, onImportClick }) => {
  return (
    <div className="empty-state-container">
      <div className="empty-state-icon">
        {/* Иконка или иллюстрация */}
      </div>
      
      <h2>У вас пока нет сотрудников</h2>
      
      <p>
        Добавьте сотрудников в систему для проведения опросов и анализа вовлеченности.
        Вы можете добавить сотрудников по одному или импортировать список из CSV-файла.
      </p>
      
      <div className="empty-state-actions">
        <button 
          className="create-employee-button"
          onClick={onCreateClick}
        >
          Добавить сотрудника
        </button>
        
        <button 
          className="import-button"
          onClick={onImportClick}
        >
          Импорт из CSV
        </button>
      </div>
    </div>
  );
};

Интеграция с API

Модуль управления командами интегрируется с API-сервисами для получения и сохранения данных о командах и сотрудниках.

API-сервис для работы с командами

Сервис teamsApi предоставляет функции для взаимодействия с API-эндпоинтами, связанными с командами и сотрудниками:

// Пример API-сервиса из src/services/api/teamsApi.ts
import { Team, TeamMember, Employee, Department } from '../../types';

const API_URL = 'https://api.hroom.ai';

export const teamsApi = {
  // Получение списка сотрудников
  async getEmployees(params = {}) {
    const queryParams = new URLSearchParams();
    
    // Добавление параметров запроса
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== '') {
        queryParams.append(key, value.toString());
      }
    });
    
    const response = await fetch(`${API_URL}/employees?${queryParams.toString()}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error('Failed to fetch employees');
    }
    
    return await response.json();
  },
  
  // Получение сотрудника по ID
  async getEmployeeById(id) {
    const response = await fetch(`${API_URL}/employees/${id}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`Failed to fetch employee with ID ${id}`);
    }
    
    return await response.json();
  },
  
  // Создание нового сотрудника
  async createEmployee(employeeData) {
    const response = await fetch(`${API_URL}/employees`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify(employeeData)
    });
    
    if (!response.ok) {
      throw new Error('Failed to create employee');
    }
    
    return await response.json();
  },
  
  // Обновление данных сотрудника
  async updateEmployee(id, employeeData) {
    const response = await fetch(`${API_URL}/employees/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify(employeeData)
    });
    
    if (!response.ok) {
      throw new Error(`Failed to update employee with ID ${id}`);
    }
    
    return await response.json();
  },
  
  // Обновление статуса сотрудника
  async updateEmployeeStatus(id, status) {
    const response = await fetch(`${API_URL}/employees/${id}/status`, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify({ status })
    });
    
    if (!response.ok) {
      throw new Error(`Failed to update status for employee with ID ${id}`);
    }
    
    return await response.json();
  },
  
  // Массовый импорт сотрудников
  async bulkImportEmployees(employees) {
    const response = await fetch(`${API_URL}/employees/bulk-import`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify({ employees })
    });
    
    if (!response.ok) {
      throw new Error('Failed to import employees');
    }
    
    return await response.json();
  },
  
  // Массовое обновление статуса сотрудников
  async bulkUpdateEmployeeStatus(employeeIds, status) {
    const response = await fetch(`${API_URL}/employees/bulk-status-update`, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify({ employeeIds, status })
    });
    
    if (!response.ok) {
      throw new Error('Failed to update employee statuses');
    }
    
    return await response.json();
  }
};

Сервис для работы с отделами

Сервис departmentService предоставляет функции для управления отделами:

// Пример сервиса из src/services/teams/departmentService.ts
import { Department } from '../types';

const API_URL = 'https://api.hroom.ai';

export const departmentService = {
  // Получение списка отделов
  async getDepartments() {
    const response = await fetch(`${API_URL}/departments`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error('Failed to fetch departments');
    }
    
    return await response.json();
  },
  
  // Получение отдела по ID
  async getDepartmentById(id) {
    const response = await fetch(`${API_URL}/departments/${id}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`Failed to fetch department with ID ${id}`);
    }
    
    return await response.json();
  },
  
  // Создание нового отдела
  async createDepartment(departmentData) {
    const response = await fetch(`${API_URL}/departments`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify(departmentData)
    });
    
    if (!response.ok) {
      throw new Error('Failed to create department');
    }
    
    return await response.json();
  },
  
  // Обновление отдела
  async updateDepartment(id, departmentData) {
    const response = await fetch(`${API_URL}/departments/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      },
      body: JSON.stringify(departmentData)
    });
    
    if (!response.ok) {
      throw new Error(`Failed to update department with ID ${id}`);
    }
    
    return await response.json();
  },
  
  // Удаление отдела
  async deleteDepartment(id) {
    const response = await fetch(`${API_URL}/departments/${id}`, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`Failed to delete department with ID ${id}`);
    }
    
    return true;
  },
  
  // Получение сотрудников отдела
  async getDepartmentEmployees(departmentId, params = {}) {
    const queryParams = new URLSearchParams();
    
    // Добавление параметров запроса
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== '') {
        queryParams.append(key, value.toString());
      }
    });
    
    const response = await fetch(`${API_URL}/departments/${departmentId}/employees?${queryParams.toString()}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`Failed to fetch employees for department ${departmentId}`);
    }
    
    return await response.json();
  }
};

Пагинация

Компонент Pagination предоставляет интерфейс для навигации по страницам списка сотрудников:

// Пример компонента Pagination из src/components/teams/Pagination.tsx
export const Pagination = ({ currentPage, totalPages, onPageChange }) => {
  // Функция для генерации массива номеров страниц для отображения
  const getPageNumbers = () => {
    const pageNumbers = [];
    
    // Ограничиваем количество отображаемых страниц
    if (totalPages <= 7) {
      // Если страниц мало, показываем все
      for (let i = 1; i <= totalPages; i++) {
        pageNumbers.push(i);
      }
    } else {
      // Если страниц много, показываем текущую, несколько соседних и границы
      if (currentPage <= 3) {
        // Начало списка
        for (let i = 1; i <= 5; i++) {
          pageNumbers.push(i);
        }
        pageNumbers.push('...');
        pageNumbers.push(totalPages);
      } else if (currentPage >= totalPages - 2) {
        // Конец списка
        pageNumbers.push(1);
        pageNumbers.push('...');
        for (let i = totalPages - 4; i <= totalPages; i++) {
          pageNumbers.push(i);
        }
      } else {
        // Середина списка
        pageNumbers.push(1);
        pageNumbers.push('...');
        for (let i = currentPage - 1; i <= currentPage + 1; i++) {
          pageNumbers.push(i);
        }
        pageNumbers.push('...');
        pageNumbers.push(totalPages);
      }
    }
    
    return pageNumbers;
  };
  
  // Переход на предыдущую страницу
  const goToPrevPage = () => {
    if (currentPage > 1) {
      onPageChange(currentPage - 1);
    }
  };
  
  // Переход на следующую страницу
  const goToNextPage = () => {
    if (currentPage < totalPages) {
      onPageChange(currentPage + 1);
    }
  };
  
  // Переход на конкретную страницу
  const goToPage = (page) => {
    if (page !== '...' && page !== currentPage) {
      onPageChange(page);
    }
  };
  
  return (
    <div className="pagination">
      <button 
        className="pagination-button prev"
        onClick={goToPrevPage}
        disabled={currentPage === 1}
      >
        < Назад
      </button>
      
      <div className="pagination-pages">
        {getPageNumbers().map((page, index) => (
          <button 
            key={index}
            className={`pagination-page ${page === currentPage ? 'active' : ''} ${page === '...' ? 'ellipsis' : ''}`}
            onClick={() => goToPage(page)}
            disabled={page === '...'}
          >
            {page}
          </button>
        ))}
      </div>
      
      <button 
        className="pagination-button next"
        onClick={goToNextPage}
        disabled={currentPage === totalPages}
      >
        Вперед >
      </button>
    </div>
  );
};

Выводы

Модуль "Управление командами" в системе HRoom предоставляет комплексный набор инструментов для организации и управления структурой компании, отделами, командами и сотрудниками. Этот модуль является фундаментальной частью платформы, так как он определяет организационную структуру, необходимую для правильного функционирования опросов, аналитики и других компонентов системы.

Основные преимущества модуля управления командами:

  • Гибкость структуры - возможность создания иерархической структуры отделов, соответствующей организации компании
  • Удобное управление сотрудниками - интуитивно понятный интерфейс для добавления, редактирования и архивирования сотрудников
  • Массовые операции - инструменты для массового импорта и обновления данных о сотрудниках
  • Эффективный поиск и фильтрация - возможность быстро находить нужных сотрудников и отделы
  • Интеграция с другими модулями - тесная связь с модулями опросов, аналитики и других компонентов системы

Модуль управления командами обеспечивает создание прочной основы для управления персоналом и проведения опросов, что является ключевым фактором для успешного анализа вовлеченности сотрудников и повышения эффективности работы команд.

```