Темный режим

2. Архитектура приложения

2.1. Структура компонентов

HRoom следует модульной архитектуре компонентов, основанной на принципах переиспользования и разделения ответственности.

Основные типы компонентов

Приложение структурировано с использованием следующих типов компонентов:

  • Компоненты страниц - находятся в директории src/pages и представляют собой полноценные страницы приложения
  • Компоненты UI - находятся в директории src/components/ui и предоставляют базовые элементы интерфейса
  • Компоненты бизнес-логики - специализированные компоненты для конкретных модулей
  • Контейнерные компоненты - управляют состоянием и передают данные презентационным компонентам

Иерархия компонентов


HRoom
├── Layout                 # Основной компонент-контейнер
│   ├── Sidebar            # Боковая навигация
│   └── Main Content       # Основное содержимое
│       ├── Dashboard      # Компоненты дашборда
│       ├── Analytics      # Компоненты аналитики
│       ├── Teams          # Управление командами
│       └── Other pages    # Прочие страницы
                

Композиция компонентов

HRoom использует композиционный подход к построению интерфейса. Пример композиции компонентов:


// Пример композиции компонентов Dashboard из src/pages/Dashboard.tsx
<Layout>
  <EngagementGauge />
  <DepartmentsHeatmap />
  <TeamsModule />
  <MetricsModule />
  <InsightsSlider />
  <QuestionsSlider />
</Layout>
                

Разделение ответственности

Компоненты в HRoom следуют принципу разделения ответственности:

  • Container Components - отвечают за данные и бизнес-логику
  • Presentational Components - отвечают за отображение UI
  • Hooks - отвечают за логику управления состоянием и побочными эффектами

Пример разделения ответственности можно увидеть в компоненте DepartmentsHeatmap:


// Компонент-контейнер
export const DepartmentsHeatmap = () => {
  // Извлечение данных через хук
  const { data, isLoading, error } = useHeatmapData();
  
  // Обработка состояний загрузки и ошибки
  if (isLoading) return <LoadingState />;
  if (error) return <ErrorState error={error} />;
  if (!data || data.length === 0) return <EmptyState />;
  
  // Рендеринг презентационных компонентов
  return (
    <div className="heatmap-container">
      <PeriodSelector />
      <MetricHeader />
      <HeatmapGrid data={data} />
    </div>
  );
};
                

2.2. Маршрутизация

HRoom использует React Router для организации маршрутизации в приложении.

Структура маршрутов

Основные маршруты приложения определены в корневом компоненте App.tsx:


// Упрощенная структура маршрутов из App.tsx
<Routes>
  {/* Публичные маршруты */}
  <Route path="/auth" element={<Auth />}>
    <Route path="login" element={<LoginForm />} />
    <Route path="signup" element={<SignupForm />} />
    <Route path="reset-password" element={<ResetPasswordForm />} />
    <Route path="new-password" element={<NewPasswordForm />} />
  </Route>
  
  <Route path="/unsubscribe" element={<Unsubscribe />} />
  
  {/* Защищенные маршруты */}
  <Route element={<ProtectedRoute />}>
    <Route path="/" element={<Layout />}>
      <Route index element={<Dashboard />} />
      <Route path="analytics" element={<Analytics />} />
      <Route path="teams" element={<Teams />} />
      <Route path="surveys" element={<Surveys />} />
      <Route path="ideas" element={<Ideas />} />
      <Route path="comments" element={<Comments />} />
      <Route path="profile" element={<Profile />} />
      
      {/* Административные маршруты */}
      <Route path="admin" element={<Admin />}>
        <Route path="companies" element={<Companies />} />
        <Route path="hrmanagers" element={<HRManagers />} />
        <Route path="questions" element={<Questions />} />
        <Route path="question-groups" element={<QuestionGroups />} />
        <Route path="settings" element={<Settings />} />
      </Route>
    </Route>
  </Route>
  
  {/* Маршрут 404 */}
  <Route path="*" element={<NotFound />} />
</Routes>
                

Вложенные маршруты

