Темный режим

3.2. Аналитика

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

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

  • Teams - аналитика по командам с тепловыми картами метрик
  • Answers - анализ ответов на опросы по метрикам и подметрикам
  • Stats - агрегированная статистика по ключевым показателям

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

Модуль аналитики организован по функциональному принципу и включает следующую структуру:


src/pages/Analytics/
├── components/            # Компоненты пользовательского интерфейса
│   ├── Teams/             # Компоненты для анализа команд
│   ├── Answers/           # Компоненты для анализа ответов
│   ├── EmptyStates/       # Компоненты для отображения пустых состояний
│   └── Stats.tsx          # Компонент статистики
├── hooks/                 # Пользовательские хуки модуля
│   ├── useAnswers.ts      # Хук для работы с данными ответов
│   ├── useAnswersFiltering.ts # Хук для фильтрации ответов
│   └── useWindowSize.ts   # Хук для адаптивности интерфейса
├── services/              # Сервисы для работы с данными
│   └── teamsService.ts    # Сервис для получения данных команд
├── types/                 # Типы TypeScript для модуля
│   └── teams.ts           # Типы для команд и их метрик
├── utils/                 # Утилиты и вспомогательные функции
│   ├── metricIcons.ts     # Иконки для различных метрик
│   ├── sortAnswers.ts     # Функции сортировки ответов
│   └── teams.ts           # Утилиты для работы с данными команд
└── index.tsx              # Главный компонент страницы аналитики
                

Анализ команд

Компонент анализа команд предоставляет инструменты для визуализации и сравнения различных метрик по командам в виде тепловых карт и списков.

Компоненты для анализа команд

  • TeamsList - список команд с возможностью фильтрации и сортировки
  • Heatmap - тепловая карта метрик по командам
  • HeatmapGrid - сетка тепловой карты для визуализации метрик
  • HeatmapCell - ячейка с данными метрики и визуальным представлением
  • MetricHeader - заголовок с названием и иконкой метрики
  • MetricsList - список доступных метрик для выбора
  • Tooltip - всплывающая подсказка с подробной информацией по метрике

Принцип работы тепловой карты команд

Тепловая карта визуализирует различные метрики по командам, используя цветовую градацию для отображения значений. Принцип работы включает следующие этапы:

  1. Получение данных - с использованием API запрашиваются данные метрик для команд за выбранный период
  2. Нормализация данных - значения метрик нормализуются в диапазоне от 0 до 100 для единообразного отображения
  3. Расчет цветовой интенсивности - для каждой ячейки тепловой карты рассчитывается цвет в зависимости от значения метрики
  4. Отображение тепловой карты - данные визуализируются в виде сетки, где строки соответствуют командам, а столбцы - метрикам
  5. Интерактивность - при наведении на ячейку отображается детальная информация в виде всплывающей подсказки

Ниже представлен пример компонента сетки тепловой карты:

// Компонент HeatmapGrid из src/pages/Analytics/components/Teams/HeatmapGrid.tsx
export const HeatmapGrid = ({ teams, metrics, data }) => {
  return (
    <div className="heatmap-grid">
      {/* Заголовки метрик */}
      <div className="metrics-headers">
        {metrics.map(metric => (
          <MetricHeader 
            key={metric.id} 
            name={metric.name} 
            icon={metric.icon} 
          />
        ))}
      </div>
      
      {/* Данные по командам */}
      {teams.map(team => (
        <div className="team-row" key={team.id}>
          <div className="team-name">{team.name}</div>
          {metrics.map(metric => {
            const value = data[team.id]?.[metric.id] || 0;
            return (
              <HeatmapCell 
                key={`${team.id}-${metric.id}`}
                value={value}
                teamId={team.id}
                metricId={metric.id}
              />
            );
          })}
        </div>
      ))}
    </div>
  );
};

Компонент ячейки тепловой карты

Ячейка тепловой карты (HeatmapCell) отвечает за визуализацию конкретного значения метрики. Принцип работы:

  1. Получение значения метрики для конкретной команды
  2. Определение цвета ячейки на основе значения (от красного для низких значений до зеленого для высоких)
  3. Отображение ячейки с соответствующим цветом и прозрачностью
  4. При наведении - отображение всплывающего окна с деталями
