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>
);
};