import type { ListScreenDefinition } from "@/lib/funnel/types"; import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { BuilderState, BuilderAction } from "./types"; import { INITIAL_STATE } from "./constants"; import { withDirty, generateScreenId, createScreenByTemplate } from "./utils"; /** * Обновляет все ссылки на oldScreenId на newScreenId во всех экранах */ function updateScreenIdReferences( screens: BuilderScreen[], oldScreenId: string, newScreenId: string ): BuilderScreen[] { return screens.map((screen) => { let updated = false; const updatedScreen = { ...screen }; // Обновляем navigation.defaultNextScreenId if (updatedScreen.navigation?.defaultNextScreenId === oldScreenId) { updatedScreen.navigation = { ...updatedScreen.navigation, defaultNextScreenId: newScreenId, }; updated = true; } // Обновляем navigation.rules[].nextScreenId if (updatedScreen.navigation?.rules && updatedScreen.navigation.rules.length > 0) { const updatedRules = updatedScreen.navigation.rules.map((rule) => { if (rule.nextScreenId === oldScreenId) { return { ...rule, nextScreenId: newScreenId }; } return rule; }); if (updatedRules.some((rule, index) => rule !== updatedScreen.navigation!.rules![index])) { updatedScreen.navigation = { ...updatedScreen.navigation, rules: updatedRules, }; updated = true; } } // Обновляем variants[].conditions[].screenId if (updatedScreen.variants && updatedScreen.variants.length > 0) { const updatedVariants = updatedScreen.variants.map((variant) => { if (!variant.conditions) return variant; const updatedConditions = variant.conditions.map((condition) => { if (condition.screenId === oldScreenId) { return { ...condition, screenId: newScreenId }; } return condition; }); if (updatedConditions.some((cond, index) => cond !== variant.conditions![index])) { return { ...variant, conditions: updatedConditions }; } return variant; }); if (updatedVariants.some((v, index) => v !== updatedScreen.variants![index])) { updatedScreen.variants = updatedVariants; updated = true; } } return updated ? updatedScreen : screen; }); } export function builderReducer(state: BuilderState, action: BuilderAction): BuilderState { switch (action.type) { case "set-meta": { return withDirty(state, { ...state, meta: { ...state.meta, ...action.payload, }, }); } case "set-default-texts": { return withDirty(state, { ...state, defaultTexts: { ...state.defaultTexts, ...action.payload, }, }); } case "add-screen": { const nextId = generateScreenId(state.screens.map((s) => s.id)); const template = action.payload?.template || "list"; const newScreen = createScreenByTemplate(template, nextId); // 🎯 АВТОМАТИЧЕСКОЕ СВЯЗЫВАНИЕ С ПРЕДЫДУЩИМ ЭКРАНОМ let updatedScreens = [...state.screens, newScreen]; // Если есть предыдущий экран и у него нет defaultNextScreenId, связываем с новым if (state.screens.length > 0) { const lastScreen = state.screens[state.screens.length - 1]; if (!lastScreen.navigation?.defaultNextScreenId) { // Обновляем предыдущий экран, чтобы он указывал на новый updatedScreens = updatedScreens.map(screen => screen.id === lastScreen.id ? { ...screen, navigation: { ...screen.navigation, defaultNextScreenId: nextId, } } : screen ); } } return withDirty(state, { ...state, screens: updatedScreens, selectedScreenId: newScreen.id, meta: { ...state.meta, firstScreenId: state.meta.firstScreenId ?? newScreen.id, }, }); } case "insert-screen": { const { atIndex, template = "list" } = action.payload; const nextId = generateScreenId(state.screens.map((s) => s.id)); const newScreen = createScreenByTemplate(template, nextId); // Вставляем экран на указанную позицию const updatedScreens = [...state.screens]; updatedScreens.splice(atIndex, 0, newScreen); // 🎯 АВТОМАТИЧЕСКОЕ СВЯЗЫВАНИЕ // Обновляем навигацию предыдущего экрана (если есть) if (atIndex > 0) { const prevScreen = updatedScreens[atIndex - 1]; const nextScreen = updatedScreens[atIndex + 1]; // Если предыдущий экран указывал на следующий, перенаправляем через новый if (nextScreen && prevScreen.navigation?.defaultNextScreenId === nextScreen.id) { updatedScreens[atIndex - 1] = { ...prevScreen, navigation: { ...prevScreen.navigation, defaultNextScreenId: nextId, }, }; // Новый экран указывает на следующий updatedScreens[atIndex] = { ...newScreen, navigation: { ...newScreen.navigation, defaultNextScreenId: nextScreen.id, }, }; } else if (!prevScreen.navigation?.defaultNextScreenId) { // Если у предыдущего нет перехода, связываем с новым updatedScreens[atIndex - 1] = { ...prevScreen, navigation: { ...prevScreen.navigation, defaultNextScreenId: nextId, }, }; } } return withDirty(state, { ...state, screens: updatedScreens, selectedScreenId: newScreen.id, meta: { ...state.meta, firstScreenId: atIndex === 0 ? newScreen.id : state.meta.firstScreenId ?? newScreen.id, }, }); } case "remove-screen": { const filtered = state.screens.filter((screen) => screen.id !== action.payload.screenId); const selectedScreenId = state.selectedScreenId === action.payload.screenId ? filtered[0]?.id ?? null : state.selectedScreenId; const nextMeta = { ...state.meta, firstScreenId: state.meta.firstScreenId === action.payload.screenId ? filtered[0]?.id ?? null : state.meta.firstScreenId, }; return withDirty(state, { ...state, screens: filtered, selectedScreenId, meta: nextMeta, }); } case "update-screen": { const { screenId, screen } = action.payload; let nextSelectedScreenId = state.selectedScreenId; // Проверяем, меняется ли ID экрана const isIdChange = "id" in screen && screen.id !== screenId; const newScreenId = isIdChange ? screen.id! : screenId; let nextScreens = state.screens.map((current) => current.id === screenId ? (() => { const nextScreen = { ...current, ...screen, title: screen.title ? { ...current.title, ...screen.title } : current.title, // Если subtitle явно передан в screen (даже если undefined), используем его // Иначе сохраняем текущий subtitle ...("subtitle" in screen ? { subtitle: screen.subtitle } : "subtitle" in current ? { subtitle: current.subtitle } : {}), ...(current.template === "list" && "list" in screen && screen.list ? { list: { ...(current as ListScreenDefinition).list, ...screen.list, options: screen.list.options ?? (current as ListScreenDefinition).list.options, }, } : {}), } as BuilderScreen; if ("variants" in screen) { if (Array.isArray(screen.variants) && screen.variants.length > 0) { nextScreen.variants = screen.variants; } else if ("variants" in nextScreen) { delete (nextScreen as Partial).variants; } } if (state.selectedScreenId === current.id && nextScreen.id !== current.id) { nextSelectedScreenId = nextScreen.id; } return nextScreen; })() : current ); // Если изменился ID экрана, обновляем все ссылки на него if (isIdChange) { nextScreens = updateScreenIdReferences(nextScreens, screenId, newScreenId); } return withDirty(state, { ...state, screens: nextScreens, selectedScreenId: nextSelectedScreenId, }); } case "reorder-screens": { const { fromIndex, toIndex } = action.payload; const previousScreens = state.screens; const newScreens = [...previousScreens]; const [movedScreen] = newScreens.splice(fromIndex, 1); newScreens.splice(toIndex, 0, movedScreen); const previousSequentialNext = new Map(); const previousIndexMap = new Map(); const newSequentialNext = new Map(); previousScreens.forEach((screen, index) => { previousSequentialNext.set(screen.id, previousScreens[index + 1]?.id); previousIndexMap.set(screen.id, index); }); newScreens.forEach((screen, index) => { newSequentialNext.set(screen.id, newScreens[index + 1]?.id); }); const totalScreens = newScreens.length; const rewiredScreens = newScreens.map((screen, index) => { const prevIndex = previousIndexMap.get(screen.id); const prevSequential = previousSequentialNext.get(screen.id); const nextSequential = newScreens[index + 1]?.id; const navigation = screen.navigation; const hasRules = Boolean(navigation?.rules && navigation.rules.length > 0); let defaultNext = navigation?.defaultNextScreenId; if (!hasRules) { if (!defaultNext || defaultNext === prevSequential) { defaultNext = nextSequential; } } else if (defaultNext === prevSequential) { defaultNext = nextSequential; } const updatedNavigation = (() => { if ((navigation?.rules && navigation.rules.length > 0) || defaultNext) { // Обновляем nextScreenId в правилах навигации при reorder const updatedRules = navigation?.rules?.map(rule => { let updatedNextScreenId = rule.nextScreenId; // Обновляем ссылки если правило указывает на экран, который "следовал" за другим экраном // и эта последовательность изменилась for (const [screenId, oldNext] of previousSequentialNext.entries()) { const newNext = newSequentialNext.get(screenId); // Если правило указывало на экран, который раньше был "следующим" // за каким-то экраном, но теперь следующим стал другой экран if (rule.nextScreenId === oldNext && newNext && oldNext !== newNext) { updatedNextScreenId = newNext; break; } } return { ...rule, nextScreenId: updatedNextScreenId }; }); return { ...(updatedRules ? { rules: updatedRules } : {}), ...(defaultNext ? { defaultNextScreenId: defaultNext } : {}), }; } return undefined; })(); let updatedHeader = screen.header; if (screen.header?.progress) { const progress = { ...screen.header.progress }; const previousProgress = prevIndex !== undefined ? previousScreens[prevIndex]?.header?.progress : undefined; if ( typeof progress.current === "number" && prevIndex !== undefined && (progress.current === prevIndex + 1 || previousProgress?.current === prevIndex + 1) ) { progress.current = index + 1; } if (typeof progress.total === "number") { const previousTotal = previousProgress?.total ?? progress.total; if (previousTotal === previousScreens.length) { progress.total = totalScreens; } } updatedHeader = { ...screen.header, progress, }; } const nextScreen: BuilderScreen = { ...screen, ...(updatedHeader ? { header: updatedHeader } : {}), }; if (updatedNavigation) { nextScreen.navigation = updatedNavigation; } else if ("navigation" in nextScreen) { delete nextScreen.navigation; } return nextScreen; }); const nextMeta = { ...state.meta, firstScreenId: rewiredScreens[0]?.id, }; const nextSelectedScreenId = movedScreen && state.selectedScreenId === movedScreen.id ? movedScreen.id : state.selectedScreenId; return withDirty(state, { ...state, screens: rewiredScreens, meta: nextMeta, selectedScreenId: nextSelectedScreenId, }); } case "set-selected-screen": { return { ...state, selectedScreenId: action.payload.screenId, }; } case "set-screens": { return withDirty(state, { ...state, screens: action.payload, selectedScreenId: action.payload[0]?.id ?? null, meta: { ...state.meta, firstScreenId: state.meta.firstScreenId ?? action.payload[0]?.id, }, }); } case "update-navigation": { const { screenId, navigation } = action.payload; return withDirty(state, { ...state, screens: state.screens.map((screen) => screen.id === screenId ? { ...screen, navigation: { defaultNextScreenId: navigation.defaultNextScreenId ?? undefined, rules: navigation.rules ?? [], isEndScreen: navigation.isEndScreen, ...("onBackScreenId" in navigation ? { onBackScreenId: navigation.onBackScreenId ?? undefined } : {}), }, } : screen ), }); } case "reset": { return action.payload ? { ...action.payload, isDirty: false } : INITIAL_STATE; } default: return state; } }