Темный режим

3.5. Опросы

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

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

  • Список опросов - управление существующими опросами
  • Создание опросов - интерфейс для создания новых опросов
  • Типы опросов - различные типы опросов для разных целей
  • Управление вопросами - создание и редактирование вопросов
  • Управление респондентами - выбор и настройка получателей опросов
  • Планирование - настройка времени проведения опросов

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

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


src/pages/Surveys/
├── components/              # Компоненты пользовательского интерфейса
│   ├── EmptyStates/         # Компоненты для отображения пустых состояний
├── services/                # Сервисы для работы с данными
├── hooks/                   # Пользовательские хуки
├── types/                   # Типы и интерфейсы
└── index.tsx                # Основной компонент страницы опросов

src/components/surveys/      # Общие компоненты для работы с опросами
├── CreateSurveyModal.tsx    # Модальное окно создания опроса
├── SurveyForm.tsx           # Форма для создания/редактирования опроса
├── SurveyList.tsx           # Список опросов
├── SurveyTypeSelect.tsx     # Выбор типа опроса
├── SurveyTypePopup.tsx      # Всплывающее окно с типами опросов
├── SurveyCalendar.tsx       # Календарь для планирования опросов
└── RespondentSearch.tsx     # Поиск и выбор респондентов
                

Типы опросов

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

1. Регулярные опросы (REGULAR)

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

2. Пульс-опросы (PULSE)

Короткие, сфокусированные опросы, которые проводятся часто (еженедельно или ежемесячно) для отслеживания динамики изменений по конкретным метрикам или темам. Обычно содержат небольшое количество вопросов.

3. Онбординг-опросы (ONBOARDING)

Специализированные опросы для новых сотрудников, помогающие оценить процесс адаптации и интеграции в команду. Обычно отправляются после определенных периодов работы (1 неделя, 1 месяц, 3 месяца).

4. Выходные интервью (EXIT)

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

Компоненты пользовательского интерфейса

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

Список опросов (SurveyList)

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

  • Просматривать опросы в виде списка с ключевой информацией
  • Фильтровать опросы по статусу, типу и дате
  • Сортировать опросы по различным параметрам
  • Выполнять действия: просмотр, редактирование, активация, архивирование, удаление
  • Отслеживать прогресс прохождения опросов (статистика ответов)
