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