2024-04-03 12:42:12 +00:00
import { useCallback , useDebugValue , useMemo , useState } from "react" ;
2024-05-06 13:47:19 +00:00
import { enqueueSnackbar } from "notistack" ;
2024-04-03 12:42:12 +00:00
import moment from "moment" ;
2024-05-06 13:47:19 +00:00
import { isResultQuestionEmpty } from "@/components/ViewPublicationPage/tools/checkEmptyData" ;
2024-05-31 17:56:17 +00:00
import { useQuizSettings } from "@contexts/QuizDataContext" ;
2024-04-03 12:42:12 +00:00
2024-05-06 13:47:19 +00:00
import { useQuizViewStore } from "@stores/quizView" ;
2024-04-03 12:42:12 +00:00
2024-05-06 13:47:19 +00:00
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals" ;
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals" ;
2024-04-03 12:42:12 +00:00
2024-05-06 13:47:19 +00:00
export function useQuestionFlowControl() {
2024-08-28 07:02:48 +00:00
//Получаем инфо о квизе и список вопросов.
2024-05-31 17:56:17 +00:00
const { settings , questions } = useQuizSettings ( ) ;
2024-08-28 07:02:48 +00:00
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//З а корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
2024-05-06 13:47:19 +00:00
const sortedQuestions = useMemo ( ( ) = > {
return [ . . . questions ] . sort ( ( a , b ) = > a . page - b . page ) ;
} , [ questions ] ) ;
2024-08-28 07:02:48 +00:00
//React сам будет менять визуал - главное говорить из какого вопроса ему брать инфо. Изменение этой переменной меняет визуал.
2024-05-31 16:41:18 +00:00
const [ currentQuestionId , setCurrentQuestionId ] = useState < string | null > ( getFirstQuestionId ) ;
2024-08-28 07:02:48 +00:00
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
2024-05-06 13:47:19 +00:00
const answers = useQuizViewStore ( ( state ) = > state . answers ) ;
2024-08-28 07:02:48 +00:00
//Список засчитанных баллов для балловых квизов
2024-05-06 13:47:19 +00:00
const pointsSum = useQuizViewStore ( ( state ) = > state . pointsSum ) ;
2024-08-28 07:02:48 +00:00
//Текущий шаг "startpage" | "question" | "contactform"
2024-05-31 16:41:18 +00:00
const setCurrentQuizStep = useQuizViewStore ( ( state ) = > state . setCurrentQuizStep ) ;
2024-08-28 07:02:48 +00:00
//Получение возможности управлять состоянием метрик
2024-05-06 13:47:19 +00:00
const vkMetrics = useVkMetricsGoals ( settings . cfg . vkMetricsNumber ) ;
const yandexMetrics = useYandexMetricsGoals ( settings . cfg . yandexMetricsNumber ) ;
2024-08-28 07:02:48 +00:00
//Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке
2024-05-31 16:41:18 +00:00
const currentQuestion = sortedQuestions . find ( ( question ) = > question . id === currentQuestionId ) ? ? sortedQuestions [ 0 ] ;
2024-05-06 13:47:19 +00:00
2024-08-28 07:02:48 +00:00
//Индекс текущего вопроса только если квиз линейный
const linearQuestionIndex = //: number | null
2024-05-31 16:41:18 +00:00
currentQuestion && sortedQuestions . every ( ( { content } ) = > content . rule . parentId !== "root" ) // null when branching enabled
2024-05-06 13:47:19 +00:00
? sortedQuestions . indexOf ( currentQuestion )
: null ;
2024-08-28 07:02:48 +00:00
//Индекс первого вопроса
2024-05-06 13:47:19 +00:00
function getFirstQuestionId() {
2024-08-28 07:02:48 +00:00
//: string | null
if ( sortedQuestions . length === 0 ) return null ; //Если нету сортированного списка, то и не рыпаемся
2024-05-06 13:47:19 +00:00
if ( settings . cfg . haveRoot ) {
2024-08-28 07:02:48 +00:00
// Если есть ветвление, то settings.cfg.haveRoot будет заполнен
//Если заполнен, то дерево растёт с root и это 1 вопрос :)
2024-05-06 13:47:19 +00:00
const nextQuestion = sortedQuestions . find (
2024-08-28 07:02:48 +00:00
//Функция ищет первое совпадение по массиву
2024-05-31 16:41:18 +00:00
( question ) = > question . id === settings . cfg . haveRoot || question . content . id === settings . cfg . haveRoot
2024-05-06 13:47:19 +00:00
) ;
if ( ! nextQuestion ) return null ;
return nextQuestion . id ;
2024-04-03 12:42:12 +00:00
}
2024-08-28 07:02:48 +00:00
//Если не возникло исключительных ситуаций - первый вопрос - нулевой элемент сортированного массива
2024-05-06 13:47:19 +00:00
return sortedQuestions [ 0 ] . id ;
}
2024-04-12 11:29:37 +00:00
2024-05-06 13:47:19 +00:00
const nextQuestionIdPointsLogic = useCallback ( ( ) = > {
2024-05-31 16:41:18 +00:00
return sortedQuestions . find ( ( question ) = > question . type === "result" && question . content . rule . parentId === "line" ) ;
2024-05-06 13:47:19 +00:00
} , [ sortedQuestions ] ) ;
2024-04-03 12:42:12 +00:00
2024-08-28 07:02:48 +00:00
//Анализируем какой вопрос должен быть следующим. Это главная логика
2024-05-06 13:47:19 +00:00
const nextQuestionIdMainLogic = useCallback ( ( ) = > {
2024-08-28 07:02:48 +00:00
//Список ответов данных этому вопросу. Вернёт QuestionAnswer | undefined
2024-05-31 16:41:18 +00:00
const questionAnswer = answers . find ( ( { questionId } ) = > questionId === currentQuestion . id ) ;
2024-04-03 12:42:12 +00:00
2024-08-28 07:02:48 +00:00
//Если questionAnswer не undefined и ответ на вопрос не является временем:
2024-05-06 13:47:19 +00:00
if ( questionAnswer && ! moment . isMoment ( questionAnswer . answer ) ) {
2024-08-28 07:02:48 +00:00
//Вопрос типизации. Получаем список строк ответов на этот вопрос
2024-05-31 16:41:18 +00:00
const userAnswers = Array . isArray ( questionAnswer . answer ) ? questionAnswer . answer : [ questionAnswer . answer ] ;
2024-04-03 12:42:12 +00:00
2024-08-28 07:02:48 +00:00
//цикл. Перебираем список условий .main и обзываем их переменной branchingRule
2024-05-06 13:47:19 +00:00
for ( const branchingRule of currentQuestion . content . rule . main ) {
2024-08-28 07:02:48 +00:00
// Перебираем список ответов. Если хоть один ответ из списка совпадает с прописанным правилом из условий - этот вопрос нужный нам. Е г о и дадимкак следующий
2024-05-31 16:41:18 +00:00
if ( userAnswers . some ( ( answer ) = > branchingRule . rules [ 0 ] . answers . includes ( answer ) ) ) {
2024-05-06 13:47:19 +00:00
return branchingRule . next ;
2024-04-03 12:42:12 +00:00
}
2024-05-06 13:47:19 +00:00
}
}
2024-04-03 12:42:12 +00:00
2024-08-28 07:02:48 +00:00
//Н е помню что это, но чёт при первом взгляде оно true только у результатов
2024-05-06 13:47:19 +00:00
if ( ! currentQuestion . required ) {
2024-08-28 07:02:48 +00:00
//Готовим с е б е дефолтный путь
2024-05-06 13:47:19 +00:00
const defaultNextQuestionId = currentQuestion . content . rule . default ;
2024-08-28 07:02:48 +00:00
//Если строка не пустая и не пробел. (Обычно при получении данных мы сразу чистим пустые строки только с пробелом на просто пустые строки. Это прост доп защита)
2024-05-31 16:41:18 +00:00
if ( defaultNextQuestionId . length > 1 && defaultNextQuestionId !== " " ) return defaultNextQuestionId ;
2024-05-06 13:47:19 +00:00
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
[ "date" , "page" , "text" , "number" ] . includes ( currentQuestion . type ) &&
currentQuestion . content . rule . children . length === 1
)
return currentQuestion . content . rule . children [ 0 ] ;
}
2024-04-16 19:20:57 +00:00
2024-05-06 13:47:19 +00:00
//ничё не нашли, ищем резулт
return sortedQuestions . find ( ( q ) = > {
2024-05-31 16:41:18 +00:00
return q . type === "result" && q . content . rule . parentId === currentQuestion . content . id ;
2024-05-06 13:47:19 +00:00
} ) ? . id ;
} , [ answers , currentQuestion , sortedQuestions ] ) ;
2024-08-28 07:02:48 +00:00
//Анализ следующего вопроса. Это логика для вопроса с баллами
2024-05-06 13:47:19 +00:00
const nextQuestionId = useMemo ( ( ) = > {
if ( settings . cfg . score ) {
return nextQuestionIdPointsLogic ( ) ;
}
return nextQuestionIdMainLogic ( ) ;
} , [ nextQuestionIdMainLogic , nextQuestionIdPointsLogic , settings . cfg . score ] ) ;
2024-08-28 07:02:48 +00:00
//Поиск предыдущго вопроса либо по индексу либо по id родителя
2024-05-06 13:47:19 +00:00
const prevQuestion =
linearQuestionIndex !== null
? sortedQuestions [ linearQuestionIndex - 1 ]
: sortedQuestions . find (
( q ) = >
2024-05-31 16:41:18 +00:00
q . id === currentQuestion ? . content . rule . parentId || q . content . id === currentQuestion ? . content . rule . parentId
2024-04-03 12:42:12 +00:00
) ;
2024-08-28 07:02:48 +00:00
//Анализ результата по количеству баллов
2024-05-06 13:47:19 +00:00
const findResultPointsLogic = useCallback ( ( ) = > {
2024-08-28 07:02:48 +00:00
//Отбираем из массива только тип резулт И результы с информацией о ожидаемых баллах И те результы, чьи суммы баллов меньше или равны насчитанным баллам юзера
2024-05-06 13:47:19 +00:00
const results = sortedQuestions . filter (
2024-05-31 16:41:18 +00:00
( e ) = > e . type === "result" && e . content . rule . minScore !== undefined && e . content . rule . minScore <= pointsSum
2024-05-06 13:47:19 +00:00
) ;
2024-08-28 07:02:48 +00:00
//Создаём массив строк из результатов. У кого есть инфо о баллах - дают свои, остальные 0
2024-05-06 13:47:19 +00:00
const numbers = results . map ( ( e ) = >
2024-05-31 16:41:18 +00:00
e . type === "result" && e . content . rule . minScore !== undefined ? e.content.rule.minScore : 0
2024-05-06 13:47:19 +00:00
) ;
2024-08-28 07:02:48 +00:00
//Извлекаем самое большое число
2024-05-06 13:47:19 +00:00
const indexOfNext = Math . max ( . . . numbers ) ;
2024-08-28 07:02:48 +00:00
//Отдаём индекс нужного нам результата
2024-05-06 13:47:19 +00:00
return results [ numbers . indexOf ( indexOfNext ) ] ;
} , [ pointsSum , sortedQuestions ] ) ;
2024-08-28 07:02:48 +00:00
//Ищем следующий вопрос (не е г о индекс, или id). Сам вопрос
2024-05-06 13:47:19 +00:00
const nextQuestion = useMemo ( ( ) = > {
let next ;
2024-08-28 07:02:48 +00:00
2024-05-06 13:47:19 +00:00
if ( settings . cfg . score ) {
2024-08-28 07:02:48 +00:00
//Ессли квиз балловый
2024-05-06 13:47:19 +00:00
if ( linearQuestionIndex !== null ) {
2024-08-28 07:02:48 +00:00
next = sortedQuestions [ linearQuestionIndex + 1 ] ; //ищем по индексу
if ( next ? . type === "result" || next == undefined ) next = findResultPointsLogic ( ) ; //если в поисках пришли к результату - считаем нужный
2024-05-06 13:47:19 +00:00
}
} else {
2024-08-28 07:02:48 +00:00
//иначе
2024-05-06 13:47:19 +00:00
if ( linearQuestionIndex !== null ) {
2024-08-28 07:02:48 +00:00
//для линейных ищем по индексу
2024-05-06 13:47:19 +00:00
next =
sortedQuestions [ linearQuestionIndex + 1 ] ? ?
2024-05-31 16:41:18 +00:00
sortedQuestions . find ( ( question ) = > question . type === "result" && question . content . rule . parentId === "line" ) ;
2024-05-06 13:47:19 +00:00
} else {
2024-08-28 07:02:48 +00:00
// для нелинейных ищем по вычесленному id
2024-05-31 16:41:18 +00:00
next = sortedQuestions . find ( ( q ) = > q . id === nextQuestionId || q . content . id === nextQuestionId ) ;
2024-05-06 13:47:19 +00:00
}
}
2024-04-16 19:20:57 +00:00
2024-05-06 13:47:19 +00:00
return next ;
2024-05-31 16:41:18 +00:00
} , [ nextQuestionId , findResultPointsLogic , linearQuestionIndex , sortedQuestions , settings . cfg . score ] ) ;
2024-05-06 13:47:19 +00:00
2024-08-28 07:02:48 +00:00
//Показать визуалом юзеру результат
2024-05-06 13:47:19 +00:00
const showResult = useCallback ( ( ) = > {
2024-05-31 16:41:18 +00:00
if ( nextQuestion ? . type !== "result" ) throw new Error ( "Current question is not result" ) ;
2024-05-06 13:47:19 +00:00
2024-08-28 07:02:48 +00:00
//Записать в переменную ид текущего вопроса
2024-05-06 13:47:19 +00:00
setCurrentQuestionId ( nextQuestion . id ) ;
2024-08-28 07:02:48 +00:00
//Смотрим по настройкам показывать ли вообще форму контактов. Показывать ли страницу результатов до или после формы контактов (ФК)
2024-06-02 11:08:09 +00:00
if (
settings . cfg . showfc !== false &&
( settings . cfg . resultInfo . showResultForm === "after" || isResultQuestionEmpty ( nextQuestion ) )
)
2024-05-06 13:47:19 +00:00
setCurrentQuizStep ( "contactform" ) ;
2024-06-02 11:08:09 +00:00
} , [ nextQuestion , setCurrentQuizStep , settings . cfg . resultInfo . showResultForm , settings . cfg . showfc ] ) ;
2024-05-06 13:47:19 +00:00
2024-08-28 07:02:48 +00:00
//рычаг управления из визуала в эту функцию
2024-05-06 13:47:19 +00:00
const showResultAfterContactForm = useCallback ( ( ) = > {
2024-05-31 16:41:18 +00:00
if ( currentQuestion ? . type !== "result" ) throw new Error ( "Current question is not result" ) ;
2024-05-06 13:47:19 +00:00
if ( isResultQuestionEmpty ( currentQuestion ) ) {
enqueueSnackbar ( "Данные отправлены" ) ;
return ;
}
2024-04-16 19:20:57 +00:00
2024-05-06 13:47:19 +00:00
setCurrentQuizStep ( "question" ) ;
} , [ currentQuestion , setCurrentQuizStep ] ) ;
2024-04-03 12:42:12 +00:00
2024-08-28 07:02:48 +00:00
//рычаг управления из визуала в эту функцию
2024-05-06 13:47:19 +00:00
const moveToPrevQuestion = useCallback ( ( ) = > {
if ( ! prevQuestion ) throw new Error ( "Previous question not found" ) ;
2024-04-03 12:42:12 +00:00
2024-05-06 13:47:19 +00:00
setCurrentQuestionId ( prevQuestion . id ) ;
} , [ prevQuestion ] ) ;
2024-08-28 07:02:48 +00:00
//рычаг управления из визуала в эту функцию
2024-05-06 13:47:19 +00:00
const moveToNextQuestion = useCallback ( ( ) = > {
if ( ! nextQuestion ) throw new Error ( "Next question not found" ) ;
// Засчитываем переход с вопроса дальше
vkMetrics . questionPassed ( currentQuestion . id ) ;
yandexMetrics . questionPassed ( currentQuestion . id ) ;
2024-05-13 09:09:15 +00:00
if ( nextQuestion . type === "result" ) return showResult ( ) ;
2024-05-06 13:47:19 +00:00
setCurrentQuestionId ( nextQuestion . id ) ;
2024-06-02 10:23:54 +00:00
} , [ currentQuestion . id , nextQuestion , showResult , vkMetrics , yandexMetrics ] ) ;
2024-05-06 13:47:19 +00:00
2024-08-28 07:02:48 +00:00
//рычаг управления из визуала в эту функцию
2024-05-06 13:47:19 +00:00
const setQuestion = useCallback (
( questionId : string ) = > {
const question = sortedQuestions . find ( ( q ) = > q . id === questionId ) ;
if ( ! question ) return ;
setCurrentQuestionId ( question . id ) ;
} ,
[ sortedQuestions ]
) ;
2024-08-28 07:02:48 +00:00
//Анализ дисаблить ли кнопки навигации
2024-05-06 13:47:19 +00:00
const isPreviousButtonEnabled = Boolean ( prevQuestion ) ;
2024-08-28 07:02:48 +00:00
//Анализ дисаблить ли кнопки навигации
2024-05-06 13:47:19 +00:00
const isNextButtonEnabled = useMemo ( ( ) = > {
2024-05-31 16:41:18 +00:00
const hasAnswer = answers . some ( ( { questionId } ) = > questionId === currentQuestion . id ) ;
2024-05-06 13:47:19 +00:00
2024-05-31 16:41:18 +00:00
if ( "required" in currentQuestion . content && currentQuestion . content . required ) {
2024-05-06 13:47:19 +00:00
return hasAnswer ;
}
2024-04-03 12:42:12 +00:00
2024-05-06 13:47:19 +00:00
return Boolean ( nextQuestion ) ;
} , [ answers , currentQuestion , nextQuestion ] ) ;
useDebugValue ( {
linearQuestionIndex ,
currentQuestion : currentQuestion ,
prevQuestion : prevQuestion ,
nextQuestion : nextQuestion ,
} ) ;
return {
currentQuestion ,
2024-05-31 16:41:18 +00:00
currentQuestionStepNumber : linearQuestionIndex === null ? null : linearQuestionIndex + 1 ,
2024-06-22 15:35:11 +00:00
nextQuestion ,
2024-05-06 13:47:19 +00:00
isNextButtonEnabled ,
isPreviousButtonEnabled ,
moveToPrevQuestion ,
moveToNextQuestion ,
showResultAfterContactForm ,
setQuestion ,
} ;
}