// Пример компонента SurveyList из src/components/surveys/SurveyList.tsx
export const SurveyList = ({ 
  surveys, 
  isLoading, 
  onCreateClick, 
  onSurveyClick 
}) => {
  const [filteredSurveys, setFilteredSurveys] = useState(surveys);
  const [filters, setFilters] = useState({
    status: 'all',
    type: 'all',
    dateRange: null
  });
  
  // Эффект для фильтрации опросов при изменении фильтров
  useEffect(() => {
    const filtered = surveys.filter(survey => {
      // Фильтрация по статусу
      if (filters.status !== 'all' && survey.status !== filters.status) {
        return false;
      }
      
      // Фильтрация по типу
      if (filters.type !== 'all' && survey.type !== filters.type) {
        return false;
      }
      
      // Фильтрация по дате
      if (filters.dateRange) {
        const surveyDate = new Date(survey.createdAt);
        if (
          (filters.dateRange.start && surveyDate < filters.dateRange.start) ||
          (filters.dateRange.end && surveyDate > filters.dateRange.end)
        ) {
          return false;
        }
      }
      
      return true;
    });
    
    setFilteredSurveys(filtered);
  }, [surveys, filters]);
  
  // Обработчик изменения фильтров
  const handleFilterChange = (filterName, value) => {
    setFilters(prev => ({
      ...prev,
      [filterName]: value
    }));
  };
  
  // Функция для отображения статуса опроса
  const renderStatus = (status) => {
    switch (status) {
      case 'DRAFT':
        return <span className="status-badge draft">Черновик</span>;
      case 'ACTIVE':
        return <span className="status-badge active">Активный</span>;
      case 'COMPLETED':
        return <span className="status-badge completed">Завершен</span>;
      case 'ARCHIVED':
        return <span className="status-badge archived">Архивный</span>;
      default:
        return null;
    }
  };
  
  // Получение локализованного названия типа опроса
  const getSurveyTypeLabel = (type) => {
    switch (type) {
      case 'REGULAR': return 'Регулярный';
      case 'PULSE': return 'Пульс-опрос';
      case 'ONBOARDING': return 'Онбординг';
      case 'EXIT': return 'Выходное интервью';
      default: return type;
    }
  };
  
  if (isLoading) {
    return <div className="loading-state">Загрузка опросов...</div>;
  }
  
  return (
    <div className="survey-list-container">
      <div className="survey-list-header">
        <h2>Опросы</h2>
        <button 
          className="create-survey-button"
          onClick={onCreateClick}
        >
          Создать опрос
        </button>
      </div>
      
      <div className="survey-filters">
        <div className="filter-group">
          <label>Статус:</label>
          <select 
            value={filters.status}
            onChange={(e) => handleFilterChange('status', e.target.value)}
          >
            <option value="all">Все статусы</option>
            <option value="DRAFT">Черновики</option>
            <option value="ACTIVE">Активные</option>
            <option value="COMPLETED">Завершенные</option>
            <option value="ARCHIVED">Архивные</option>
          </select>
        </div>
        
        <div className="filter-group">
          <label>Тип:</label>
          <select 
            value={filters.type}
            onChange={(e) => handleFilterChange('type', e.target.value)}
          >
            <option value="all">Все типы</option>
            <option value="REGULAR">Регулярные</option>
            <option value="PULSE">Пульс-опросы</option>
            <option value="ONBOARDING">Онбординг</option>
            <option value="EXIT">Выходные интервью</option>
          </select>
        </div>
      </div>
      
      {filteredSurveys.length === 0 ? (
        <div className="empty-surveys">
          <p>Опросы не найдены. Измените параметры фильтрации или создайте новый опрос.</p>
        </div>
      ) : (
        <div className="surveys-grid">
          {filteredSurveys.map(survey => (
            <div 
              key={survey.id}
              className="survey-card"
              onClick={() => onSurveyClick(survey.id)}
            >
              <div className="survey-card-header">
                <h3 className="survey-title">{survey.title}</h3>
                {renderStatus(survey.status)}
              </div>
              
              <div className="survey-type">
                {getSurveyTypeLabel(survey.type)}
              </div>
              
              <div className="survey-description">
                {survey.description || 'Описание отсутствует'}
              </div>
              
              <div className="survey-meta">
                <div className="survey-dates">
                  <div>Создан: {new Date(survey.createdAt).toLocaleDateString()}</div>
                  {survey.startDate && (
                    <div>Старт: {new Date(survey.startDate).toLocaleDateString()}</div>
                  )}
                  {survey.endDate && (
                    <div>Завершение: {new Date(survey.endDate).toLocaleDateString()}</div>
                  )}
                </div>
                
                <div className="survey-stats">
                  <div className="survey-questions-count">
                    Вопросов: {survey.questions?.length || 0}
                  </div>
                  
                  {survey.respondents !== undefined && (
                    <div className="survey-respondents">
                      Участников: {survey.respondents}
                    </div>
                  )}
                  
                  {survey.responses !== undefined && (
                    <div className="survey-responses">
                      Ответов: {survey.responses}
                      {survey.respondents && (
                        <span className="response-rate">
                          ({Math.round((survey.responses / survey.respondents) * 100)}%)
                        </span>
                      )}
                    </div>
                  )}
                </div>
              </div>
              
              <div className="survey-actions">
                <button className="view-survey-button">Просмотреть</button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

Создание опроса (CreateSurveyModal)

Компонент CreateSurveyModal предоставляет интерфейс для создания новых опросов. Он включает:

  • Выбор типа опроса
  • Заполнение основной информации (название, описание)
  • Добавление и настройку вопросов
  • Выбор респондентов
  • Планирование даты начала и окончания опроса
// Пример компонента CreateSurveyModal из src/components/surveys/CreateSurveyModal.tsx
export const CreateSurveyModal = ({ isOpen, onClose, onSave }) => {
  const [step, setStep] = useState(1);
  const [surveyData, setSurveyData] = useState({
    title: '',
    description: '',
    type: '',
    questions: [],
    respondents: [],
    startDate: null,
    endDate: null,
    status: 'DRAFT'
  });
  
  // Обработчик изменения основной информации об опросе
  const handleBasicInfoChange = (field, value) => {
    setSurveyData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Сброс ошибки для измененного поля
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };
  
  // Обработчик добавления вопроса
  const handleAddQuestion = (question) => {
    setSurveyData(prev => ({
      ...prev,
      questions: [...prev.questions, {
        id: `temp-${Date.now()}`,
        ...question
      }]
    }));
  };
  
  // Обработчик удаления вопроса
  const handleRemoveQuestion = (index) => {
    setSurveyData(prev => ({
      ...prev,
      questions: prev.questions.filter((q, i) => i !== index)
    }));
  };
  
  // Обработчик выбора респондентов
  const handleRespondentsChange = (selectedRespondents) => {
    setSurveyData(prev => ({
      ...prev,
      respondents: selectedRespondents
    }));
  };
  
  // Обработчик установки дат опроса
  const handleDateChange = (field, date) => {
    setSurveyData(prev => ({
      ...prev,
      [field]: date
    }));
  };
  
  // Валидация данных текущего шага
  const isStepValid = () => {
    switch (step) {
      case 1: // Выбор типа
        return !!surveyData.type;
      case 2: // Основная информация
        return !!surveyData.title.trim();
      case 3: // Вопросы
        return surveyData.questions.length > 0;
      case 4: // Респонденты
        return surveyData.respondents.length > 0;
      case 5: // Даты
        return !!surveyData.startDate;
      default:
        return true;
    }
  };
  
  // Обработчик перехода к следующему шагу
  const handleNextStep = () => {
    if (isStepValid()) {
      setStep(prevStep => prevStep + 1);
    }
  };
  
  // Обработчик возврата к предыдущему шагу
  const handlePrevStep = () => {
    setStep(prevStep => prevStep - 1);
  };
  
  // Обработчик сохранения опроса
  const handleSave = (asDraft = true) => {
    const finalData = {
      ...surveyData,
      status: asDraft ? 'DRAFT' : 'ACTIVE',
      createdAt: new Date().toISOString()
    };
    
    onSave(finalData);
    onClose();
  };
  
  if (!isOpen) {
    return null;
  }
  
  return (
    <div className="modal-overlay">
      <div className="modal-content create-survey-modal">
        <div className="modal-header">
          <h2>Создание нового опроса</h2>
          <button className="close-button" onClick={onClose}>×</button>
        </div>
        
        <div className="modal-progress-steps">
          <div className={`step ${step >= 1 ? 'active' : ''}`}>Тип</div>
          <div className={`step ${step >= 2 ? 'active' : ''}`}>Информация</div>
          <div className={`step ${step >= 3 ? 'active' : ''}`}>Вопросы</div>
          <div className={`step ${step >= 4 ? 'active' : ''}`}>Участники</div>
          <div className={`step ${step >= 5 ? 'active' : ''}`}>Даты</div>
        </div>
        
        <div className="modal-body">
          {step === 1 && (
            <SurveyTypeSelect onTypeSelect={handleTypeSelect} />
          )}
          
          {step === 2 && (
            <div className="survey-basic-info">
              <div className="form-group">
                <label>Название опроса *</label>
                <input 
                  type="text" 
                  value={surveyData.title} 
                  onChange={(e) => handleBasicInfoChange('title', e.target.value)}
                  placeholder="Введите название опроса"
                />
              </div>
              
              <div className="form-group">
                <label>Описание</label>
                <textarea 
                  value={surveyData.description} 
                  onChange={(e) => handleBasicInfoChange('description', e.target.value)}
                  placeholder="Введите описание опроса (необязательно)"
                  rows={4}
                />
              </div>
            </div>
          )}
          
          {step === 3 && (
            <div className="survey-questions-editor">
              <h3>Вопросы опроса</h3>
              
              {surveyData.questions.map((question, index) => (
                <div key={index} className="question-item">
                  <div className="question-content">
                    <h4>{question.text}</h4>
                    <div className="question-type">{question.type}</div>
                  </div>
                  
                  <div className="question-actions">
                    <button onClick={() => handleUpdateQuestion(index, question)}>
                      Редактировать
                    </button>
                    <button onClick={() => handleRemoveQuestion(index)}>
                      Удалить
                    </button>
                  </div>
                </div>
              ))}
              
              <button className="add-question-button" onClick={handleAddQuestion}>
                Добавить вопрос
              </button>
            </div>
          )}
          
          {step === 4 && (
            <div className="survey-respondents-selector">
              <h3>Выбор участников опроса</h3>
              <RespondentSearch 
                selectedRespondents={surveyData.respondents}
                onChange={handleRespondentsChange}
              />
            </div>
          )}
          
          {step === 5 && (
            <div className="survey-scheduling">
              <div className="form-row">
                <div className="form-group">
                  <label>Дата начала *</label>
                  <SurveyCalendar 
                    selectedDate={surveyData.startDate}
                    onChange={(date) => handleDateChange('startDate', date)}
                  />
                </div>
                
                <div className="form-group">
                  <label>Дата окончания</label>
                  <SurveyCalendar 
                    selectedDate={surveyData.endDate}
                    onChange={(date) => handleDateChange('endDate', date)}
                    minDate={surveyData.startDate}
                  />
                </div>
              </div>
            </div>
          )}
        </div>
        
        <div className="modal-footer">
          {step > 1 && (
            <button 
              className="secondary-button"
              onClick={handlePrevStep}
            >
              Назад
            </button>
          )}
          
          {step < 5 ? (
            <button 
              className="primary-button"
              onClick={handleNextStep}
              disabled={!isStepValid()}
            >
              Далее
            </button>
          ) : (
            <div className="final-actions">
              <button 
                className="secondary-button"
                onClick={() => handleSave(true)}
              >
                Сохранить как черновик
              </button>
              
              <button 
                className="primary-button"
                onClick={() => handleSave(false)}
                disabled={!isStepValid()}
              >
                Активировать опрос
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

Выбор типа опроса (SurveyTypeSelect)

Компонент SurveyTypeSelect предоставляет пользователю возможность выбрать тип создаваемого опроса с описанием каждого типа и рекомендациями по использованию:

// Пример компонента SurveyTypeSelect из src/components/surveys/SurveyTypeSelect.tsx
export const SurveyTypeSelect = ({ onTypeSelect }) => {
  const surveyTypes = [
    {
      id: 'REGULAR',
      name: 'Регулярный опрос',
      description: 'Комплексный опрос для глубокой оценки вовлеченности и опыта сотрудников.',
      icon: 'regular-icon',
      recommendedFrequency: 'Раз в квартал или полгода',
      recommendedQuestions: '20-30 вопросов'
    },
    {
      id: 'PULSE',
      name: 'Пульс-опрос',
      description: 'Короткий, частый опрос для мониторинга ключевых показателей и быстрого получения обратной связи.',
      icon: 'pulse-icon',
      recommendedFrequency: 'Еженедельно или ежемесячно',
      recommendedQuestions: '5-10 вопросов'
    },
    {
      id: 'ONBOARDING',
      name: 'Онбординг-опрос',
      description: 'Специализированный опрос для оценки процесса адаптации новых сотрудников.',
      icon: 'onboarding-icon',
      recommendedFrequency: 'После 1 недели, 1 месяца и 3 месяцев работы',
      recommendedQuestions: '10-15 вопросов'
    },
    {
      id: 'EXIT',
      name: 'Выходное интервью',
      description: 'Опрос для уходящих сотрудников, направленный на выявление причин ухода и сбор отзывов о компании.',
      icon: 'exit-icon',
      recommendedFrequency: 'При увольнении сотрудника',
      recommendedQuestions: '15-20 вопросов'
    }
  ];
  
  return (
    <div className="survey-type-select">
      <h3>Выберите тип опроса</h3>
      
      <div className="survey-types-grid">
        {surveyTypes.map(type => (
          <div 
            key={type.id}
            className="survey-type-card"
            onClick={() => onTypeSelect(type.id)}
          >
            <div className={`type-icon ${type.icon}`}></div>
            <h4 className="type-name">{type.name}</h4>
            <p className="type-description">{type.description}</p>
            
            <div className="type-recommendations">
              <div className="recommendation">
                <span className="label">Рекомендуемая частота:</span>
                <span className="value">{type.recommendedFrequency}</span>
              </div>
              
              <div className="recommendation">
                <span className="label">Рекомендуемое количество вопросов:</span>
                <span className="value">{type.recommendedQuestions}</span>
              </div>
            </div>
            
            <button className="select-type-button">Выбрать</button>
          </div>
        ))}
      </div>
      
      <div className="type-selection-help">
        <p>
          Не уверены, какой тип опроса выбрать? Регулярные опросы подходят для комплексной оценки, 
          пульс-опросы — для быстрой обратной связи по конкретным вопросам.
        </p>
      </div>
    </div>
  );
};

Форма опроса (SurveyForm)

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

// Пример компонента SurveyForm из src/components/surveys/SurveyForm.tsx
export const SurveyForm = ({ 
  survey, 
  onSave, 
  onCancel,
  isEditing = false
}) => {
  const [formData, setFormData] = useState(
    survey || {
      title: '',
      description: '',
      type: '',
      questions: [],
      respondents: [],
      startDate: null,
      endDate: null,
      status: 'DRAFT'
    }
  );
  
  const [errors, setErrors] = useState({});
  
  // Обработчик изменения полей формы
  const handleChange = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Сброс ошибки для измененного поля
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };
  
  // Обработчик добавления вопроса
  const handleAddQuestion = (question) => {
    setFormData(prev => ({
      ...prev,
      questions: [...prev.questions, {
        id: `temp-${Date.now()}`,
        ...question
      }]
    }));
  };
  
  // Обработчик удаления вопроса
  const handleRemoveQuestion = (index) => {
    setFormData(prev => {
      const newQuestions = [...prev.questions];
      newQuestions.splice(index, 1);
      return {
        ...prev,
        questions: newQuestions
      };
    });
  };
  
  // Обработчик выбора респондентов
  const handleRespondentsChange = (selectedRespondents) => {
    setFormData(prev => ({
      ...prev,
      respondents: selectedRespondents
    }));
  };
  
  // Обработчик установки дат опроса
  const handleDateChange = (field, date) => {
    setFormData(prev => ({
      ...prev,
      [field]: date
    }));
  };
  
  // Валидация данных формы
  const handleSubmit = () => {
    // Валидация данных текущего шага
    const validationErrors = {};
    
    if (!formData.title.trim()) {
      validationErrors.title = 'Название опроса обязательно';
    }
    
    if (!formData.type) {
      validationErrors.type = 'Выберите тип опроса';
    }
    
    if (formData.questions.length === 0) {
      validationErrors.questions = 'Добавьте хотя бы один вопрос';
    }
    
    if (formData.respondents.length === 0) {
      validationErrors.respondents = 'Выберите получателей опроса';
    }
    
    if (!formData.startDate) {
      validationErrors.startDate = 'Выберите дату начала опроса';
    }
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    // Если валидация прошла успешно, вызываем функцию сохранения
    onSave({
      ...formData,
      updatedAt: new Date().toISOString()
    });
  };
  
  return (
    <div className="survey-form">
      <div className="form-section">
        <h3>Основная информация</h3>
        
        <div className="form-group">
          <label>Название опроса *</label>
          <input 
            type="text"
            value={formData.title}
            onChange={(e) => handleChange('title', e.target.value)}
            placeholder="Введите название опроса"
            className={errors.title ? 'error' : ''}
          />
          {errors.title && <div className="error-message">{errors.title}</div>}
        </div>
        
        <div className="form-group">
          <label>Описание</label>
          <textarea 
            value={formData.description}
            onChange={(e) => handleChange('description', e.target.value)}
            placeholder="Введите описание опроса (необязательно)"
            rows={4}
          />
        </div>
        
        <div className="form-group">
          <label>Тип опроса *</label>
          <select 
            value={formData.type}
            onChange={(e) => handleChange('type', e.target.value)}
            className={errors.type ? 'error' : ''}
          >
            <option value="">Выберите тип опроса</option>
            <option value="REGULAR">Регулярный опрос</option>
            <option value="PULSE">Пульс-опрос</option>
            <option value="ONBOARDING">Онбординг-опрос</option>
            <option value="EXIT">Выходное интервью</option>
          </select>
          {errors.type && <div className="error-message">{errors.type}</div>}
        </div>
      </div>
      
      <div className="form-section">
        <h3>Вопросы</h3>
        
        {formData.questions.length === 0 ? (
          <div className="no-questions">
            <p>Вопросы не добавлены. Используйте кнопку ниже, чтобы добавить вопросы в опрос.</p>
          </div>
        ) : (
          <div className="questions-list">
            {formData.questions.map((question, index) => (
              <div key={question.id} className="question-item">
                <div className="question-number">{index + 1}</div>
                <div className="question-content">
                  <div className="question-text">{question.text}</div>
                  <div className="question-type">{question.type}</div>
                </div>
                <div className="question-actions">
                  <button onClick={() => handleUpdateQuestion(index, question)}>
                    Редактировать
                  </button>
                  <button onClick={() => handleRemoveQuestion(index)}>
                    Удалить
                  </button>
                </div>
              </div>
            ))}
          </div>
        )}
        
        <button 
          className="add-question-button"
          onClick={() => {/* Открытие модального окна добавления вопроса */}}
        >
          Добавить вопрос
        </button>
      </div>
      
      <div className="form-section">
        <h3>Получатели опроса</h3>
        
        <RespondentSearch 
          selectedRespondents={formData.respondents}
          onChange={(respondents) => handleChange('respondents', respondents)}
        />
        
        {errors.respondents && <div className="error-message">{errors.respondents}</div>}
      </div>
      
      <div className="form-section">
        <h3>Расписание</h3>
        
        <div className="form-row">
          <div className="form-group">
            <label>Дата начала *</label>
            <SurveyCalendar 
              selectedDate={formData.startDate}
              onChange={(date) => handleDateChange('startDate', date)}
            />
            {errors.startDate && <div className="error-message">{errors.startDate}</div>}
          </div>
          
          <div className="form-group">
            <label>Дата окончания</label>
            <SurveyCalendar 
              selectedDate={formData.endDate}
              onChange={(date) => handleDateChange('endDate', date)}
              minDate={formData.startDate}
            />
          </div>
        </div>
      </div>
      
      <div className="form-actions">
        <button 
          className="cancel-button"
          onClick={onCancel}
        >
          Отмена
        </button>
        
        <div className="save-options">
          <button 
            className="save-draft-button"
            onClick={() => {
              handleSubmit();
              // Сохранение как черновик
            }}
          >
            Сохранить как черновик
          </button>
          
          <button 
            className="activate-button"
            onClick={() => {
              handleSubmit();
              // Активация опроса
            }}
          >
            {isEditing ? 'Обновить и активировать' : 'Активировать опрос'}
          </button>
        </div>
      </div>
    </div>
  );
};

Поиск респондентов (RespondentSearch)

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

// Пример компонента RespondentSearch из src/components/surveys/RespondentSearch.tsx
export const RespondentSearch = ({ selectedRespondents = [], onChange }) => {
  const [searchQuery, setSearchQuery] = useState('');
  const [activeTab, setActiveTab] = useState('departments'); // 'departments', 'teams', 'employees'
  const [searchResults, setSearchResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  
  // Эффект для выполнения поиска при изменении запроса
  useEffect(() => {
    const fetchResults = async () => {
      if (!searchQuery.trim()) {
        setSearchResults([]);
        return;
      }
      
      setIsLoading(true);
      
      try {
        // API-запрос в зависимости от активной вкладки
        let results;
        switch (activeTab) {
          case 'departments':
            results = await fetch(`/api/departments?search=${searchQuery}`).then(res => res.json());
            break;
          case 'teams':
            results = await fetch(`/api/teams?search=${searchQuery}`).then(res => res.json());
            break;
          case 'employees':
            results = await fetch(`/api/employees?search=${searchQuery}`).then(res => res.json());
            break;
          default:
            results = [];
        }
        
        setSearchResults(results);
      } catch (error) {
        console.error('Error searching respondents:', error);
        setSearchResults([]);
      } finally {
        setIsLoading(false);
      }
    };
    
    // Задержка для предотвращения слишком частых запросов
    const timeoutId = setTimeout(fetchResults, 300);
    
    return () => clearTimeout(timeoutId);
  }, [searchQuery, activeTab]);
  
  // Проверка, выбран ли элемент
  const isSelected = (item) => {
    return selectedRespondents.some(
      respondent => respondent.id === item.id && respondent.type === activeTab
    );
  };
  
  // Обработчик выбора/отмены выбора элемента
  const toggleSelection = (item) => {
    if (isSelected(item)) {
      // Удаление из выбранных
      onChange(
        selectedRespondents.filter(
          respondent => !(respondent.id === item.id && respondent.type === activeTab)
        )
      );
    } else {
      // Добавление к выбранным
      onChange([
        ...selectedRespondents,
        { id: item.id, name: item.name, type: activeTab }
      ]);
    }
  };
  
  // Обработчик удаления выбранного элемента
  const handleRemoveSelected = (respondent) => {
    onChange(
      selectedRespondents.filter(
        r => !(r.id === respondent.id && r.type === respondent.type)
      )
    );
  };
  
  return (
    <div className="respondent-search">
      <div className="search-container">
        <div className="search-tabs">
          <button 
            className={`tab-button ${activeTab === 'departments' ? 'active' : ''}`}
            onClick={() => setActiveTab('departments')}
          >
            Отделы
          </button>
          
          <button 
            className={`tab-button ${activeTab === 'teams' ? 'active' : ''}`}
            onClick={() => setActiveTab('teams')}
          >
            Команды
          </button>
          
          <button 
            className={`tab-button ${activeTab === 'employees' ? 'active' : ''}`}
            onClick={() => setActiveTab('employees')}
          >
            Сотрудники
          </button>
        </div>
        
        <div className="search-input-container">
          <input 
            type="text"
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            placeholder={`Поиск ${
              activeTab === 'departments' ? 'отделов' : 
              activeTab === 'teams' ? 'команд' : 'сотрудников'
            }...`}
          />
          
          {searchQuery && (
            <button 
              className="clear-search"
              onClick={() => setSearchQuery('')}
            >
              ×
            </button>
          )}
        </div>
        
        <div className="search-results">
          {isLoading ? (
            <div className="loading-indicator">Загрузка...</div>
          ) : searchResults.length > 0 ? (
            <ul className="results-list">
              {searchResults.map(item => (
                <li 
                  key={item.id}
                  className={`result-item ${isSelected(item) ? 'selected' : ''}`}
                  onClick={() => toggleSelection(item)}
                >
                  <span className="item-name">{item.name}</span>
                  
                  {isSelected(item) ? (
                    <span className="selection-indicator">✓</span>
                  ) : (
                    <span className="selection-add">+</span>
                  )}
                </li>
              ))}
            </ul>
          ) : searchQuery ? (
            <div className="no-results">
              Ничего не найдено. Попробуйте изменить запрос.
            </div>
          ) : null}
        </div>
      </div>
      
      <div className="selected-respondents">
        <h4>Выбранные получатели ({selectedRespondents.length}):</h4>
        
        {selectedRespondents.length === 0 ? (
          <div className="no-selected">
            Получатели не выбраны. Используйте поиск для выбора получателей опроса.
          </div>
        ) : (
          <ul className="selected-list">
            {selectedRespondents.map(respondent => (
              <li key={`${respondent.type}-${respondent.id}`} className="selected-item">
                <span className="item-type">
                  {respondent.type === 'departments' ? 'Отдел' : 
                   respondent.type === 'teams' ? 'Команда' : 'Сотрудник'}:
                </span>
                
                <span className="item-name">{respondent.name}</span>
                
                <button 
                  className="remove-item"
                  onClick={() => handleRemoveSelected(respondent)}
                >
                  ×
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
};

Календарь опроса (SurveyCalendar)

Компонент SurveyCalendar предоставляет интерфейс для выбора даты начала и окончания опроса:

// Пример компонента SurveyCalendar из src/components/surveys/SurveyCalendar.tsx
export const SurveyCalendar = ({ selectedDate, onChange, minDate }) => {
  const [currentMonth, setCurrentMonth] = useState(
    selectedDate ? new Date(selectedDate) : new Date()
  );
  
  // Получение дней текущего месяца
  const getDaysInMonth = (year, month) => {
    return new Date(year, month + 1, 0).getDate();
  };
  
  // Получение дня недели для первого дня месяца
  const getFirstDayOfMonth = (year, month) => {
    return new Date(year, month, 1).getDay();
  };
  
  // Проверка, является ли дата выбранной
  const isSelectedDate = (date) => {
    if (!selectedDate) return false;
    
    const selected = new Date(selectedDate);
    return (
      date.getDate() === selected.getDate() &&
      date.getMonth() === selected.getMonth() &&
      date.getFullYear() === selected.getFullYear()
    );
  };
  
  // Проверка, доступен ли день для выбора
  const isDateDisabled = (date) => {
    if (minDate) {
      const min = new Date(minDate);
      min.setHours(0, 0, 0, 0);
      return date < min;
    }
    
    // По умолчанию считаем, что прошедшие дни недоступны
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    return date < today;
  };
  
  // Обработчик выбора даты
  const handleDateClick = (date) => {
    if (isDateDisabled(date)) return;
    onChange(date.toISOString());
  };
  
  // Обработчик переключения месяца
  const handleMonthChange = (increment) => {
    setCurrentMonth(prevMonth => {
      const newMonth = new Date(prevMonth);
      newMonth.setMonth(newMonth.getMonth() + increment);
      return newMonth;
    });
  };
  
  // Формирование календарной сетки
  const renderCalendarGrid = () => {
    const year = currentMonth.getFullYear();
    const month = currentMonth.getMonth();
    const daysInMonth = getDaysInMonth(year, month);
    const firstDay = getFirstDayOfMonth(year, month);
    
    const days = [];
    
    // Заполнение пустыми ячейками до первого дня месяца
    for (let i = 0; i < firstDay; i++) {
      days.push(<div key={`empty-${i}`} className="calendar-day empty"></div>);
    }
    
    // Заполнение днями месяца
    for (let day = 1; day <= daysInMonth; day++) {
      const date = new Date(year, month, day);
      const isDisabled = isDateDisabled(date);
      const isSelected = isSelectedDate(date);
      
      days.push(
        <div 
          key={`day-${day}`}
          className={`calendar-day ${isDisabled ? 'disabled' : ''} ${isSelected ? 'selected' : ''}`}
          onClick={() => !isDisabled && handleDateClick(date)}
        >
          {day}
        </div>
      );
    }
    
    return days;
  };
  
  // Форматирование названия месяца и года
  const getMonthYearHeader = () => {
    const options = { month: 'long', year: 'numeric' };
    return currentMonth.toLocaleDateString('ru-RU', options);
  };
  
  return (
    <div className="survey-calendar">
      <div className="calendar-header">
        <button 
          className="month-nav prev"
          onClick={() => handleMonthChange(-1)}
        >
          <
        </button>
        
        <div className="current-month">
          {getMonthYearHeader()}
        </div>
        
        <button 
          className="month-nav next"
          onClick={() => handleMonthChange(1)}
        >
          >
        </button>
      </div>
      
      <div className="weekdays">
        <div>Пн</div>
        <div>Вт</div>
        <div>Ср</div>
        <div>Чт</div>
        <div>Пт</div>
        <div>Сб</div>
        <div>Вс</div>
      </div>
      
      <div className="calendar-grid">
        {renderCalendarGrid()}
      </div>
      
      {selectedDate && (
        <div className="selected-date-display">
          Выбранная дата: {new Date(selectedDate).toLocaleDateString('ru-RU')}
        </div>
      )}
    </div>
  );
};

Типы вопросов

Система опросов HRoom поддерживает различные типы вопросов для сбора разнообразной обратной связи:

1. Оценка по шкале (RATING)

Позволяет респондентам оценить утверждение по шкале (обычно от 1 до 5 или от 1 до 10). Пример: "Оцените от 1 до 5, насколько вы удовлетворены рабочей атмосферой в команде".

2. Единственный выбор (SINGLE_CHOICE)

Вопрос с набором вариантов ответа, из которых респондент может выбрать только один. Пример: "Как часто вы получаете обратную связь от руководителя?" с вариантами "Ежедневно", "Еженедельно", "Ежемесячно", "Редко", "Никогда".

3. Множественный выбор (MULTIPLE_CHOICE)

Вопрос с набором вариантов ответа, из которых респондент может выбрать несколько. Пример: "Какие факторы больше всего влияют на вашу продуктивность?" с различными вариантами ответов.

4. Открытый вопрос (OPEN_TEXT)

Вопрос, на который респондент может дать произвольный текстовый ответ. Пример: "Что бы вы изменили в рабочем процессе вашей команды?"

5. Матрица оценки (MATRIX)

Группа связанных утверждений, каждое из которых оценивается по одинаковой шкале. Пример: несколько утверждений о работе руководителя, каждое из которых оценивается от 1 до 5.

6. Шкала согласия (LIKERT_SCALE)

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

7. Числовой ввод (NUMERIC)

Вопрос, требующий ввода числового значения. Пример: "Сколько часов в неделю вы обычно работаете сверхурочно?"

8. Ранжирование (RANKING)

Вопрос, требующий расположить варианты в порядке приоритета. Пример: "Расположите следующие факторы мотивации в порядке важности для вас".

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

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

// Пример сервиса для работы с опросами из src/services/surveyService.ts
import { Survey, SurveyResponse } from '../types/survey';

export const surveyService = {
  // Получение списка опросов
  async getSurveys(filters = {}) {
    try {
      const queryParams = new URLSearchParams();
      
      // Добавление параметров фильтрации
      Object.entries(filters).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          queryParams.append(key, value.toString());
        }
      });
      
      const response = await fetch(`/api/surveys?${queryParams.toString()}`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        }
      });
      
      if (!response.ok) {
        throw new Error('Failed to fetch surveys');
      }
      
      return await response.json();
    } catch (error) {
      console.error('Error fetching surveys:', error);
      throw error;
    }
  },
  
  // Получение опроса по ID
  async getSurveyById(id) {
    try {
      const response = await fetch(`/api/surveys/${id}`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        }
      });
      
      if (!response.ok) {
        throw new Error(`Failed to fetch survey with ID ${id}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error fetching survey ${id}:`, error);
      throw error;
    }
  },
  
  // Создание нового опроса
  async createSurvey(surveyData) {
    try {
      const response = await fetch('/api/surveys', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        },
        body: JSON.stringify(surveyData)
      });
      
      if (!response.ok) {
        throw new Error('Failed to create survey');
      }
      
      return await response.json();
    } catch (error) {
      console.error('Error creating survey:', error);
      throw error;
    }
  },
  
  // Обновление существующего опроса
  async updateSurvey(id, surveyData) {
    try {
      const response = await fetch(`/api/surveys/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        },
        body: JSON.stringify(surveyData)
      });
      
      if (!response.ok) {
        throw new Error(`Failed to update survey with ID ${id}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error updating survey ${id}:`, error);
      throw error;
    }
  },
  
  // Изменение статуса опроса
  async updateSurveyStatus(id, status) {
    try {
      const response = await fetch(`/api/surveys/${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 survey with ID ${id}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error updating survey status ${id}:`, error);
      throw error;
    }
  },
  
  // Удаление опроса
  async deleteSurvey(id) {
    try {
      const response = await fetch(`/api/surveys/${id}`, {
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        }
      });
      
      if (!response.ok) {
        throw new Error(`Failed to delete survey with ID ${id}`);
      }
      
      return true;
    } catch (error) {
      console.error(`Error deleting survey ${id}:`, error);
      throw error;
    }
  },
  
  // Получение ответов на опрос
  async getSurveyResponses(surveyId) {
    try {
      const response = await fetch(`/api/surveys/${surveyId}/responses`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        }
      });
      
      if (!response.ok) {
        throw new Error(`Failed to fetch responses for survey with ID ${surveyId}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error fetching survey responses ${surveyId}:`, error);
      throw error;
    }
  },
  
  // Получение статистики по опросу
  async getSurveyStatistics(surveyId) {
    try {
      const response = await fetch(`/api/surveys/${surveyId}/statistics`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('authToken')}`
        }
      });
      
      if (!response.ok) {
        throw new Error(`Failed to fetch statistics for survey with ID ${surveyId}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error fetching survey statistics ${surveyId}:`, error);
      throw error;
    }
  }
};

Жизненный цикл опроса

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

1. Создание (DRAFT)

На этом этапе HR-менеджер создает опрос, определяет его тип, добавляет вопросы, выбирает респондентов и устанавливает даты проведения. Опрос сохраняется в статусе "Черновик" и недоступен для прохождения.

2. Активация (ACTIVE)

Когда опрос готов, HR-менеджер активирует его. Система отправляет уведомления выбранным респондентам по электронной почте с ссылкой для прохождения опроса. В этом статусе опрос открыт для ответов.

3. Завершение (COMPLETED)

Опрос автоматически переходит в статус "Завершен" по достижении даты окончания. Также HR-менеджер может вручную завершить опрос раньше запланированной даты. После завершения опрос закрывается для новых ответов.

4. Архивация (ARCHIVED)

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

Типы данных опросов

Модуль опросов использует следующие основные типы данных:

Survey

Базовый тип для представления опроса с его свойствами:

// Из src/types/survey.ts
export type SurveyStatus = 'DRAFT' | 'ACTIVE' | 'COMPLETED' | 'ARCHIVED';

export type SurveyType = 'REGULAR' | 'PULSE' | 'ONBOARDING' | 'EXIT';

export interface Survey {
  id: string;
  title: string;
  description?: string;
  type: SurveyType;
  status: SurveyStatus;
  questions: Question[];
  startDate: string;
  endDate?: string;
  createdAt: string;
  updatedAt: string;
  respondents?: number;
  responses?: number;
}

Question

Тип для представления вопроса в опросе:

// Из src/types/question.ts
export type QuestionType = 
  | 'RATING' 
  | 'SINGLE_CHOICE' 
  | 'MULTIPLE_CHOICE' 
  | 'OPEN_TEXT' 
  | 'MATRIX' 
  | 'LIKERT_SCALE'
  | 'NUMERIC'
  | 'RANKING';

export interface Question {
  id: string;
  text: string;
  type: QuestionType;
  required: boolean;
  options?: string[];
  minValue?: number;
  maxValue?: number;
  step?: number;
  groupId?: string;
  metricId?: string;
}

SurveyResponse

Тип для представления ответа респондента на опрос:

// Из src/types/survey.ts
export interface SurveyResponse {
  id: string;
  surveyId: string;
  userId: string;
  answers: {
    questionId: string;
    value: number | string | string[];
    comment?: string;
  }[];
  submittedAt: string;
}

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

Главный компонент страницы опросов объединяет все описанные выше компоненты и функциональность:

// Пример основного компонента страницы опросов из src/pages/Surveys/index.tsx
import React, { useEffect, useState } from 'react';
import { SurveyList } from '../../components/surveys/SurveyList';
import { CreateSurveyModal } from '../../components/surveys/CreateSurveyModal';
import { Survey, SurveyStatus } from '../../types/survey';
import { surveyService } from '../../services/surveyService';
import { SurveyEmpty } from './components/EmptyStates/SurveyEmpty';

export const Surveys = () => {
  const [surveys, setSurveys] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
  const [selectedSurveyId, setSelectedSurveyId] = useState(null);
  
  // Загрузка списка опросов
  const fetchSurveys = async () => {
    try {
      setIsLoading(true);
      const surveysData = await surveyService.getSurveys();
      setSurveys(surveysData);
      setError(null);
    } catch (err) {
      console.error('Error loading surveys:', err);
      setError('Произошла ошибка при загрузке опросов. Пожалуйста, попробуйте позже.');
    } finally {
      setIsLoading(false);
    }
  };
  
  // Загрузка опросов при монтировании компонента
  useEffect(() => {
    fetchSurveys();
  }, []);
  
  // Открытие модального окна создания опроса
  const handleCreateClick = () => {
    setIsCreateModalOpen(true);
  };
  
  // Закрытие модального окна создания опроса
  const handleModalClose = () => {
    setIsCreateModalOpen(false);
  };
  
  // Обработчик сохранения нового опроса
  const handleSaveSurvey = async (surveyData) => {
    try {
      const newSurvey = await surveyService.createSurvey(surveyData);
      setSurveys(prevSurveys => [...prevSurveys, newSurvey]);
      setIsCreateModalOpen(false);
    } catch (err) {
      console.error('Error creating survey:', err);
      // Обработка ошибки
    }
  };
  
  // Обработчик клика по опросу
  const handleSurveyClick = (surveyId) => {
    setSelectedSurveyId(surveyId);
    // Навигация к детальной странице опроса или открытие модального окна с деталями
  };
  
  // Обработчик изменения статуса опроса
  const handleStatusChange = async (surveyId, newStatus) => {
    try {
      await surveyService.updateSurveyStatus(surveyId, newStatus);
      
      // Обновление состояния опроса в интерфейсе
      setSurveys(prevSurveys => 
        prevSurveys.map(survey => 
          survey.id === surveyId ? { ...survey, status: newStatus } : survey
        )
      );
    } catch (err) {
      console.error(`Error updating survey status ${surveyId}:`, err);
      // Обработка ошибки
    }
  };
  
  // Обработчик удаления опроса
  const handleDeleteSurvey = async (surveyId) => {
    try {
      await surveyService.deleteSurvey(surveyId);
      
      // Удаление опроса из списка
      setSurveys(prevSurveys => 
        prevSurveys.filter(survey => survey.id !== surveyId)
      );
    } catch (err) {
      console.error(`Error deleting survey ${surveyId}:`, err);
      // Обработка ошибки
    }
  };
  
  // Пустое состояние при отсутствии опросов
  if (!isLoading && !error && surveys.length === 0) {
    return <SurveyEmpty onCreateClick={handleCreateClick} />;
  }
  
  return (
    <div className="surveys-page">
      {error && (
        <div className="error-message">
          {error}
          <button onClick={fetchSurveys}>Повторить попытку</button>
        </div>
      )}
      
      <SurveyList 
        surveys={surveys}
        isLoading={isLoading}
        onCreateClick={handleCreateClick}
        onSurveyClick={handleSurveyClick}
        onStatusChange={handleStatusChange}
        onDeleteSurvey={handleDeleteSurvey}
      />
      
      {isCreateModalOpen && (
        <CreateSurveyModal 
          isOpen={isCreateModalOpen}
          onClose={handleModalClose}
          onSave={handleSaveSurvey}
        />
      )}
    </div>
  );
};

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

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

// Пример компонента SurveyEmpty из src/pages/Surveys/components/EmptyStates/SurveyEmpty.tsx
export const SurveyEmpty = ({ onCreateClick }) => {
  return (
    <div className="empty-surveys-container">
      <div className="empty-surveys-icon">
        {/* Иллюстрация или иконка для пустого состояния */}
      </div>
      
      <h2>У вас пока нет опросов</h2>
      
      <p>
        Создайте свой первый опрос, чтобы начать собирать ценную обратную связь от сотрудников.
        Опросы помогают оценить вовлеченность и удовлетворенность команды, выявить проблемы и найти возможности для улучшения.
      </p>
      
      <div className="survey-types-preview">
        <div className="survey-type-item">
          <h4>Регулярные опросы</h4>
          <p>Комплексная оценка вовлеченности и удовлетворенности сотрудников</p>
        </div>
        
        <div className="survey-type-item">
          <h4>Пульс-опросы</h4>
          <p>Короткие, частые опросы для мониторинга ключевых показателей</p>
        </div>
        
        <div className="survey-type-item">
          <h4>Онбординг-опросы</h4>
          <p>Оценка процесса адаптации новых сотрудников</p>
        </div>
        
        <div className="survey-type-item">
          <h4>Выходные интервью</h4>
          <p>Сбор обратной связи от уходящих сотрудников</p>
        </div>
      </div>
      
      <button 
        className="create-first-survey-button"
        onClick={onCreateClick}
      >
        Создать первый опрос
      </button>
    </div>
  );
};

Выводы

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

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

  • Гибкость - поддержка различных типов опросов для разных целей (регулярные, пульс-опросы, онбординг, выходные интервью)
  • Разнообразие вопросов - поддержка разных типов вопросов для сбора как количественных, так и качественных данных
  • Таргетирование - возможность точно выбирать получателей опросов (отделы, команды, отдельные сотрудники)
  • Планирование - инструменты для настройки расписания проведения опросов
  • Мониторинг - отслеживание статуса прохождения опросов и сбор статистики
  • Интеграция - тесная связь с другими модулями системы для комплексного анализа данных

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