// Пример компонента HeatmapCell из src/pages/Analytics/components/Teams/HeatmapCell.tsx
export const HeatmapCell = ({ value, teamId, metricId }) => {
  const [showTooltip, setShowTooltip] = useState(false);
  const cellRef = useRef(null);
  
  // Определение цвета ячейки на основе значения (от 0 до 100)
  const getBackgroundColor = (value) => {
    // Для значений ниже 50 - оттенки красного
    if (value < 50) {
      const intensity = 100 - Math.round(value * 2);
      return `rgba(255, ${intensity}, ${intensity}, ${0.3 + value / 150})`;
    }
    // Для значений выше 50 - оттенки зеленого
    else {
      const intensity = Math.round((value - 50) * 2);
      return `rgba(${100 - intensity}, 255, ${100 - intensity}, ${0.3 + value / 150})`;
    }
  };
  
  return (
    <div 
      className="heatmap-cell"
      ref={cellRef}
      style={{ backgroundColor: getBackgroundColor(value) }}
      onMouseEnter={() => setShowTooltip(true)}
      onMouseLeave={() => setShowTooltip(false)}
    >
      {value > 0 && <span className="cell-value">{value}</span>}
      
      {showTooltip && (
        <Tooltip 
          teamId={teamId}
          metricId={metricId}
          value={value}
          anchorEl={cellRef.current}
        />
      )}
    </div>
  );
};

Всплывающие подсказки

Компонент Tooltip отображает детальную информацию о метрике при наведении на ячейку. Он включает:

  • Название команды и метрики
  • Текущее значение метрики
  • Изменение по сравнению с предыдущим периодом
  • График тренда за последние периоды
  • Краткие рекомендации по улучшению показателя

Фильтрация данных команд

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

  • Фильтрация по отделу
  • Фильтрация по периоду (неделя, месяц, квартал, год)
  • Фильтрация по диапазону значений метрик
  • Поиск по названию команды

Анализ ответов на опросы

Компонент анализа ответов позволяет исследовать ответы сотрудников на вопросы опросов, группировать их по метрикам и подметрикам, а также выявлять закономерности и тренды.

Компоненты для анализа ответов

  • AnswersList - список ответов с фильтрацией и группировкой
  • AnswerCard - карточка с детальной информацией по ответу
  • AnswerFilters - компонент с фильтрами для ответов
  • SortControls - элементы управления сортировкой
  • MetricGroup - группа метрик для группировки ответов
  • SubmetricGroup - группа подметрик для более детального анализа
  • QuestionCard - карточка с вопросом и распределением ответов

Хук useAnswers

Хук useAnswers отвечает за получение и обработку данных ответов на опросы. Он выполняет следующие функции:

  • Получение ответов по идентификатору опроса
  • Группировка ответов по метрикам и подметрикам
  • Расчет статистических показателей (средние значения, медианы, распределения)
  • Кэширование полученных данных для оптимизации производительности
  • Обработка состояний загрузки и ошибок
// Пример хука 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]);
  
  // Расчет статистических показателей
  const statistics = useMemo(() => {
    return calculateStatistics(answers);
  }, [answers]);
  
  return { 
    answers, 
    groupedAnswers, 
    statistics,
    isLoading, 
    error 
  };
};

Фильтрация и сортировка ответов

Модуль анализа ответов предоставляет развернутые возможности для фильтрации и сортировки данных:

Хук useAnswersFiltering

Хук useAnswersFiltering реализует логику фильтрации ответов по различным критериям:

// Упрощенный пример хука useAnswersFiltering
export const useAnswersFiltering = (answers) => {
  const [filters, setFilters] = useState({
    metrics: [],
    submetrics: [],
    dateRange: { start: null, end: null },
    teams: [],
    departments: [],
    minScore: 0,
    maxScore: 100
  });
  
  const [sortConfig, setSortConfig] = useState({
    field: 'date',
    direction: 'desc'
  });
  
  // Применение фильтров к ответам
  const filteredAnswers = useMemo(() => {
    return answers.filter(answer => {
      // Фильтрация по метрикам
      if (filters.metrics.length > 0 && !filters.metrics.includes(answer.metricId)) {
        return false;
      }
      
      // Фильтрация по подметрикам
      if (filters.submetrics.length > 0 && !filters.submetrics.includes(answer.submetricId)) {
        return false;
      }
      
      // Фильтрация по дате
      if (filters.dateRange.start && new Date(answer.submittedAt) < filters.dateRange.start) {
        return false;
      }
      if (filters.dateRange.end && new Date(answer.submittedAt) > filters.dateRange.end) {
        return false;
      }
      
      // Фильтрация по командам
      if (filters.teams.length > 0 && !filters.teams.includes(answer.teamId)) {
        return false;
      }
      
      // Фильтрация по отделам
      if (filters.departments.length > 0 && !filters.departments.includes(answer.departmentId)) {
        return false;
      }
      
      // Фильтрация по оценке
      if (answer.score < filters.minScore || answer.score > filters.maxScore) {
        return false;
      }
      
      return true;
    });
  }, [answers, filters]);
  
  // Сортировка отфильтрованных ответов
  const sortedAnswers = useMemo(() => {
    return sortAnswers(filteredAnswers, sortConfig);
  }, [filteredAnswers, sortConfig]);
  
  // Функция установки фильтров
  const setFilter = (filterType, value) => {
    setFilters(prev => ({
      ...prev,
      [filterType]: value
    }));
  };
  
  // Функция установки сортировки
  const setSorting = (field, direction) => {
    setSortConfig({ field, direction });
  };
  
  return {
    filters,
    setFilter,
    sortConfig,
    setSorting,
    filteredAnswers: sortedAnswers
  };
};

