import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import ReactDOM from 'react-dom/client'; import Papa from 'papaparse'; // FIX: Removed `startOfMonth` from import to resolve module resolution error. The logic is now handled with the native Date object. import { differenceInDays, endOfMonth, endOfWeek, endOfYear, format, isValid, parse, startOfWeek, startOfYear, subDays } from 'date-fns'; import { ResponsiveContainer, BarChart, LineChart, XAxis, YAxis, Tooltip, Legend, Bar, Line, CartesianGrid, ComposedChart } from 'recharts'; import { GoogleGenAI } from '@google/genai'; // --- Constants --- const GOOGLE_SHEET_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSO-grXAucfxwoFEcP8-NQWZkH31k591TSJP2_NAbH3V5UY8AB0NKAiv3eNVd2Yyg/pub?output=csv'; const YANDEX_DIRECT_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTSm9MmbWGZhFhm_dv6H-2Rz33C12MG88x2IOk_AUTsVglnRVVKzFAgApMyHVCvxg/pub?output=csv'; const FINAL_STAGE_NAME = 'Успешно реализовано'; const INITIAL_YANDEX_DATA_TSV = `Кампания № Кампании Показы Клики CTR (%) Расход (руб.) Ср. цена клика (руб.) Ср. цена тыс. показов (руб.) Конверсия (%) Цена цели (руб.) Конверсии РСЯ_рф_конверсия_17.07.25_квиз 702088042 15050 56 0,37 2000 35,71 132,89 7,14 500 4 РСЯ_рф_конв_клики_17.07.25_квиз 702088050 69067 177 0,26 8033,08 45,38 116,31 3,39 1338,85 6 РСЯ_авто_рф_конв_клики_17.07.25_квиз 702088422 55102 243 0,44 7892,4 32,48 143,23 6,58 493,28 16 РСЯ_кз_конв_17.07.25_квиз 702088743 53381 126 0,24 0 0 0 - - 0 РСЯ_авто_кз_конв_клики_17.07.25_квиз 702088922 302817 561 0,19 8741,09 15,58 28,87 3,39 460,06 19 РСЯ_рф_конв_клики_21.07.25_квиз 702158186 61382 237 0,39 10209,69 43,08 166,33 5,06 850,81 12 РСЯ_бренд_рф_конв_клики_25.07.25_квиз 702256683 124130 486 0,39 22534,95 46,37 181,54 10,08 459,9 49 РСЯ_рф_конв_клики_01.09.25_квиз 702256694 65336 203 0,31 8707,43 42,89 133,27 11,33 378,58 23 РСЯ_рф_конв_клики_29.07.25_квиз 702306963 50793 144 0,28 7995,82 55,53 157,42 7,64 726,89 11 РСЯ_бренд_кз_конв_клики_29.07.25_квиз 702307429 219176 537 0,25 6638,93 12,36 30,29 1,68 737,66 9 РСЯ_бренд_рф_конв_клики_27.08.25_квиз 702918805 4845 39 0,8 1702,6 43,66 351,41 2,56 1702,6 1 РСЯ_авто_кз_конв_клики_28.08.25_квиз 702933594 46123 86 0,19 1601,46 18,62 34,72 2,33 800,73 2 РСЯ_рф_конв_клики_28.08.25_квиз 702933600 4726 19 0,4 919,56 48,4 194,57 - - 0 РСЯ_бренд_рф_конв_клики_01.09.25_квиз 703003478 1773 19 1,07 1044,97 55 589,38 - - 0 РСЯ_авто_кз_конв_клики_01.09.25_квиз 703003482 459449 559 0,12 8991,72 16,09 19,57 3,76 428,18 21 РСЯ_рф_конв_клики_01.09.25_квиз 703003494 64034 220 0,34 8549,88 38,86 133,52 9,09 427,49 20 РСЯ_рф_конв_клики_10.09.25_квиз 703223485 11851 47 0,4 1584,98 33,72 133,74 2,13 1584,98 1 РСЯ_рф_конв_клики_10.09.25_квиз 703223490 10804 47 0,44 1882,98 40,06 174,29 2,13 1882,98 1 РСЯ_авто_кз_конв_клики_10.09.25_квиз 703223494 213814 280 0,13 4151,96 14,83 19,42 5 296,57 14`; // --- Gemini Service --- // ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ О БЕЗОПАСНОСТИ! // Следующий API-ключ встроен непосредственно в код, чтобы обеспечить немедленную работоспособность. // Это делает его видимым для любого, кто посетит ваш сайт. // РЕКОМЕНДУЕТСЯ: Как можно скорее перейдите на использование серверного прокси, // чтобы защитить ваш ключ от кражи и несанкционированного использования. const GEMINI_API_KEY = "AIzaSyDSprnYlSc_dEy9kz2WHO1A3z3lRN9GQ9s"; const generateDashboardAnalysis = async (data) => { if (!data || data.length === 0) return "Нет данных для анализа."; const simplifiedData = data.slice(-50).map(({ dateString, createdBy, stage, tags }) => ({ date: dateString, createdBy, stage, tags })); const prompt = ` Ты — опытный бизнес-аналитик, специализирующийся на воронках лидов и Customer Journey Map. Твоя задача — проанализировать данные о лидах, а НЕ о продажах. Забудь о деньгах, сфокусируйся на движении лидов и их характеристиках. Проанализируй следующие данные о лидах: 1. **Краткое резюме (2-3 предложения):** Опиши общее состояние воронки. Всего лидов в работе: ${data.length}. 2. **Портрет целевой аудитории (ЦА):** На основе 'Тегов сделки' составь краткий, но емкий портрет потенциального клиента. 3. **"Бутылочное горлышко" воронки:** Определи этап, на котором теряется больше всего лидов. 4. **Одна действенная рекомендация:** Что можно сделать, чтобы улучшить конверсию? Представь анализ в четком виде, используя markdown для заголовков (##) и списков (*). Вот данные о лидах (показаны последние 50 для краткости): ${JSON.stringify(simplifiedData, null, 2)} `; try { if (!process.env.API_KEY) { return "## Ошибка Конфигурации\n\nAPI-ключ Gemini не определен в коде."; } const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: prompt, }); return response.text; } catch (error) { console.error("Error generating static analysis with Gemini:", error); return "## Ошибка Аналитики\n\nНе удалось сгенерировать отчет. Возможные причины: проблемы с интернет-соединением или временная недоступность сервиса Gemini."; } }; const generateInteractiveAnalysis = async (data, question) => { if (!data || data.length === 0) return "Нет данных для ответа на вопрос."; if (!question) return "Пожалуйста, задайте вопрос."; const simplifiedData = data.slice(-100).map(({ dateString, createdBy, stage, tags }) => ({ date: dateString, createdBy, stage, tags })); const prompt = ` Ты — ассистент-аналитик. Тебе предоставлены данные о лидах и вопрос от пользователя. Твоя задача — дать четкий и лаконичный ответ на вопрос, основываясь ИСКЛЮЧИТЕЛЬНО на предоставленных данных. Не придумывай информацию. Если данных недостаточно, так и скажи. Используй markdown для форматирования. **Данные (до 100 записей):** ${JSON.stringify(simplifiedData, null, 2)} **Вопрос пользователя:** ${question} `; try { if (!process.env.API_KEY) { return "## Ошибка Конфигурации\n\nAPI-ключ Gemini не определен в коде."; } const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: prompt, }); return response.text; } catch (error) { console.error("Error generating interactive analysis with Gemini:", error); return "## Ошибка\n\nНе удалось получить ответ. Возможные причины: проблемы с интернет-соединением или временная недоступность сервиса Gemini."; } }; const generateYandexDirectAnalysis = async (data) => { if (!data || data.length === 0) return "Нет данных для анализа."; const simplifiedData = data.slice(-50).map(({ dateString, campaign, impressions, clicks, cost, conversions }) => ({ date: dateString, campaign, impressions, clicks, cost, conversions })); const prompt = ` Ты — эксперт по цифровому маркетингу, специализирующийся на аналитике Яндекс.Директ. Проанализируй следующие данные о рекламных кампаниях: 1. **Краткое резюме:** Опиши общую эффективность, включая расходы, клики и конверсии. 2. **Лучшие по конверсиям:** Укажи кампанию с наибольшим числом конверсий. 3. **Самые рентабельные:** Определи кампанию с самой низкой стоимостью конверсии (цели). 4. **Самые затратные:** Укажи кампанию с самой высокой стоимостью конверсии. 5. **Одна действенная рекомендация:** Что можно сделать для оптимизации, основываясь на данных о конверсиях и их стоимости? Представь анализ в четком виде, используя markdown для заголовков (##) и списков (*). Вот данные (показаны последние 50 для краткости): ${JSON.stringify(simplifiedData, null, 2)} `; try { if (!process.env.API_KEY) { return "## Ошибка Конфигурации\n\nAPI-ключ Gemini не определен в коде."; } const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: prompt, }); return response.text; } catch (error) { console.error("Error generating Yandex.Direct analysis with Gemini:", error); return "## Ошибка Аналитики\n\nНе удалось сгенерировать отчет. Возможные причины: проблемы с интернет-соединением или временная недоступность сервиса Gemini."; } }; // --- Data Hooks --- const useGoogleSheetData = () => { const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const processCsvData = useCallback((csvText) => { const parseDate = (dateStr) => { if (!dateStr || typeof dateStr !== 'string') return null; const datePart = dateStr.split(' ')[0]; const parsed = parse(datePart, 'dd.MM.yyyy', new Date()); return isValid(parsed) ? parsed : null; }; const parseResult = Papa.parse(csvText, { header: true, skipEmptyLines: true }); if (parseResult.errors.length) throw new Error('Ошибка обработки CSV.'); const parsedData = parseResult.data .map(row => { const date = parseDate(row['Дата создания']); if (!date) return null; return { date, dateString: format(date, 'dd.MM.yyyy'), createdBy: (row['Кем создана'] || 'Неизвестно').trim(), stage: (row['Этап сделки'] || 'Не указан').trim(), tags: (row['Теги сделки'] || '').trim(), closingDate: parseDate(row['Дата закрытия']), status: parseDate(row['Дата закрытия']) ? 'Закрыта' : 'Открыта' }; }) .filter(Boolean) .sort((a, b) => a.date.getTime() - b.date.getTime()); setData(parsedData); setError(null); }, []); const fetchData = useCallback(async (url) => { setIsLoading(true); setError(null); try { const fetchUrl = new URL(url); fetchUrl.searchParams.append('_cache_bust', Date.now().toString()); const response = await fetch(fetchUrl.toString()); if (!response.ok) throw new Error(`Ошибка загрузки данных (статус ${response.status}). Убедитесь, что таблица опубликована.`); processCsvData(await response.text()); } catch (e) { console.error("Failed to fetch sheet data:", e); setError(e); } finally { setIsLoading(false); } }, [processCsvData]); const handleManualUpload = useCallback((file) => { if (!file) return; setIsLoading(true); setError(null); const reader = new FileReader(); reader.onload = (e) => { try { if (e.target?.result) { processCsvData(e.target.result); } } catch (parseError) { setError(parseError); } finally { setIsLoading(false); } }; reader.onerror = () => setError(new Error('Не удалось прочитать файл.')); reader.readAsText(file); }, [processCsvData]); useEffect(() => { fetchData(GOOGLE_SHEET_CSV_URL); }, [fetchData]); return { data, isLoading, error, refreshData: () => fetchData(GOOGLE_SHEET_CSV_URL), handleManualUpload }; }; const useYandexDirectData = () => { const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const processCsvData = useCallback((csvText) => { setIsLoading(true); let parseResult = Papa.parse(csvText, { header: true, skipEmptyLines: true, delimiter: "\t" }); if (parseResult.data.length > 0 && Object.keys(parseResult.data[0]).length === 1) { const commaParseResult = Papa.parse(csvText, { header: true, skipEmptyLines: true }); if (Object.keys(commaParseResult.data[0] || {}).length > 1) { parseResult = commaParseResult; } } if (parseResult.errors.length > 0 && (!parseResult.data || parseResult.data.length === 0)) { setError(new Error('Ошибка обработки CSV. Проверьте формат файла.')); setIsLoading(false); return; } const hasDateColumn = parseResult.meta.fields.includes('Дата'); const parseDate = (row) => { if (hasDateColumn && row['Дата']) { const dateStr = String(row['Дата']).trim(); if (dateStr) { const parsed = parse(dateStr, 'dd.MM.yyyy', new Date()); if (isValid(parsed)) return parsed; } } const campaignName = row['Кампания']; if (campaignName && typeof campaignName === 'string') { const match = campaignName.match(/(\d{2}\.\d{2}\.\d{2})/); if (match && match[1]) { const parsed = parse(match[1], 'dd.MM.yy', new Date()); if (isValid(parsed)) return parsed; } } return null; }; const parsedData = parseResult.data .map(row => { const date = parseDate(row); if (!date) return null; const impressions = parseInt(String(row['Показы'] || '0').replace(/\s/g, '')) || 0; const clicks = parseInt(String(row['Клики'] || '0').replace(/\s/g, '')) || 0; const cost = parseFloat(String(row['Расход (руб.)'] || '0').replace(/\s/g, '').replace(',', '.')) || 0; const conversions = parseInt(String(row['Конверсии'] || '0').replace(/\s/g, '')) || 0; const cpa = parseFloat(String(row['Цена цели (руб.)'] || '0').replace(/\s/g, '').replace(',', '.')) || 0; return { date, dateString: format(date, 'dd.MM.yyyy'), campaign: (row['Кампания'] || 'Неизвестно').trim(), impressions, clicks, cost, conversions, cpa: conversions > 0 ? cost / conversions : cpa, // Recalculate or use provided ctr: impressions > 0 ? (clicks / impressions) * 100 : 0, cpc: clicks > 0 ? cost / clicks : 0, }; }) .filter(item => item && item.campaign) .sort((a, b) => a.date.getTime() - b.date.getTime()); setData(parsedData); setError(null); setIsLoading(false); }, []); const fetchData = useCallback(async (url) => { setIsLoading(true); setError(null); try { const fetchUrl = new URL(url); fetchUrl.searchParams.append('_cache_bust', Date.now().toString()); const response = await fetch(fetchUrl.toString()); if (!response.ok) throw new Error(`Ошибка загрузки данных (статус ${response.status}). Убедитесь, что таблица опубликована.`); processCsvData(await response.text()); } catch (e) { console.error("Failed to fetch sheet data:", e); setError(e); setIsLoading(false); } }, [processCsvData]); const handleManualUpload = useCallback((file) => { if (!file) return; setIsLoading(true); setError(null); const reader = new FileReader(); reader.onload = (e) => { try { if (e.target?.result) { processCsvData(e.target.result); } } catch (parseError) { setError(parseError); } finally { setIsLoading(false); } }; reader.onerror = () => { setError(new Error('Не удалось прочитать файл.')); setIsLoading(false); }; reader.readAsText(file); }, [processCsvData]); useEffect(() => { processCsvData(INITIAL_YANDEX_DATA_TSV); }, [processCsvData]); return { data, isLoading, error, refreshData: () => fetchData(YANDEX_DIRECT_CSV_URL), handleManualUpload }; }; // --- Components --- const CustomTooltip = ({ active, payload, label, formatter }) => { if (active && payload && payload.length) { return React.createElement('div', { className: "bg-slate-700/80 backdrop-blur-sm border border-slate-600 p-3 rounded-lg shadow-lg text-sm" }, React.createElement('p', { className: "label font-bold text-slate-200" }, `${label}`), payload.map((pld, index) => React.createElement('p', { key: index, style: { color: pld.color } }, `${pld.name}: ${formatter ? formatter(pld.value, pld.dataKey) : pld.value}`)) ); } return null; }; const TopItemsChart = ({ data, color, dataKey = "value", name = "Количество", formatter }) => { if (!data || data.length === 0) return React.createElement('div', { className: "flex items-center justify-center h-[300px] text-slate-500" }, "Нет данных"); return React.createElement(ResponsiveContainer, { width: "100%", height: 300 }, React.createElement(BarChart, { data: data, layout: "vertical", margin: { top: 5, right: 30, left: 100, bottom: 5 } }, React.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: "rgba(100, 116, 139, 0.1)" }), React.createElement(XAxis, { type: "number", axisLine: false, tickLine: false, fontSize: 12, tick: { fill: '#94a3b8' }, tickFormatter: val => val.toLocaleString('ru-RU') }), React.createElement(YAxis, { dataKey: "name", type: "category", width: 120, axisLine: false, tickLine: false, fontSize: 12, tick: { textAnchor: 'end', dx: -5, fill: '#94a3b8' } }), React.createElement(Tooltip, { content: React.createElement(CustomTooltip, { formatter: formatter || ((val) => val.toLocaleString('ru-RU')) }), cursor: { fill: 'rgba(100, 116, 139, 0.1)' } }), React.createElement(Bar, { dataKey: dataKey, name: name, fill: color, radius: [0, 4, 4, 0], barSize: 20 }) ) ); }; const LeadsOverTimeChart = ({ data, color, name = "Количество" }) => { if (!data || data.length === 0) return React.createElement('div', { className: "flex items-center justify-center h-[300px] text-slate-500" }, "Нет данных"); return React.createElement(ResponsiveContainer, { width: "100%", height: 300 }, React.createElement(LineChart, { data, margin: { top: 5, right: 30, left: 20, bottom: 5 } }, React.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: "rgba(100, 116, 139, 0.1)" }), React.createElement(XAxis, { dataKey: "month", fontSize: 12, tick: { fill: '#94a3b8' } }), React.createElement(YAxis, { fontSize: 12, tick: { fill: '#94a3b8' } }), React.createElement(Tooltip, { content: React.createElement(CustomTooltip, { formatter: (val) => val.toLocaleString('ru-RU') }) }), React.createElement(Line, { type: "monotone", dataKey: "value", name: name, stroke: color, strokeWidth: 2, dot: { r: 4 }, activeDot: { r: 8 } }) ) ); }; const PerformanceOverTimeChart = ({ data }) => { if (!data || data.length === 0) return React.createElement('div', { className: "flex items-center justify-center h-[300px] text-slate-500" }, "Нет данных"); return React.createElement(ResponsiveContainer, { width: "100%", height: 300 }, React.createElement(ComposedChart, { data, margin: { top: 5, right: 30, left: 20, bottom: 5 } }, React.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: "rgba(100, 116, 139, 0.1)" }), React.createElement(XAxis, { dataKey: "date", fontSize: 12, tick: { fill: '#94a3b8' } }), React.createElement(YAxis, { yAxisId: "left", fontSize: 12, tick: { fill: '#94a3b8' } }), React.createElement(YAxis, { yAxisId: "right", orientation: "right", fontSize: 12, tick: { fill: '#94a3b8' }, tickFormatter: val => `${val.toLocaleString('ru-RU')} ₽` }), React.createElement(Tooltip, { content: React.createElement(CustomTooltip, { formatter: (val, key) => key === 'cost' ? `${val.toFixed(2)} ₽` : val.toLocaleString('ru-RU') }) }), React.createElement(Legend, { wrapperStyle: { fontSize: "12px" } }), React.createElement(Bar, { yAxisId: "left", dataKey: "impressions", name: "Показы", fill: "#334155", barSize: 20 }), React.createElement(Line, { yAxisId: "left", type: "monotone", dataKey: "clicks", name: "Клики", stroke: "#22d3ee", strokeWidth: 2 }), React.createElement(Line, { yAxisId: "right", type: "monotone", dataKey: "cost", name: "Расход", stroke: "#a78bfa", strokeWidth: 2 }) ) ); }; const ChartIcon = () => React.createElement('svg', { xmlns: "http://www.w3.org/2000/svg", className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, React.createElement('path', { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" })); const RefreshIcon = ({ isSpinning }) => React.createElement('svg', { xmlns: "http://www.w3.org/2000/svg", className: `h-5 w-5 ${isSpinning ? 'animate-spin' : ''}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, React.createElement('path', { strokeLinecap: "round", strokeLinejoin: "round", d: "M4 4v5h5M20 20v-5h-5M4 4l1.5 1.5A9 9 0 0120.5 16.5M20 20l-1.5-1.5A9 9 0 003.5 7.5" })); const SparklesIcon = () => React.createElement('svg', { xmlns: "http://www.w3.org/2000/svg", className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, React.createElement('path', { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.293 2.293a1 1 0 01-1.414 1.414L10 6M17 7l-2.293 2.293a1 1 0 01-1.414-1.414L15 6m-5.001 10.001l-2.293 2.293a1 1 0 01-1.414-1.414L10 16m7-2l-2.293 2.293a1 1 0 01-1.414-1.414L15 16" })); const Header = ({ onRefresh, isRefreshing, lastUpdated }) => React.createElement('header', { className: "flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6" }, React.createElement('div', null, React.createElement('h1', { className: "text-3xl font-bold text-white flex items-center gap-3" }, React.createElement(ChartIcon), "Дэшборд анализа воронок"), React.createElement('p', { className: "text-slate-400 mt-1" }, "Интерактивный анализ движения лидов на базе Gemini") ), React.createElement('div', { className: "flex items-center gap-4 mt-4 sm:mt-0" }, React.createElement('span', { className: "text-sm text-slate-500" }, lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString()}` : 'Загрузка...'), React.createElement('button', { onClick: onRefresh, disabled: isRefreshing, className: "flex items-center gap-2 bg-violet-600 hover:bg-violet-700 disabled:bg-slate-600 text-white font-semibold py-2 px-4 rounded-lg transition-transform hover:scale-105 shadow-lg" }, React.createElement(RefreshIcon, { isSpinning: isRefreshing }), isRefreshing ? 'Обновление...' : 'Обновить') ) ); const KpiCard = ({ title, value, description, isLoading }) => { if (isLoading) return React.createElement('div', { className: "bg-slate-800/50 p-6 rounded-xl animate-pulse" }, React.createElement('div', { className: "h-4 bg-slate-700 rounded w-3/4 mb-4" }), React.createElement('div', { className: "h-10 bg-slate-700 rounded w-1/2 mb-2" }), React.createElement('div', { className: "h-3 bg-slate-700 rounded w-full" })); return React.createElement('div', { className: "bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6 shadow-lg transition-all hover:border-cyan-400 hover:-translate-y-1" }, React.createElement('h3', { className: "text-sm font-medium text-slate-400" }, title), React.createElement('p', { className: "text-4xl font-bold text-white mt-2" }, value), React.createElement('p', { className: "text-xs text-slate-500 mt-1" }, description) ); }; const Panel = ({ title, icon, children, isLoading }) => React.createElement('div', { className: "bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl shadow-lg p-6" }, React.createElement('h2', { className: "text-xl font-bold text-white flex items-center gap-3 mb-4" }, icon, title), isLoading ? React.createElement('div', { className: "flex items-center justify-center h-[300px]" }, React.createElement('div', { className: "h-16 w-16 border-4 border-t-transparent border-slate-600 rounded-full animate-spin" })) : children ); const ErrorDisplay = ({ error, onFileUpload }) => { const fileInputRef = useRef(null); const handleFileSelectClick = () => fileInputRef.current?.click(); const handleFileChange = (event) => onFileUpload(event.target.files?.[0]); const is400Error = error?.message?.includes('статус 400'); return React.createElement('div', { className: "bg-red-900/40 border border-red-500/50 text-red-200 p-6 rounded-lg mb-8" }, React.createElement('h3', { className: "font-bold text-lg text-white" }, "Ошибка загрузки данных"), React.createElement('p', { className: "mt-1" }, is400Error ? "Эта ошибка почти всегда означает, что Google Таблица не опубликована корректно." : "Произошла непредвиденная ошибка."), is400Error && React.createElement('ol', { className: "list-decimal list-inside space-y-2 mt-4 text-sm" }, React.createElement('li', null, "В Google Таблице, нажмите ", React.createElement('strong', null, "Файл > Поделиться > Опубликовать в интернете.")), React.createElement('li', null, "Измените формат с 'Веб-страница' на ", React.createElement('strong', null, "'Значения, разделенные запятыми (.csv)'.")), React.createElement('li', null, "Нажмите ", React.createElement('strong', null, "Опубликовать"), ". Приложение попытается загрузить данные.") ), React.createElement('div', { className: "mt-6 pt-4 border-t border-red-500/30" }, React.createElement('h4', { className: "font-semibold text-white" }, "Альтернатива: Загрузить файл вручную"), React.createElement('input', { type: "file", ref: fileInputRef, onChange: handleFileChange, style: { display: 'none' }, accept: ".csv,.tsv" }), React.createElement('button', { onClick: handleFileSelectClick, className: "mt-4 w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg" }, "Выбрать .csv или .tsv файл") ), error?.message && React.createElement('div', { className: "mt-4 text-xs bg-black/20 p-2 rounded" }, React.createElement('strong', null, "Технические детали: "), error.message) ); }; const datePresets = { 'week': 'Эта неделя', 'month': 'Этот месяц', 'last30': 'Последние 30 дней', 'year': 'Этот год', 'all': 'Все время' }; const FilterBar = ({ onDatePresetChange, activePreset, customDateRange, onCustomDateChange, managers, selectedManager, onManagerChange, tags, selectedTag, onTagChange }) => { return React.createElement('div', { className: "bg-slate-800/50 border border-slate-700 rounded-xl p-4 mb-8 flex flex-wrap items-center gap-x-4 gap-y-3" }, React.createElement('div', { className: "flex flex-wrap items-center gap-x-2 gap-y-3" }, React.createElement('span', { className: "text-sm font-semibold mr-2" }, "Период:"), Object.entries(datePresets).map(([key, label]) => React.createElement('button', { key: key, onClick: () => onDatePresetChange(key), className: `px-3 py-1.5 text-xs sm:text-sm rounded-md transition-colors ${activePreset === key ? 'bg-violet-600 text-white font-semibold' : 'bg-slate-700 hover:bg-slate-600 text-slate-300'}` }, label)) ), React.createElement('div', { className: "flex items-center gap-2" }, React.createElement('input', { type: "date", value: customDateRange.start ? format(customDateRange.start, 'yyyy-MM-dd') : '', onChange: e => onCustomDateChange('start', e.target.valueAsDate), className: "bg-slate-700 text-slate-300 text-sm rounded-md px-2 py-1 border border-slate-600 focus:ring-violet-500 outline-none" }), React.createElement('span', { className: "text-slate-500" }, "-"), React.createElement('input', { type: "date", value: customDateRange.end ? format(customDateRange.end, 'yyyy-MM-dd') : '', onChange: e => onCustomDateChange('end', e.target.valueAsDate), className: "bg-slate-700 text-slate-300 text-sm rounded-md px-2 py-1 border border-slate-600 focus:ring-violet-500 outline-none" }) ), (managers || tags) && React.createElement('div', { className: "flex flex-wrap items-center gap-x-4 gap-y-3" }, managers && React.createElement('select', { value: selectedManager, onChange: e => onManagerChange(e.target.value), className: "bg-slate-700 text-slate-300 text-sm rounded-md px-2 py-1.5 border border-slate-600 focus:ring-violet-500 outline-none" }, React.createElement('option', { value: "all" }, managers.length > 0 ? "Все менеджеры" : "Все кампании"), managers.map(m => React.createElement('option', { key: m, value: m }, m)) ), tags && React.createElement('select', { value: selectedTag, onChange: e => onTagChange(e.target.value), className: "bg-slate-700 text-slate-300 text-sm rounded-md px-2 py-1.5 border border-slate-600 focus:ring-violet-500 outline-none" }, React.createElement('option', { value: "all" }, "Все теги"), tags.map(t => React.createElement('option', { key: t, value: t }, t)) ) ) ); }; const InteractiveAiPanel = ({ data, isLoading, type = 'funnel' }) => { const [question, setQuestion] = useState(''); const [answer, setAnswer] = useState(''); const [isAsking, setIsAsking] = useState(false); const handleAsk = async () => { if (!question.trim()) return; setIsAsking(true); setAnswer(''); const result = await generateInteractiveAnalysis(data, question); setAnswer(result); setIsAsking(false); }; const formattedAnswer = useMemo(() => { if (!answer) return ''; return answer.split('\n').map(line => { if (line.startsWith('## ')) return `

