179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
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;
|
||
}
|