Функции сортировки

Для упорядочивания ответов используются различные алгоритмы сортировки, определенные в утилите sortAnswers.ts:

// Пример функции сортировки из src/pages/Analytics/utils/sortAnswers.ts
export const sortAnswers = (answers, sortConfig) => {
  const { field, direction } = sortConfig;
  
  return [...answers].sort((a, b) => {
    if (field === 'date') {
      const dateA = new Date(a.submittedAt).getTime();
      const dateB = new Date(b.submittedAt).getTime();
      return direction === 'asc' ? dateA - dateB : dateB - dateA;
    }
    
    if (field === 'score') {
      return direction === 'asc' ? a.score - b.score : b.score - a.score;
    }
    
    if (field === 'questionText') {
      return direction === 'asc' 
        ? a.questionText.localeCompare(b.questionText)
        : b.questionText.localeCompare(a.questionText);
    }
    
    if (field === 'metricName') {
      return direction === 'asc'
        ? a.metricName.localeCompare(b.metricName)
        : b.metricName.localeCompare(a.metricName);
    }
    
    // Сортировка по количеству ответов
    if (field === 'responsesCount') {
      return direction === 'asc'
        ? a.responsesCount - b.responsesCount
        : b.responsesCount - a.responsesCount;
    }
    
    // По умолчанию возвращаем без изменений
    return 0;
  });
};

Группировка ответов по метрикам

Компонент MetricGroup отвечает за группировку и отображение ответов по метрикам. Он организует ответы в иерархическую структуру:

// Пример компонента MetricGroup из src/pages/Analytics/components/Answers/MetricGroup.tsx
export const MetricGroup = ({ metric, submetrics, answers }) => {
  const [expanded, setExpanded] = useState(true);
  
  // Расчет агрегированных показателей для метрики
  const averageScore = useMemo(() => {
    const scores = answers.map(a => a.score);
    return scores.length ? 
      scores.reduce((acc, score) => acc + score, 0) / scores.length :
      0;
  }, [answers]);
  
  // Группировка ответов по подметрикам
  const answersBySubmetric = useMemo(() => {
    return submetrics.reduce((acc, submetric) => {
      acc[submetric.id] = answers.filter(
        answer => answer.submetricId === submetric.id
      );
      return acc;
    }, {});
  }, [submetrics, answers]);
  
  return (
    <div className="metric-group">
      <div 
        className="metric-header"
        onClick={() => setExpanded(!expanded)}
      >
        <div className="metric-icon" style={{ backgroundColor: metric.color }}>
          {/* Иконка метрики */}
        </div>
        <div className="metric-title">
          <h3>{metric.name}</h3>
          <span className="answers-count">
            {answers.length} ответов | Средний балл: {averageScore.toFixed(1)}
          </span>
        </div>
        <div className="expand-icon">
          {expanded ? '▼' : '►'}
        </div>
      </div>
      
      {expanded && (
        <div className="submetrics-container">
          {submetrics.map(submetric => (
            <SubmetricGroup 
              key={submetric.id}
              submetric={submetric}
              answers={answersBySubmetric[submetric.id] || []}
            />
          ))}
        </div>
      )}
    </div>
  );
};

Карточки ответов и вопросов

Компоненты AnswerCard и QuestionCard отвечают за отображение детальной информации об ответах и вопросах:

  • AnswerCard - отображает информацию о конкретном ответе, включая текст ответа, метрику, оценку и дополнительную информацию
  • QuestionCard - отображает вопрос, распределение ответов и статистические показатели