${line.substring(3)}

`; if (line.startsWith('* ')) return `
  • ${line.substring(2).replace(/\*\*(.*?)\*\*/g, '$1')}
`; return `

${line.replace(/\*\*(.*?)\*\*/g, '$1')}

`; }).join(''); }, [answer]); return React.createElement(Panel, { title: "Интерактивный AI-Аналитик", icon: React.createElement(SparklesIcon), isLoading: isLoading }, React.createElement('div', { className: "space-y-4" }, React.createElement('p', { className: "text-sm text-slate-400" }, "Задайте вопрос по отфильтрованным данным. Например: 'Какие теги самые популярные у менеджера X?' или 'Сравни конверсию по тегам'"), React.createElement('div', { className: "flex gap-2" }, React.createElement('input', { type: "text", value: question, onChange: e => setQuestion(e.target.value), placeholder: "Ваш вопрос...", className: "flex-grow bg-slate-700 text-slate-200 rounded-md px-3 py-2 border border-slate-600 focus:ring-violet-500 outline-none" }), React.createElement('button', { onClick: handleAsk, disabled: isAsking, className: "bg-violet-600 hover:bg-violet-700 disabled:bg-slate-600 text-white font-semibold py-2 px-4 rounded-lg" }, isAsking ? 'Анализ...' : 'Спросить') ), (isAsking || answer) && React.createElement('div', { className: "mt-4 pt-4 border-t border-slate-700" }, isAsking ? React.createElement('p', null, 'Gemini анализирует данные...') : React.createElement('div', { className: "prose prose-sm prose-invert text-slate-300 max-w-none", dangerouslySetInnerHTML: { __html: formattedAnswer } }) ) ) ); }; // --- Dashboard Components --- const FunnelDashboard = () => { const { data: allData, isLoading, error, refreshData, handleManualUpload } = useGoogleSheetData(); const [staticAnalysis, setStaticAnalysis] = useState(''); const [isAnalyzing, setIsAnalyzing] = useState(false); const [datePreset, setDatePreset] = useState('all'); const [customDateRange, setCustomDateRange] = useState({ start: null, end: null }); const [selectedManager, setSelectedManager] = useState('all'); const [selectedTag, setSelectedTag] = useState('all'); const { managers, tags } = useMemo(() => { const managerSet = new Set(); const tagSet = new Set(); allData.forEach(item => { managerSet.add(item.createdBy); item.tags.split(',').forEach(tag => tag.trim() && tagSet.add(tag.trim())); }); return { managers: Array.from(managerSet).sort(), tags: Array.from(tagSet).sort() }; }, [allData]); const getDateRange = useCallback(() => { if (datePreset === 'custom' && customDateRange.start && customDateRange.end) return customDateRange; const now = new Date(); switch (datePreset) { case 'week': return { start: startOfWeek(now, { weekStartsOn: 1 }), end: endOfWeek(now, { weekStartsOn: 1 }) }; // FIX: Replaced date-fns startOfMonth with native Date object to resolve import error. case 'month': return { start: new Date(now.getFullYear(), now.getMonth(), 1), end: endOfMonth(now) }; case 'last30': return { start: subDays(now, 30), end: now }; case 'year': return { start: startOfYear(now), end: endOfYear(now) }; default: return { start: null, end: null }; } }, [datePreset, customDateRange]); const filteredData = useMemo(() => { const { start, end } = getDateRange(); const endDate = end ? new Date(end.setHours(23, 59, 59, 999)) : null; return allData.filter(item => { const dateMatch = !start || !endDate || (item.date >= start && item.date <= endDate); const managerMatch = selectedManager === 'all' || item.createdBy === selectedManager; const tagMatch = selectedTag === 'all' || item.tags.split(',').map(t => t.trim()).includes(selectedTag); return dateMatch && managerMatch && tagMatch; }); }, [allData, getDateRange, selectedManager, selectedTag]); useEffect(() => { if (!isLoading && !error && filteredData.length > 0) { setIsAnalyzing(true); generateDashboardAnalysis(filteredData).then(setStaticAnalysis).finally(() => setIsAnalyzing(false)); } else { setStaticAnalysis(''); } }, [filteredData, isLoading, error]); const totals = useMemo(() => { if (filteredData.length === 0) return { leadCount: 0, activeLeads: 0, conversionRate: 0, avgLeadLifetime: 0 }; const convertedLeads = filteredData.filter(item => item.stage === FINAL_STAGE_NAME).length; const closedDeals = filteredData.filter(item => item.closingDate); const totalLifetime = closedDeals.reduce((sum, item) => sum + differenceInDays(item.closingDate, item.date), 0); return { leadCount: filteredData.length, activeLeads: filteredData.filter(item => item.status === 'Открыта').length, conversionRate: filteredData.length > 0 ? (convertedLeads / filteredData.length) * 100 : 0, avgLeadLifetime: closedDeals.length > 0 ? totalLifetime / closedDeals.length : 0, }; }, [filteredData]); const chartData = useMemo(() => { const STAGE_ORDER = ['Новая заявка', 'Квалификация', 'Презентация', 'Переговоры', 'Подписание договора', FINAL_STAGE_NAME, 'Некачественный лид']; const transformToChartData = (source: { [key: string]: number }, limit = 7) => Object.entries(source).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value).slice(0, limit); // FIX: Add explicit types to reduce accumulators to help TypeScript with type inference. const dealsByStage = filteredData.reduce((acc: { [key: string]: number }, { stage }) => { acc[stage] = (acc[stage] || 0) + 1; return acc; }, {}); const dealsByCreator = filteredData.reduce((acc: { [key: string]: number }, { createdBy }) => { acc[createdBy] = (acc[createdBy] || 0) + 1; return acc; }, {}); const dealsByTag = filteredData.reduce((acc: { [key: string]: number }, { tags }) => { tags.split(',').forEach(t => { const tag = t.trim(); if(tag) acc[tag] = (acc[tag] || 0) + 1; }); return acc; }, {}); // FIX: Explicitly type the accumulator parameter 'acc' to correctly infer the type for the `reduce` operation, resolving downstream errors. const leadsByMonth = filteredData.reduce((acc: { [key: string]: { date: Date, value: number } }, { date }) => { const month = format(date, 'MMM yyyy'); // FIX: Replaced date-fns startOfMonth with native Date object to resolve import error. if (!acc[month]) acc[month] = { date: new Date(date.getFullYear(), date.getMonth(), 1), value: 0 }; acc[month].value++; return acc; }, {}); return { funnel: Object.entries(dealsByStage).map(([name, value]) => ({ name, value })).sort((a, b) => STAGE_ORDER.indexOf(a.name) - STAGE_ORDER.indexOf(b.name)), topCreators: transformToChartData(dealsByCreator), topTags: transformToChartData(dealsByTag, 10), leadsOverTime: Object.values(leadsByMonth).sort((a, b) => a.date.getTime() - b.date.getTime()).map(d => ({...d, month: format(d.date, 'MMM yy')})) }; }, [filteredData]); const formattedStaticAnalysis = useMemo(() => { return staticAnalysis.split('\n').map(line => { if (line.startsWith('## ')) return `

