"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { TextInput } from "@/components/ui/TextInput/TextInput"; import { Button } from "@/components/ui/button"; import { TemplateConfig } from "@/components/admin/builder/templates"; import { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context"; import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { NavigationRuleDefinition, ScreenDefinition } from "@/lib/funnel/types"; import { cn } from "@/lib/utils"; import { validateBuilderState } from "@/lib/admin/builder/validation"; type ValidationIssues = ReturnType["issues"]; function isListScreen( screen: BuilderScreen ): screen is BuilderScreen & { list: { selectionType: "single" | "multi"; options: Array<{ id: string; label: string; description?: string; emoji?: string }>; }; } { return screen.template === "list" && "list" in screen; } function Section({ title, description, children, }: { title: string; description?: string; children: ReactNode; }) { return (

{title}

{description &&

{description}

}
{children}
); } function ValidationSummary({ issues }: { issues: ValidationIssues }) { if (issues.length === 0) { return (
Всё хорошо — воронка валидна.
); } return (
{issues.map((issue, index) => (
{issue.severity === "error" ? "Ошибка" : "Предупреждение"} {issue.screenId ? ` · ${issue.screenId}` : ""} {issue.optionId ? ` · ${issue.optionId}` : ""}

{issue.message}

))}
); } export function BuilderSidebar() { const state = useBuilderState(); const dispatch = useBuilderDispatch(); const selectedScreen = useBuilderSelectedScreen(); const [activeTab, setActiveTab] = useState<"funnel" | "screen">(selectedScreen ? "screen" : "funnel"); const selectedScreenId = selectedScreen?.id ?? null; useEffect(() => { setActiveTab((previous) => { if (selectedScreenId) { return "screen"; } return previous === "screen" ? "funnel" : previous; }); }, [selectedScreenId]); const validation = useMemo(() => validateBuilderState(state), [state]); const screenValidationIssues = useMemo(() => { if (!selectedScreenId) { return [] as ValidationIssues; } return validation.issues.filter((issue) => issue.screenId === selectedScreenId); }, [selectedScreenId, validation]); const screenOptions = useMemo( () => state.screens.map((screen) => ({ id: screen.id, title: screen.title.text })), [state.screens] ); const handleMetaChange = (field: keyof typeof state.meta, value: string) => { dispatch({ type: "set-meta", payload: { [field]: value } }); }; const handleFirstScreenChange = (value: string) => { dispatch({ type: "set-meta", payload: { firstScreenId: value } }); }; const getScreenById = (screenId: string): BuilderScreen | undefined => state.screens.find((item) => item.id === screenId); const updateNavigation = ( screen: BuilderScreen, navigationUpdates: Partial = {} ) => { dispatch({ type: "update-navigation", payload: { screenId: screen.id, navigation: { defaultNextScreenId: navigationUpdates.defaultNextScreenId ?? screen.navigation?.defaultNextScreenId, rules: navigationUpdates.rules ?? screen.navigation?.rules ?? [], }, }, }); }; const handleDefaultNextChange = (screenId: string, nextScreenId: string | "") => { const screen = getScreenById(screenId); if (!screen) { return; } updateNavigation(screen, { defaultNextScreenId: nextScreenId || undefined, }); }; const updateRules = (screenId: string, rules: NavigationRuleDefinition[]) => { const screen = getScreenById(screenId); if (!screen) { return; } updateNavigation(screen, { rules }); }; const handleRuleOperatorChange = ( screenId: string, index: number, operator: NavigationRuleDefinition["conditions"][0]["operator"] ) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, conditions: rule.conditions.map((condition, conditionIndex) => conditionIndex === 0 ? { ...condition, operator, } : condition ), } : rule ); updateRules(screenId, nextRules); }; const handleRuleOptionToggle = (screenId: string, ruleIndex: number, optionId: string) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, currentIndex) => { if (currentIndex !== ruleIndex) { return rule; } const [condition] = rule.conditions; const optionIds = new Set(condition.optionIds ?? []); if (optionIds.has(optionId)) { optionIds.delete(optionId); } else { optionIds.add(optionId); } return { ...rule, conditions: [ { ...condition, optionIds: Array.from(optionIds), }, ], }; }); updateRules(screenId, nextRules); }; const handleRuleNextScreenChange = (screenId: string, ruleIndex: number, nextScreenId: string) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, currentIndex) => currentIndex === ruleIndex ? { ...rule, nextScreenId } : rule ); updateRules(screenId, nextRules); }; const handleAddRule = (screen: BuilderScreen) => { if (!isListScreen(screen)) { return; } const defaultCondition: NavigationRuleDefinition["conditions"][number] = { screenId: screen.id, operator: "includesAny", optionIds: screen.list.options.slice(0, 1).map((option) => option.id), }; const nextRules = [ ...(screen.navigation?.rules ?? []), { nextScreenId: state.screens[0]?.id ?? screen.id, conditions: [defaultCondition] }, ]; updateNavigation(screen, { rules: nextRules }); }; const handleRemoveRule = (screenId: string, ruleIndex: number) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.filter((_, index) => index !== ruleIndex); updateNavigation(screen, { rules: nextRules }); }; const handleDeleteScreen = (screenId: string) => { if (state.screens.length <= 1) { return; } dispatch({ type: "remove-screen", payload: { screenId } }); }; const handleTemplateUpdate = (screenId: string, updates: Partial) => { dispatch({ type: "update-screen", payload: { screenId, screen: updates as Partial, }, }); }; const selectedScreenIsListType = selectedScreen ? isListScreen(selectedScreen) : false; return (
Режим редактирования

Настройки

{activeTab === "funnel" ? (
handleMetaChange("id", event.target.value)} /> handleMetaChange("title", event.target.value)} /> handleMetaChange("description", event.target.value)} />
Всего экранов {state.screens.length}
{state.screens.map((screen, index) => ( {index + 1}. {screen.title.text} {screen.template} ))}
) : selectedScreen ? (
ID: {selectedScreen.id} Тип: {selectedScreen.template} Позиция: экран {state.screens.findIndex((screen) => screen.id === selectedScreen.id) + 1} из {state.screens.length}
Текущий шаблон: {selectedScreen.template}
handleTemplateUpdate(selectedScreen.id, updates)} />
{selectedScreenIsListType && (

Направляйте пользователей на разные экраны в зависимости от выбора.

{(selectedScreen.navigation?.rules ?? []).length === 0 && (
Правил пока нет
)} {(selectedScreen.navigation?.rules ?? []).map((rule, ruleIndex) => (
Правило {ruleIndex + 1}
{selectedScreen.template === "list" ? (
Варианты ответа
{selectedScreen.list.options.map((option) => { const condition = rule.conditions[0]; const isChecked = condition.optionIds?.includes(option.id) ?? false; return ( ); })}
) : (
Навигационные правила с вариантами ответа доступны только для экранов со списком.
)}
))}
)}

Удаление экрана нельзя отменить. Все связи с этим экраном будут потеряны.

) : (
Выберите экран в списке слева, чтобы настроить его параметры.
)}
); }