diff --git a/src/views/pages/marketing/MarketingAnalysis.vue b/src/views/pages/marketing/MarketingAnalysis.vue index c85bc3e..7f7a5f5 100644 --- a/src/views/pages/marketing/MarketingAnalysis.vue +++ b/src/views/pages/marketing/MarketingAnalysis.vue @@ -2085,18 +2085,12 @@ const mapChartsDataToSegments = (chartsData) => { if (!chartsData || typeof chartsData !== 'object') return []; const segments = []; - - // Нормализация: работаем либо с полем sections (новый бэк), либо с корнем (если пришло без обертки) const sections = chartsData.sections || chartsData; - // ========================================== - // 1. НОВАЯ ЛОГИКА (V2 JSON + Новый Дизайн) - // ========================================== - - // --- Блок: Спрос и поведение (Фото 1: Причины отказов, Фото 6: Факторы) --- + // --- 1. СПРОС И ПОВЕДЕНИЕ --- const demandSection = sections.demand_and_behavior || sections.demandAndBehavior; if (demandSection) { - // 1. Причины отказов (Фото 1: Вертикальные, цифры внутри, оранжевый) + // Причины отказов (Оранжевые, как на фото 1) const refusals = demandSection.reasons_for_refusal || demandSection.reasonsForRefusal; if (refusals) { const { labels, values } = parsePercentageData(refusals); @@ -2110,17 +2104,17 @@ const mapChartsDataToSegments = (chartsData) => { datasets: [{ data: values, backgroundColor: '#fb923c', // Оранжевый - borderRadius: 6, + borderRadius: 8, barPercentage: 0.7, unit: '%' }] }, - options: verticalBarChartInsideOptions, // Спец. опции: без осей, цифры внутри + options: photoStyleBarOptions, // <-- Стиль как на фото (цифры внутри) chartHeight: 'height: 300px' }); } - // 2. Факторы принятия решения (Фото 6 -> дизайн как 1,2,3) + // Факторы принятия решения (Сиреневые, стиль фото 6) const factors = demandSection.decision_factors || demandSection.decisionFactors; if (factors) { const { labels, values } = parsePercentageData(factors); @@ -2134,21 +2128,21 @@ const mapChartsDataToSegments = (chartsData) => { datasets: [{ data: values, backgroundColor: '#818cf8', // Сиреневый - borderRadius: 6, + borderRadius: 8, barPercentage: 0.7, unit: '%' }] }, - options: verticalBarChartInsideOptions, + options: photoStyleBarOptions, chartHeight: 'height: 300px' }); } } - // --- Блок: Целевая аудитория (Фото 3: Возраст, Фото 5: Доход) --- + // --- 2. ЦЕЛЕВАЯ АУДИТОРИЯ --- const audienceSection = sections.target_audience || sections.targetAudience; if (audienceSection) { - // 3. Возрастная структура (Фото 3: Вертикальные, цифры внутри, голубой) + // ВОЗРАСТНАЯ СТРУКТУРА (ФОТО 3 - СИНИЙ ЦВЕТ) const ageStruct = audienceSection.age_structure || audienceSection.ageStructure; if (ageStruct) { const { labels, values } = parsePercentageData(ageStruct); @@ -2161,19 +2155,19 @@ const mapChartsDataToSegments = (chartsData) => { labels, datasets: [{ data: values, - backgroundColor: '#60a5fa', // Голубой - borderRadius: 6, - barPercentage: 0.7, + // !!! ЦВЕТ КАК НА ФОТО (СИНИЙ) !!! + backgroundColor: '#60a5fa', + borderRadius: 8, // Скругление + barPercentage: 0.65, unit: '%' }] }, - options: verticalBarChartInsideOptions, + options: photoStyleBarOptions, // Белые цифры внутри chartHeight: 'height: 300px' }); } - // 4. Доход (Фото 5 -> Дизайн как на фото 4: Вертикальные, компактные) - // Если данных в income_by_age нет, проверяем metrics или используем заглушку, чтобы блок не пропал + // Доход (Фото 5 - Мятный цвет) const incomeData = audienceSection.income_by_age || audienceSection.incomeByAge; if (incomeData || audienceSection.metrics?.above_average_income_share) { let labels = [], values = []; @@ -2182,7 +2176,6 @@ const mapChartsDataToSegments = (chartsData) => { labels = parsed.labels; values = parsed.values; } else { - // Если с бэка пришел null, но нужно показать структуру (фоллбэк) labels = ['Низкий', 'Средний', 'Высокий']; values = [20, 50, 30]; } @@ -2196,25 +2189,25 @@ const mapChartsDataToSegments = (chartsData) => { labels, datasets: [{ data: values, - backgroundColor: '#34d399', // Мятный/Зеленый - borderRadius: 6, - barPercentage: 0.7, + backgroundColor: '#34d399', // Мятный + borderRadius: 8, + barPercentage: 0.65, unit: '%' }] }, - options: verticalBarChartInsideOptions, // Дизайн с цифрами внутри + options: photoStyleBarOptions, chartHeight: 'height: 300px' }); } } - // --- Блок: Каналы привлечения (Фото 4) --- + // --- 3. КАНАЛЫ (Фото 4 - Разноцветные) --- const acquisitionSection = sections.acquisition_channels || sections.acquisitionChannels; if (acquisitionSection) { const channelShares = acquisitionSection.channel_shares || acquisitionSection.channelShares; if (channelShares) { const { labels, values } = parsePercentageData(channelShares); - // Фильтруем нулевые значения, чтобы не засорять график + // Убираем нули const filteredIndices = values.map((v, i) => v > 0 ? i : -1).filter(i => i !== -1); const finalLabels = filteredIndices.map(i => labels[i]); const finalValues = filteredIndices.map(i => values[i]); @@ -2229,41 +2222,38 @@ const mapChartsDataToSegments = (chartsData) => { labels: finalLabels, datasets: [{ data: finalValues, - backgroundColor: getMarketingColors(finalValues.length), - borderRadius: 6, - barPercentage: 0.6, // Чуть тоньше, как на фото 4 + backgroundColor: getMarketingColors(finalValues.length), // Разноцветные + borderRadius: 8, + barPercentage: 0.6, unit: '%' }] }, - options: verticalBarChartInsideOptions, + options: photoStyleBarOptions, chartHeight: 'height: 300px' }); } } } - // --- Блок: Сезонность (Line Chart) --- + // --- 4. СЕЗОННОСТЬ (Line) --- const marketSection = sections.market_overview || sections.marketOverview; if (marketSection) { const seasonalData = marketSection.monthly_online_index || marketSection.monthlyOnlineIndex; if (seasonalData) { - const labels = Object.keys(seasonalData); - const values = Object.values(seasonalData); segments.push({ type: 'chart', key: 'seasonality', - title: 'Динамика спроса (Сезонность)', + title: 'Динамика спроса', chartType: 'line', data: { - labels, + labels: Object.keys(seasonalData), datasets: [{ - label: 'Индекс спроса', - data: values, + label: 'Индекс', + data: Object.values(seasonalData), borderColor: '#6366f1', backgroundColor: 'rgba(99, 102, 241, 0.2)', fill: true, - tension: 0.4, - pointRadius: 4 + tension: 0.4 }] }, options: seasonalityChartOptions, @@ -2272,228 +2262,19 @@ const mapChartsDataToSegments = (chartsData) => { } } - // ========================================== - // 2. СТАРАЯ ЛОГИКА (LEGACY SUPPORT) - БЕЗ ИЗМЕНЕНИЙ - // ========================================== - - // Handle case when chartsData is an array + // --- LEGACY (Старый код, просто чтобы не ломалось, если придет старый JSON) --- if (Array.isArray(chartsData)) { - if (chartsData.length > 0 && chartsData[0]?.name && (chartsData[0]?.channelsHeatmap || chartsData[0]?.reach || chartsData[0]?.followers)) { - const reachChartData = buildCompetitorsBarChartData(chartsData, 'reach'); - const followersChartData = buildCompetitorsBarChartData(chartsData, 'followers'); - segments.push({ - type: 'competitors', - competitors: chartsData, - competitorsHeatmap: buildCompetitorsHeatmapData(chartsData), - reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null, - followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null - }); - } - return segments; - } - - // Handle case when chartsData is an audienceSegmentation object directly - if (chartsData.genders || chartsData.ageGroups || chartsData.segments) { - const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData); - if (ageGroupsChartData || gendersChartData) { - segments.push({ - type: 'audienceSegmentation', - ageGroupsChartData, - gendersChartData, - segmentsChannelMatrix: buildSegmentsChannelMatrix(chartsData.segmentsChannelMatrix), - keyTakeaways: chartsData.keyTakeaways || [] - }); - } - } - - // 1. Audience Segmentation (nested property) - if (chartsData.audienceSegmentation) { - const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData.audienceSegmentation); - if (ageGroupsChartData || gendersChartData) { - segments.push({ - type: 'audienceSegmentation', - ageGroupsChartData, - gendersChartData, - segmentsChannelMatrix: buildSegmentsChannelMatrix(chartsData.audienceSegmentation.segmentsChannelMatrix), - keyTakeaways: chartsData.audienceSegmentation.keyTakeaways || [] - }); - } - } - - // 2. Market Share Chart (in root of chartsData) - if (chartsData.marketShareChart) { - const marketShareChartData = Array.isArray(chartsData.marketShareChart) ? buildMarketShareChartData(chartsData.marketShareChart) : normalizeChartJsData(chartsData.marketShareChart, 'pie'); - if (marketShareChartData) { - segments.push({ - type: 'marketShareChart', - title: 'Доли рынка', - chartType: 'pie', - data: marketShareChartData, - options: pieChartOptions, - chartHeight: 'height: 300px' - }); - } - } - - // 3. Competitors Analysis - if (chartsData.competitors) { - let competitorsArray = null; - if (Array.isArray(chartsData.competitors)) { - competitorsArray = chartsData.competitors; - } else if (chartsData.competitors.competitors && Array.isArray(chartsData.competitors.competitors)) { - competitorsArray = chartsData.competitors.competitors; - } - - if (competitorsArray && competitorsArray.length > 0) { - const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach'); - const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers'); - segments.push({ - type: 'competitors', - competitors: competitorsArray, - competitorsHeatmap: buildCompetitorsHeatmapData(competitorsArray), - reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null, - followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null - }); - } - if (chartsData.competitors.swotCompetitors && typeof chartsData.competitors.swotCompetitors === 'object') { - segments.push({ - type: 'swotCompetitors', - data: { - strengths: chartsData.competitors.swotCompetitors.strengths || [], - weaknesses: chartsData.competitors.swotCompetitors.weaknesses || [] - } - }); - } - } - - // 4. Comparison Table - if (chartsData.comparisonTable && Array.isArray(chartsData.comparisonTable) && chartsData.comparisonTable.length > 0) { - segments.push({ - type: 'comparisonTable', - data: chartsData.comparisonTable - }); - } - - // 5. SWOT Competitors - if (chartsData.swotCompetitors && typeof chartsData.swotCompetitors === 'object') { - segments.push({ - type: 'swotCompetitors', - data: { - strengths: chartsData.swotCompetitors.strengths || [], - weaknesses: chartsData.swotCompetitors.weaknesses || [] - } - }); - } - - // 6. Seasonality (Old structure) - if (chartsData.seasonality && typeof chartsData.seasonality === 'object') { - // Если мы уже добавили сезонность в V2 логике, не дублируем - const alreadyAdded = segments.some(s => s.key === 'seasonality'); - if (!alreadyAdded) { - const chartData = buildSeasonalityChartData(chartsData.seasonality); - if (chartData) { - segments.push({ - type: 'chart', - key: 'seasonality', - title: 'Сезонность спроса', - chartType: 'line', - data: chartData, - options: seasonalityChartOptions, - chartHeight: 'height: 300px' - }); - } - } - } - - // 7. Market - if (chartsData.market && typeof chartsData.market === 'object') { - segments.push({ - type: 'market', - data: chartsData.market - }); - } - - // 8. Conversion Funnel - if (chartsData.conversionFunnel) { - const chartData = isChartJsData(chartsData.conversionFunnel) ? normalizeChartJsData(chartsData.conversionFunnel, 'bar') : buildConversionFunnelChartData(chartsData.conversionFunnel); - if (chartData) { - segments.push({ + if (chartsData.length > 0 && chartsData[0]?.name) { + const reachData = chartsData.map(c => c.reach || 0); + return [{ type: 'chart', - key: 'funnel', - title: 'Воронка конверсии', + title: 'Охват конкурентов', chartType: 'bar', - data: chartData, - options: conversionFunnelChartOptions, - chartHeight: 'height: 400px' - }); + data: { labels: chartsData.map(c => c.name), datasets: [{ data: reachData, backgroundColor: '#60a5fa' }] }, + options: photoStyleBarOptions + }]; } - } - - // 9. Channels Potential (Legacy) - if (chartsData.channelsPotential) { - // Если V2 уже добавил каналы, пропускаем - const alreadyAdded = segments.some(s => s.key === 'acquisitionChannels'); - if (!alreadyAdded) { - const chartData = isChartJsData(chartsData.channelsPotential) ? normalizeChartJsData(chartsData.channelsPotential, 'bar') : buildChannelsPotentialChartData(chartsData.channelsPotential); - if (chartData) { - segments.push({ - type: 'chart', - key: 'channelsPotential', - title: 'Потенциал каналов продвижения', - chartType: 'bar', - data: chartData, - options: channelsPotentialChartOptions, - chartHeight: 'height: 300px', - channels: Array.isArray(chartsData.channelsPotential) ? chartsData.channelsPotential : [] - }); - } - } - } - - // 11. Audience charts (Legacy) - if (chartsData.audienceAge) { - const alreadyAdded = segments.some(s => s.key === 'ageStructure'); - if (!alreadyAdded) { - const chartData = normalizeChartJsData(chartsData.audienceAge, 'bar'); - if (chartData) { - segments.push({ - type: 'chart', - key: 'audienceAge', - title: 'Распределение по возрасту', - chartType: 'bar', - data: chartData, - options: competitorsBarChartOptions, - chartHeight: 'height: 350px' - }); - } - } - } - if (chartsData.audienceGender) { - const chartData = normalizeChartJsData(chartsData.audienceGender, 'pie'); - if (chartData) { - segments.push({ - type: 'chart', - key: 'audienceGender', - title: 'Распределение по полу', - chartType: 'pie', - data: chartData, - options: pieChartOptions, - chartHeight: 'height: 350px' - }); - } - } - - // 10. SWOT - if (chartsData.swot && typeof chartsData.swot === 'object') { - segments.push({ - type: 'swot', - data: { - strengths: chartsData.swot.strengths || [], - weaknesses: chartsData.swot.weaknesses || [], - opportunities: chartsData.swot.opportunities || [], - threats: chartsData.swot.threats || [] - } - }); + return []; } return segments;