${line.substring(3)}

`; if (line.startsWith('* ')) return `
  • ${line.substring(2).replace(/\*\*(.*?)\*\*/g, '$1')}
`; return `

${line.replace(/\*\*(.*?)\*\*/g, '$1')}

`; }).join(''); }, [staticAnalysis]); const DealsDataTable = ({ data }) => { const [page, setPage] = useState(0); const rowsPerPage = 10; const pages = Math.ceil(data.length / rowsPerPage); const paginatedData = useMemo(() => data.slice(page * rowsPerPage, (page + 1) * rowsPerPage), [data, page]); useEffect(() => setPage(0), [data]); return React.createElement('div', null, React.createElement('div', { className: "overflow-x-auto" }, React.createElement('table', { className: "w-full text-sm text-left text-slate-400" }, React.createElement('thead', { className: "text-xs text-slate-300 uppercase bg-slate-800" }, React.createElement('tr', null, React.createElement('th', { className: "px-6 py-3" }, "Дата"), React.createElement('th', { className: "px-6 py-3" }, "Кем создана"), React.createElement('th', { className: "px-6 py-3" }, "Этап"), React.createElement('th', { className: "px-6 py-3" }, "Теги"), React.createElement('th', { className: "px-6 py-3" }, "Статус") ) ), React.createElement('tbody', null, paginatedData.map((tx, i) => React.createElement('tr', { key: i, className: "border-b border-slate-700 hover:bg-slate-800/50" }, React.createElement('td', { className: "px-6 py-4" }, tx.dateString), React.createElement('td', { className: "px-6 py-4" }, tx.createdBy), React.createElement('td', { className: "px-6 py-4" }, React.createElement('span', { className: 'bg-violet-900 text-violet-300 text-xs px-2.5 py-0.5 rounded' }, tx.stage)), React.createElement('td', { className: "px-6 py-4" }, tx.tags.split(',').map(t => t.trim() && React.createElement('span', { key: t, className: 'bg-slate-700 text-slate-300 text-xs mr-2 mb-1 inline-block px-2.5 py-0.5 rounded-full' }, t))), React.createElement('td', { className: "px-6 py-4" }, React.createElement('span', { className: `px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${tx.status === 'Открыта' ? 'bg-sky-900 text-sky-300' : 'bg-slate-700 text-slate-400'}` }, tx.status)) ))) ) ), pages > 1 && React.createElement('div', { className: "flex justify-between items-center mt-4" }, React.createElement('span', { className: "text-sm text-slate-400" }, `Всего: ${data.length} записей`), React.createElement('div', { className: "flex items-center space-x-2 text-sm" }, React.createElement('button', { onClick: () => setPage(0), disabled: page === 0, className: "px-3 py-1 bg-slate-700 rounded disabled:opacity-50 hover:bg-slate-600 transition-colors" }, "«"), React.createElement('button', { onClick: () => setPage(p => Math.max(0, p - 1)), disabled: page === 0, className: "px-3 py-1 bg-slate-700 rounded disabled:opacity-50 hover:bg-slate-600 transition-colors" }, "Назад"), React.createElement('span', { className: "px-4 py-1 bg-slate-800 rounded-md" }, `Страница ${page + 1} из ${pages}`), React.createElement('button', { onClick: () => setPage(p => Math.min(pages - 1, p + 1)), disabled: page === pages - 1, className: "px-3 py-1 bg-slate-700 rounded disabled:opacity-50 hover:bg-slate-600 transition-colors" }, "Вперед"), React.createElement('button', { onClick: () => setPage(pages - 1), disabled: page === pages - 1, className: "px-3 py-1 bg-slate-700 rounded disabled:opacity-50 hover:bg-slate-600 transition-colors" }, "»") ) ) ); }; if (error) return React.createElement(ErrorDisplay, { error, onFileUpload: handleManualUpload }); return React.createElement('div', { className: "space-y-8" }, React.createElement(FilterBar, { onDatePresetChange: (p) => { setDatePreset(p); setCustomDateRange({ start: null, end: null }); }, activePreset: datePreset, customDateRange, onCustomDateChange: (type, date) => { setCustomDateRange(prev => ({ ...prev, [type]: date })); setDatePreset('custom'); }, managers, selectedManager, onManagerChange: setSelectedManager, tags, selectedTag, onTagChange: setSelectedTag }), React.createElement('div', { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6" }, React.createElement(KpiCard, { title: "Всего лидов", value: totals.leadCount.toLocaleString('ru-RU'), description: "Общее количество лидов за период", isLoading }), React.createElement(KpiCard, { title: "Активных лидов", value: totals.activeLeads.toLocaleString('ru-RU'), description: "Лиды, которые еще в работе", isLoading }), React.createElement(KpiCard, { title: "Конверсия в успех", value: `${totals.conversionRate.toFixed(2)}%`, description: `Доля лидов в статусе "${FINAL_STAGE_NAME}"`, isLoading }), React.createElement(KpiCard, { title: "Средний цикл сделки", value: `${totals.avgLeadLifetime.toFixed(1)} дн.`, description: "Среднее время от создания до закрытия", isLoading }) ), React.createElement('div', { className: "grid grid-cols-1 lg:grid-cols-3 gap-8" }, React.createElement('div', { className: "lg:col-span-2" }, React.createElement(Panel, { title: "Воронка по этапам", isLoading }, React.createElement(TopItemsChart, { data: chartData.funnel, color: "#8b5cf6", name: "Лиды" })) ), React.createElement(Panel, { title: "Анализ от Gemini", icon: React.createElement(SparklesIcon), isLoading: isLoading || isAnalyzing }, isAnalyzing ? React.createElement('p', null, 'Gemini анализирует данные...') : React.createElement('div', { className: "prose prose-sm prose-invert text-slate-300 max-w-none", dangerouslySetInnerHTML: { __html: formattedStaticAnalysis } }) ) ), React.createElement(InteractiveAiPanel, { data: filteredData, isLoading }), React.createElement('div', { className: "grid grid-cols-1 lg:grid-cols-2 gap-8" }, React.createElement(Panel, { title: "Топ-5 менеджеров", isLoading }, React.createElement(TopItemsChart, { data: chartData.topCreators, color: "#22d3ee", name: "Лиды" })), React.createElement(Panel, { title: "Топ-10 тегов", isLoading }, React.createElement(TopItemsChart, { data: chartData.topTags, color: "#a78bfa", name: "Лиды" })) ), React.createElement(Panel, { title: "Динамика создания лидов", isLoading }, React.createElement(LeadsOverTimeChart, { data: chartData.leadsOverTime, color: "#6366f1", name: "Лиды" })), React.createElement(Panel, { title: `Таблица сделок (${filteredData.length})` }, React.createElement(DealsDataTable, { data: filteredData })) ); }; const YandexDirectDashboard = () => { const { data: allData, isLoading, error, refreshData, handleManualUpload } = useYandexDirectData(); const [analysis, setAnalysis] = useState(''); const [isAnalyzing, setIsAnalyzing] = useState(false); useEffect(() => { if (!isLoading && allData.length > 0) { setIsAnalyzing(true); generateYandexDirectAnalysis(allData).then(setAnalysis).finally(() => setIsAnalyzing(false)); } }, [allData, isLoading]); const totals = useMemo(() => { if (isLoading || allData.length === 0) return { totalCost: 0, totalClicks: 0, totalImpressions: 0, avgCtr: 0, totalConversions: 0, avgCpa: 0 }; const totalCost = allData.reduce((s, i) => s + i.cost, 0); const totalClicks = allData.reduce((s, i) => s + i.clicks, 0); const totalImpressions = allData.reduce((s, i) => s + i.impressions, 0); const totalConversions = allData.reduce((s, i) => s + i.conversions, 0); return { totalCost, totalClicks, totalImpressions, avgCtr: totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0, totalConversions, avgCpa: totalConversions > 0 ? totalCost / totalConversions : 0, }; }, [allData, isLoading]); const chartData = useMemo(() => { if (allData.length === 0) return { performance: [], topCost: [], topClicks: [], cheapestCpa: [], mostExpensiveCpa: [] }; // FIX: Explicitly typed the accumulator in reduce to fix type inference issues, resolving downstream errors. const performanceByDate = allData.reduce((acc: { [key: string]: { date: string, impressions: number, clicks: number, cost: number } }, item) => { if (!acc[item.dateString]) acc[item.dateString] = { date: item.dateString, impressions: 0, clicks: 0, cost: 0 }; acc[item.dateString].impressions += item.impressions; acc[item.dateString].clicks += item.clicks; acc[item.dateString].cost += item.cost; return acc; }, {}); const campaignsWithConversions = allData.filter(c => c.conversions > 0); return { performance: Object.values(performanceByDate).sort((a, b) => parse(a.date, 'dd.MM.yyyy', new Date()).getTime() - parse(b.date, 'dd.MM.yyyy', new Date()).getTime()), topCost: [...allData].sort((a, b) => b.cost - a.cost).slice(0, 7).map(c => ({ name: c.campaign, value: c.cost })), topClicks: [...allData].sort((a, b) => b.clicks - a.clicks).slice(0, 7).map(c => ({ name: c.campaign, value: c.clicks })), cheapestCpa: campaignsWithConversions.sort((a, b) => a.cpa - b.cpa).slice(0, 7).map(c => ({ name: c.campaign, value: c.cpa })), mostExpensiveCpa: campaignsWithConversions.sort((a, b) => b.cpa - a.cpa).slice(0, 7).map(c => ({ name: c.campaign, value: c.cpa })), }; }, [allData]); const formattedAnalysis = useMemo(() => { return analysis.split('\n').map(line => { if (line.startsWith('## ')) return `

