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; }