// Пример компонента QuestionCard из src/pages/Analytics/components/Answers/QuestionCard.tsx
export const QuestionCard = ({ question, answers }) => {
  // Распределение ответов по значениям (для вопросов с выбором оценки)
  const distribution = useMemo(() => {
    const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
    
    answers.forEach(answer => {
      if (answer.value >= 1 && answer.value <= 5) {
        dist[answer.value]++;
      }
    });
    
    return dist;
  }, [answers]);
  
  // Расчет процентного соотношения оценок
  const percentages = useMemo(() => {
    const total = Object.values(distribution).reduce((sum, count) => sum + count, 0);
    
    return Object.entries(distribution).reduce((acc, [score, count]) => {
      acc[score] = total > 0 ? (count / total) * 100 : 0;
      return acc;
    }, {});
  }, [distribution]);
  
  return (
    <div className="question-card">
      <h4 className="question-text">{question.text}</h4>
      
      <div className="question-stats">
        <div className="responses-count">
          Всего ответов: {answers.length}
        </div>
        
        <div className="average-score">
          Средняя оценка: {
            (answers.reduce((sum, a) => sum + a.value, 0) / 
            (answers.length || 1)).toFixed(1)
          }
        </div>
      </div>
      
      <div className="score-distribution">
        {Object.entries(percentages).map(([score, percentage]) => (
          <div key={score} className="score-bar-container">
            <span className="score-label">{score}</span>
            <div className="score-bar">
              <div 
                className="score-bar-fill"
                style={{ width: `${percentage}%` }}
              ></div>
            </div>
            <span className="score-percentage">
              {percentage.toFixed(1)}%
            </span>
          </div>
        ))}
      </div>
      
      {/* Комментарии к вопросу, если есть */}
      {answers.some(a => a.comment) && (
        <div className="question-comments">
          <h5>Комментарии:</h5>
          <ul>
            {answers
              .filter(a => a.comment)
              .map((a, idx) => (
                <li key={idx}>{a.comment}</li>
              ))
            }
          </ul>
        </div>
      )}
    </div>
  );
};

Статистика и агрегированные показатели

Компонент Stats предоставляет агрегированные статистические показатели для быстрого обзора ключевых метрик:

Компонент агрегированной статистики

Компонент Stats отображает обобщенные статистические данные в виде карточек с ключевыми показателями:

// Пример компонента Stats из src/pages/Analytics/components/Stats.tsx
export const Stats = ({ data }) => {
  // Вычисление общей статистики
  const statistics = useMemo(() => [
    {
      label: 'Средний уровень вовлеченности',
      value: data.engagementScore ? `${data.engagementScore.toFixed(1)}%` : 'N/A',
      change: data.engagementChange,
      icon: 'engagement',
      color: getScoreColor(data.engagementScore)
    },
    {
      label: 'Количество активных опросов',
      value: data.activeSurveys || 0,
      change: data.surveysChange,
      icon: 'surveys',
      color: '#6366f1'
    },
    {
      label: 'Процент участия в опросах',
      value: data.participationRate ? `${data.participationRate.toFixed(1)}%` : 'N/A',
      change: data.participationChange,
      icon: 'participation',
      color: getScoreColor(data.participationRate)
    },
    {
      label: 'Метрики с критическими показателями',
      value: data.criticalMetrics || 0,
      change: data.criticalMetricsChange,
      icon: 'warning',
      color: data.criticalMetrics > 0 ? '#ef4444' : '#10b981'
    }
  ], [data]);
  
  return (
    <div className="stats-container">
      {statistics.map((stat, index) => (
        <StatBar 
          key={index}
          label={stat.label}
          value={stat.value}
          change={stat.change}
          icon={stat.icon}
          color={stat.color}
        />
      ))}
    </div>
  );
};

Компонент StatBar

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

// Пример компонента StatBar из src/pages/Analytics/components/StatBar.tsx
export const StatBar = ({ label, value, change, icon, color }) => {
  // Определение стиля для индикатора изменения
  const getChangeStyle = (change) => {
    if (!change || change === 0) return 'neutral';
    return change > 0 ? 'positive' : 'negative';
  };
  
  // Форматирование значения изменения
  const formatChange = (change) => {
    if (!change || change === 0) return '0%';
    const prefix = change > 0 ? '+' : '';
    return `${prefix}${change.toFixed(1)}%`;
  };
  
  return (
    <div className="stat-bar">
      <div className="stat-icon" style={{ backgroundColor: color }}>
        {/* Иконка статистики */}
      </div>
      
      <div className="stat-content">
        <div className="stat-label">{label}</div>
        <div className="stat-value">{value}</div>
        
        {change !== undefined && (
          <div className={`stat-change ${getChangeStyle(change)}`}>
            {formatChange(change)}
          </div>
        )}
      </div>
    </div>
  );
};