${line.substring(3)}

`; if (line.startsWith('* ')) return `
  • ${line.substring(2).replace(/\*\*(.*?)\*\*/g, '$1')}
`; return `

${line.replace(/\*\*(.*?)\*\*/g, '$1')}

`; }).join(''); }, [analysis]); if (error) return React.createElement(ErrorDisplay, { error, onFileUpload: handleManualUpload }); const currencyFormatter = (value) => `${value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽`; return React.createElement('div', { className: "space-y-8" }, React.createElement('div', { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6" }, React.createElement(KpiCard, { title: "Общий расход", value: currencyFormatter(totals.totalCost), description: "Сумма затрат на рекламу", isLoading }), React.createElement(KpiCard, { title: "Всего кликов", value: totals.totalClicks.toLocaleString('ru-RU'), description: "Переходы по объявлениям", isLoading }), React.createElement(KpiCard, { title: "Всего показов", value: totals.totalImpressions.toLocaleString('ru-RU'), description: "Сколько раз видели рекламу", isLoading }), React.createElement(KpiCard, { title: "Средний CTR", value: `${totals.avgCtr.toFixed(2)}%`, description: "Кликабельность объявлений", isLoading }), React.createElement(KpiCard, { title: "Всего конверсий", value: totals.totalConversions.toLocaleString('ru-RU'), description: "Количество целевых действий", isLoading }), React.createElement(KpiCard, { title: "Средняя цена цели", value: currencyFormatter(totals.avgCpa), description: "Стоимость одной конверсии", isLoading }), ), React.createElement('div', { className: "grid grid-cols-1 lg:grid-cols-3 gap-8" }, React.createElement('div', { className: "lg:col-span-2" }, React.createElement(Panel, { title: "Эффективность по дням", isLoading }, React.createElement(PerformanceOverTimeChart, { data: chartData.performance })) ), React.createElement(Panel, { title: "Анализ от Gemini", icon: React.createElement(SparklesIcon), isLoading: isLoading || isAnalyzing }, isAnalyzing ? React.createElement('p', null, 'Gemini анализирует данные...') : React.createElement('div', { className: "prose prose-sm prose-invert text-slate-300 max-w-none", dangerouslySetInnerHTML: { __html: formattedAnalysis } }) ) ), React.createElement('div', { className: "grid grid-cols-1 lg:grid-cols-2 gap-8" }, React.createElement(Panel, { title: "Топ-7 кампаний по расходу", isLoading }, React.createElement(TopItemsChart, { data: chartData.topCost, color: "#ef4444", name: "Расход", formatter: currencyFormatter })), React.createElement(Panel, { title: "Топ-7 кампаний по кликам", isLoading }, React.createElement(TopItemsChart, { data: chartData.topClicks, color: "#3b82f6", name: "Клики" })) ), React.createElement('div', { className: "grid grid-cols-1 lg:grid-cols-2 gap-8" }, React.createElement(Panel, { title: "Топ-7 кампаний с дешевыми заявками", isLoading }, React.createElement(TopItemsChart, { data: chartData.cheapestCpa, color: "#22c55e", name: "Цена цели", formatter: currencyFormatter })), React.createElement(Panel, { title: "Топ-7 кампаний с дорогими заявками", isLoading }, React.createElement(TopItemsChart, { data: chartData.mostExpensiveCpa, color: "#f97316", name: "Цена цели", formatter: currencyFormatter })) ) ); }; const App = () => { const [activeTab, setActiveTab] = useState('yandex'); const [lastUpdated, setLastUpdated] = useState(new Date()); const [isRefreshing, setIsRefreshing] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const handleRefresh = useCallback(() => { setIsRefreshing(true); setRefreshKey(prev => prev + 1); setLastUpdated(new Date()); // Simulate refresh end setTimeout(() => setIsRefreshing(false), 1500); }, []); const TabButton = ({ name, label, activeTab, setActiveTab }) => { const isActive = activeTab === name; return React.createElement('button', { onClick: () => setActiveTab(name), className: `px-1 py-4 text-sm font-medium border-b-2 transition-colors ${isActive ? 'border-violet-500 text-violet-400' : 'border-transparent text-slate-400 hover:border-slate-500 hover:text-slate-300'}` }, label); }; return React.createElement('div', { className: "p-4 sm:p-6 lg:p-8" }, React.createElement(Header, { onRefresh: handleRefresh, isRefreshing, lastUpdated }), React.createElement('div', { className: "mt-6" }, React.createElement('div', { className: "border-b border-slate-700" }, React.createElement('nav', { className: "-mb-px flex space-x-6", "aria-label": "Tabs" }, React.createElement(TabButton, { name: "funnel", label: "Анализ Воронки", activeTab, setActiveTab }), React.createElement(TabButton, { name: "yandex", label: "Яндекс.Директ", activeTab, setActiveTab }) ) ) ), React.createElement('div', { className: "mt-8" }, activeTab === 'funnel' ? React.createElement(FunnelDashboard, { key: `funnel-${refreshKey}` }) : React.createElement(YandexDirectDashboard, { key: `yandex-${refreshKey}` }) ) ); }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(React.createElement(App));