w-funnel/src/lib/funnel/navigation.ts
dev.daminik00 6c50d05123 ab
2025-10-21 01:27:08 +02:00

179 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FunnelAnswers, NavigationConditionDefinition, NavigationRuleDefinition, ScreenDefinition } from "./types";
import { calculateAgeFromArray, createAgeValue, createGenerationValue } from "@/lib/age-utils";
import { getZodiacSign } from "@/lib/funnel/zodiac";
/**
* Тип для функции проверки Unleash условий
* Передается извне чтобы избежать зависимости от React hooks
*/
export type UnleashChecker = (
flag: string,
expectedVariants: string[],
operator?: "includesAny" | "includesAll" | "includesExactly" | "equals"
) => boolean;
/**
* Расширенная функция получения ответов экрана
* Автоматически рассчитывает возраст и знак зодиака для date экранов
*/
function getScreenAnswers(answers: FunnelAnswers, screenId: string, allScreens?: ScreenDefinition[]): string[] {
const rawAnswers = answers[screenId] ?? [];
// 🎯 ОСОБАЯ ЛОГИКА для date экранов - автоматически добавляем рассчитанные значения
const screen = allScreens?.find(s => s.id === screenId);
if (screen?.template === "date" && rawAnswers.length === 3) {
const [month, day, year] = rawAnswers.map(Number);
// Валидируем дату
if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && year >= 1900) {
const dateArray = [month, day, year];
const enhancedAnswers = [...rawAnswers];
try {
// 🎂 Добавляем возрастные значения
const age = calculateAgeFromArray(dateArray);
if (age > 0) {
enhancedAnswers.push(
createAgeValue(age), // "26-30"
`age-${age}`, // "age-25"
createGenerationValue(year) // "millennials"
);
}
// ♈ Добавляем знак зодиака
const zodiac = getZodiacSign(month, day);
if (zodiac) {
enhancedAnswers.push(zodiac); // "aries"
}
} catch (error) {
// В случае ошибки возвращаем исходные ответы
console.warn('Error calculating age/zodiac from date:', error);
}
return enhancedAnswers;
}
}
return rawAnswers;
}
function satisfiesCondition(
condition: NavigationConditionDefinition,
answers: FunnelAnswers,
allScreens?: ScreenDefinition[],
unleashChecker?: UnleashChecker
): boolean {
const conditionType = condition.conditionType ?? "options";
// 🎯 UNLEASH AB ТЕСТЫ: проверка через Unleash feature flags
if (conditionType === "unleash") {
if (!unleashChecker || !condition.unleashFlag || !condition.unleashVariants) {
return false;
}
const operator = condition.operator ?? "includesAny";
return unleashChecker(
condition.unleashFlag,
condition.unleashVariants,
operator
);
}
// Существующая логика для options и values
const selected = new Set(getScreenAnswers(answers, condition.screenId, allScreens));
const operator = condition.operator ?? "includesAny";
// 🎯 НОВАЯ ЛОГИКА: поддержка values для любых экранов
const expectedValues = conditionType === "values"
? new Set(condition.values ?? [])
: new Set(condition.optionIds ?? []);
if (expectedValues.size === 0) {
return false;
}
switch (operator) {
case "includesAny": {
return Array.from(expectedValues).some((value) => selected.has(value));
}
case "includesAll": {
return Array.from(expectedValues).every((value) => selected.has(value));
}
case "includesExactly": {
if (selected.size !== expectedValues.size) {
return false;
}
for (const value of expectedValues) {
if (!selected.has(value)) {
return false;
}
}
return true;
}
case "equals": {
// 🎯 НОВЫЙ ОПЕРАТОР: точное совпадение для одиночных значений
const selectedArray = Array.from(selected);
const expectedArray = Array.from(expectedValues);
return selectedArray.length === 1 &&
expectedArray.length === 1 &&
selectedArray[0] === expectedArray[0];
}
default:
return false;
}
}
export function matchesNavigationConditions(
conditions: NavigationConditionDefinition[] | undefined,
answers: FunnelAnswers,
allScreens?: ScreenDefinition[],
unleashChecker?: UnleashChecker
): boolean {
if (!conditions || conditions.length === 0) {
return false;
}
return conditions.every((condition) => satisfiesCondition(condition, answers, allScreens, unleashChecker));
}
function satisfiesRule(
rule: NavigationRuleDefinition,
answers: FunnelAnswers,
allScreens?: ScreenDefinition[],
unleashChecker?: UnleashChecker
): boolean {
return matchesNavigationConditions(rule.conditions, answers, allScreens, unleashChecker);
}
export function resolveNextScreenId(
currentScreen: ScreenDefinition,
answers: FunnelAnswers,
orderedScreens: ScreenDefinition[],
unleashChecker?: UnleashChecker
): string | undefined {
const navigation = currentScreen.navigation;
if (navigation?.rules) {
for (const rule of navigation.rules) {
if (satisfiesRule(rule, answers, orderedScreens, unleashChecker)) {
return rule.nextScreenId;
}
}
}
if (navigation?.defaultNextScreenId) {
return navigation.defaultNextScreenId;
}
const currentIndex = orderedScreens.findIndex((screen) => screen.id === currentScreen.id);
if (currentIndex === -1) {
return undefined;
}
const nextScreen = orderedScreens[currentIndex + 1];
return nextScreen?.id;
}