HRoom активно использует вложенные маршруты для организации логических групп страниц, например:

  • Маршруты аутентификации (/auth/*)
  • Административный панель (/admin/*)
  • Профиль пользователя (/profile/*)

Защищенные маршруты

Для защиты маршрутов от неавторизованного доступа используется компонент ProtectedRoute:


// Пример компонента ProtectedRoute
const ProtectedRoute = () => {
  const { isAuthenticated, isLoading } = useAuth();
  const location = useLocation();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (!isAuthenticated) {
    // Перенаправление на страницу входа с сохранением целевого URL
    return <Navigate to="/auth/login" state={{ from: location }} replace />;
  }
  
  // Рендер дочерних маршрутов если пользователь аутентифицирован
  return <Outlet />;
};
                

Программная навигация

В компонентах часто используется хук useNavigate для программной навигации:


const SurveyForm = () => {
  const navigate = useNavigate();
  
  const handleSubmit = async (data) => {
    // Отправка формы
    const result = await createSurvey(data);
    
    if (result.success) {
      // Программная навигация после успешного создания
      navigate('/surveys');
    }
  };
  
  // ...остальная часть компонента
};
                

2.3. Управление состоянием

HRoom использует комбинацию нескольких подходов к управлению состоянием на разных уровнях приложения.

Локальное состояние компонентов

Для управления локальным состоянием используются React-хуки useState и useReducer:


// Пример использования useState в SurveyForm.tsx
const SurveyForm = () => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [questions, setQuestions] = useState([]);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // ...логика компонента
};

// Пример использования useReducer для сложного состояния
const initialState = { 
  filters: {}, 
  sorting: { field: 'name', direction: 'asc' },
  pagination: { page: 1, perPage: 10 }
};

function tableReducer(state, action) {
  switch (action.type) {
    case 'SET_FILTER':
      return { ...state, filters: { ...state.filters, [action.field]: action.value } };
    case 'CLEAR_FILTERS':
      return { ...state, filters: {} };
    case 'SET_SORTING':
      return { ...state, sorting: { field: action.field, direction: action.direction } };
    case 'SET_PAGE':
      return { ...state, pagination: { ...state.pagination, page: action.page } };
    default:
      return state;
  }
}

const EmployeeTable = () => {
  const [state, dispatch] = useReducer(tableReducer, initialState);
  // ...логика компонента
};
                

Пользовательские хуки для управления состоянием

HRoom активно использует пользовательские хуки для инкапсуляции и переиспользования логики управления состоянием:


// Пример хука useAnswers из src/pages/Analytics/hooks/useAnswers.ts
export const useAnswers = (surveyId) => {
  const [answers, setAnswers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchAnswers = async () => {
      try {
        setIsLoading(true);
        const data = await answersService.getAnswersBySurveyId(surveyId);
        setAnswers(data);
        setError(null);
      } catch (err) {
        setError(err.message || 'Ошибка при загрузке ответов');
      } finally {
        setIsLoading(false);
      }
    };
    
    if (surveyId) {
      fetchAnswers();
    }
  }, [surveyId]);
  
  const groupedAnswers = useMemo(() => {
    return groupAnswersByMetric(answers);
  }, [answers]);
  
  return { answers, groupedAnswers, isLoading, error };
};
                

Контекст React для глобального состояния

Для управления глобальным состоянием на уровне приложения используются React Контексты:


// Пример использования контекста для глобального состояния

// 1. Создание контекста
const CompanyContext = createContext();

// 2. Провайдер контекста
export const CompanyProvider = ({ children }) => {
  const [companyId, setCompanyId] = useState(() => {
    // Инициализация из localStorage или значение по умолчанию
    return localStorage.getItem('companyId') || null;
  });
  
  useEffect(() => {
    // Синхронизация с localStorage
    if (companyId) {
      localStorage.setItem('companyId', companyId);
    } else {
      localStorage.removeItem('companyId');
    }
  }, [companyId]);
  
  return (
    <CompanyContext.Provider value={{ companyId, setCompanyId }}>
      {children}
    </CompanyContext.Provider>
  );
};

// 3. Хук для использования контекста
export const useCompany = () => {
  const context = useContext(CompanyContext);
  if (!context) {
    throw new Error('useCompany must be used within a CompanyProvider');
  }
  return context;
};

// 4. Использование в компонентах
const TeamsList = () => {
  const { companyId } = useCompany();
  // ...остальная часть компонента
};
                

Кэширование и обработка асинхронных состояний

Для оптимизации производительности и обработки асинхронных операций в HRoom используются хуки с механизмами кэширования данных и обработки состояний загрузки:


// Пример из useHeatmapData.ts с кэшированием результатов
export const useHeatmapData = (departmentId = null, metricId = null) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const cache = useRef(new Map());
  
  useEffect(() => {
    const fetchData = async () => {
      const cacheKey = `${departmentId}_${metricId}`;
      
      // Проверка наличия данных в кэше
      if (cache.current.has(cacheKey)) {
        setData(cache.current.get(cacheKey));
        setIsLoading(false);
        return;
      }
      
      try {
        setIsLoading(true);
        const result = await heatmapApi.getHeatmapData(departmentId, metricId);
        
        // Сохранение в кэше
        cache.current.set(cacheKey, result);
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message || 'Ошибка при загрузке данных');
        setData(null);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchData();
  }, [departmentId, metricId]);
  
  return { data, isLoading, error };
};
                

2.4. Аутентификация и авторизация

HRoom использует защищенное API (api.hroom.ai) для обеспечения безопасной аутентификации пользователей и проверки их полномочий.

Аутентификация пользователей

Процесс аутентификации в HRoom включает несколько методов входа:

  • Вход через email и пароль
  • Сброс и восстановление пароля
  • Регистрация новых пользователей

Аутентификация реализована с использованием стандартных RESTful API-запросов:


// Пример авторизации через API
const loginUser = async (email, password) => {
  try {
    const response = await fetch('https://api.hroom.ai/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password })
    });
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message || 'Ошибка авторизации');
    }
    
    // Сохранение токена для дальнейших запросов
    localStorage.setItem('authToken', data.token);
    
    return { user: data.user, error: null };
  } catch (error) {
    return { user: null, error: error.message };
  }
};

// Функция для регистрации пользователя
const registerUser = async (email, password, userData) => {
  try {
    const response = await fetch('https://api.hroom.ai/auth/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password, ...userData })
    });
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message || 'Ошибка регистрации');
    }
    
    return { user: data.user, error: null };
  } catch (error) {
    return { user: null, error: error.message };
  }
};

// Функция для выхода пользователя
const logoutUser = async () => {
  try {
    localStorage.removeItem('authToken');
    return { success: true, error: null };
  } catch (error) {
    return { success: false, error: error.message };
  }
};
                

Состояние аутентификации

Для отслеживания состояния аутентификации пользователя используется React Context и пользовательский хук:


// Пример контекста Auth
import { createContext, useContext, useEffect, useState } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Проверка наличия токена при загрузке
    const checkAuthStatus = async () => {
      const token = localStorage.getItem('authToken');
      
      if (token) {
        try {
          // Проверка валидности токена через API
          const response = await fetch('https://api.hroom.ai/auth/validate', {
            headers: {
              'Authorization': `Bearer ${token}`
            }
          });
          
          if (response.ok) {
            const userData = await response.json();
            setCurrentUser(userData);
          } else {
            // Токен недействителен, удаляем его
            localStorage.removeItem('authToken');
            setCurrentUser(null);
          }
        } catch (error) {
          console.error('Ошибка при проверке токена:', error);
          localStorage.removeItem('authToken');
          setCurrentUser(null);
        }
      }
      
      setLoading(false);
    };
    
    checkAuthStatus();
  }, []);
  
  const value = {
    currentUser,
    isAuthenticated: !!currentUser,
    isLoading: loading
  };
  
  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
};

// Хук для использования в компонентах
export const useAuth = () => {
  return useContext(AuthContext);
};
                

Авторизация и контроль доступа

HRoom реализует ролевую модель доступа (RBAC) для контроля прав пользователей:

  • Admin - полный доступ ко всем функциям системы
  • HRManager - управление командами, опросами и аналитикой
  • TeamLead - доступ к ограниченному набору функций
  • Employee - базовый доступ для работы с системой

Проверка прав доступа реализована с использованием компонента высшего порядка:


// Пример компонента для проверки ролей
const RoleBasedRoute = ({ requiredRoles, children }) => {
  const { currentUser, isLoading } = useAuth();
  const [userRoles, setUserRoles] = useState([]);
  const [checking, setChecking] = useState(true);
  
  useEffect(() => {
    const checkUserRoles = async () => {
      if (!currentUser) {
        setChecking(false);
        return;
      }
      
      try {
        // Получение ролей пользователя из API
        const token = localStorage.getItem('authToken');
        const response = await fetch('https://api.hroom.ai/users/roles', {
          headers: {
            'Authorization': `Bearer ${token}`
          }
        });
        
        if (response.ok) {
          const data = await response.json();
          setUserRoles(data.roles || []);
        }
      } catch (error) {
        console.error('Ошибка при получении ролей:', error);
      } finally {
        setChecking(false);
      }
    };
    
    if (!isLoading) {
      checkUserRoles();
    }
  }, [currentUser, isLoading]);
  
  if (isLoading || checking) {
    return <LoadingSpinner />;
  }
  
  // Проверка наличия требуемой роли
  const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
  
  if (!hasRequiredRole) {
    return <AccessDenied />;
  }
  
  return children;
};

// Пример использования
<RoleBasedRoute requiredRoles={['Admin', 'HRManager']}>
  <AdminPanel />
</RoleBasedRoute>
                

Безопасность токенов

HRoom использует JWT-токены для авторизации API-запросов:


// Использование токена в API-запросах
export const apiClient = axios.create({
  baseURL: 'https://api.hroom.ai',
});

apiClient.interceptors.request.use(async (config) => {
  const token = localStorage.getItem('authToken');
  
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  
  return config;
});
                

Страницы аутентификации

HRoom включает следующие страницы для управления аутентификацией:

  • LoginForm - вход в систему
  • SignupForm - регистрация нового пользователя
  • ResetPasswordForm - запрос сброса пароля
  • NewPasswordForm - установка нового пароля

Пример компонента входа в систему:


// Упрощенный пример из LoginForm.tsx
export const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const navigate = useNavigate();
  const location = useLocation();
  
  const from = location.state?.from?.pathname || '/';
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');
    
    try {
      const { user, error } = await loginUser(email, password);
      
      if (error) {
        throw new Error(error);
      }
      
      // Перенаправление на предыдущую страницу или дашборд
      navigate(from, { replace: true });
    } catch (err) {
      setError(err.message || 'Произошла ошибка при входе');
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Форма входа */}
    </form>
  );
};