Адаптивность интерфейса

Модуль аналитики адаптируется под различные размеры экрана с помощью хука useWindowSize, который определяет текущие размеры окна браузера и позволяет компонентам реагировать на изменения:

// Пример хука useWindowSize из src/pages/Analytics/hooks/useWindowSize.ts
export const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });
  
  useEffect(() => {
    // Функция обработки изменения размера окна
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    
    // Установка начальных размеров
    handleResize();
    
    // Добавление обработчика события изменения размера окна
    window.addEventListener('resize', handleResize);
    
    // Очистка обработчика при размонтировании компонента
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return windowSize;
};

Адаптивное отображение компонентов

На основе данных о размере окна компоненты аналитики адаптируют свое отображение:

  • На маленьких экранах отображается упрощенный вид без тепловых карт
  • Для планшетов и больших экранов используется полноценное отображение с тепловыми картами
  • Количество отображаемых метрик и размер карточек меняются в зависимости от ширины экрана

Пустые состояния и обработка ошибок

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

  • AnalyticsEmpty - компонент, отображаемый при отсутствии данных для анализа
  • Обработчики ошибок при загрузке данных с предложениями по решению проблемы
  • Индикаторы загрузки для улучшения пользовательского опыта во время получения данных

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

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

  • metricsApi - API для получения метрик и статистики
  • teamsApi - API для работы с данными команд
  • answersService - сервис для получения и анализа ответов на опросы

API-интеграции инкапсулированы в специализированные сервисы, что обеспечивает более чистую архитектуру и упрощает поддержку кода. Также реализована обработка ошибок и повторные попытки при сбоях в сетевых запросах.

Главный компонент страницы аналитики

Основной компонент Analytics объединяет все описанные выше компоненты и служит точкой входа в модуль аналитики:

// Пример основного компонента Analytics из src/pages/Analytics/index.tsx
export const Analytics = () => {
  const [activeTab, setActiveTab] = useState('teams');
  const [selectedSurveyId, setSelectedSurveyId] = useState(null);
  const { data, isLoading, error } = useAnalyticsData();
  
  // Обработка состояний загрузки и ошибок
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (error) {
    return <ErrorMessage message={error} />;
  }
  
  if (!data) {
    return <AnalyticsEmpty />;
  }
  
  return (
    <div className="analytics-container">
      <h1>Аналитика</h1>
      
      <Stats data={data.statistics} />
      
      <div className="analytics-tabs">
        <div className="tab-buttons">
          <button 
            className={activeTab === 'teams' ? 'active' : ''}
            onClick={() => setActiveTab('teams')}
          >
            Команды
          </button>
          <button 
            className={activeTab === 'answers' ? 'active' : ''}
            onClick={() => setActiveTab('answers')}
          >
            Ответы на опросы
          </button>
        </div>
        
        <div className="tab-content">
          {activeTab === 'teams' && (
            <div className="teams-container">
              <Heatmap 
                teams={data.teams}
                metrics={data.metrics}
                heatmapData={data.heatmapData}
              />
            </div>
          )}
          
          {activeTab === 'answers' && (
            <div className="answers-container">
              <div className="survey-selector">
                <label>Выберите опрос:</label>
                <select 
                  value={selectedSurveyId || ''}
                  onChange={(e) => setSelectedSurveyId(e.target.value)}
                >
                  <option value="">Выберите опрос</option>
                  {data.surveys.map(survey => (
                    <option key={survey.id} value={survey.id}>
                      {survey.title}
                    </option>
                  ))}
                </select>
              </div>
              
              {selectedSurveyId && (
                <AnswersList surveyId={selectedSurveyId} />
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

Выводы

Модуль аналитики HRoom представляет собой мощный инструмент для анализа данных команд и результатов опросов. Он предоставляет HR-менеджерам и руководителям компаний возможность глубокого анализа различных метрик и показателей, помогая выявлять как проблемные зоны, так и сильные стороны команд.

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

  • Интуитивно понятная визуализация данных с помощью тепловых карт
  • Гибкие инструменты фильтрации и сортировки
  • Детальный анализ ответов на опросы с группировкой по метрикам
  • Адаптивный дизайн для работы на различных устройствах
  • Оптимизированная производительность с использованием кэширования и мемоизации

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