diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue
index 52b8578..4a498b8 100644
--- a/src/views/pages/MarketingAnalysis.vue
+++ b/src/views/pages/MarketingAnalysis.vue
@@ -588,14 +588,17 @@
-
Доли каналов
+
Эффективность каналов
-
+
+
+ Нет данных об эффективности каналов
+
@@ -1943,38 +1946,176 @@ const channelSharesBarOptions = computed(() => {
};
});
+// Функция для извлечения числового значения из стоимости (например, "2500 KZT" -> 2500)
+function extractCostNumber(costStr) {
+ if (!costStr || costStr === '—') return 0;
+ const num = Number(String(costStr).replace(/[^\d.]/g, ''));
+ return Number.isFinite(num) ? num : 0;
+}
+
+// График эффективности каналов (стоимость привлечения)
+const channelEfficiencyBarData = computed(() => {
+ const eff = report.value?.sections?.acquisition_channels?.channel_efficiency ?? {};
+
+ // Маппинг ключей каналов на читаемые названия
+ const channelLabels = {
+ 'yandex': 'Яндекс',
+ 'yandex_direct': 'Яндекс Директ',
+ 'seo': 'SEO',
+ 'retargeting': 'Ретаргет',
+ 'vkontakte': 'VKontakte',
+ 'youtube': 'YouTube',
+ 'instagram': 'Instagram',
+ 'tiktok': 'TikTok',
+ 'kaspi': 'Kaspi',
+ 'google_search': 'Google Поиск',
+ '2gis': '2GIS',
+ 'google_ads': 'Google Ads',
+ 'facebook': 'Facebook'
+ };
+
+ function getChannelLabel(key) {
+ return channelLabels[key] || key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+ }
+
+ // Цвета для разных каналов
+ const channelColors = [
+ PPT_BLUE,
+ PPT_ORANGE,
+ PPT_GREEN,
+ '#60A5FA',
+ '#A855F7',
+ '#EC4899',
+ '#F59E0B',
+ '#10B981',
+ '#3B82F6',
+ '#8B5CF6'
+ ];
+
+ // Получаем все каналы из данных
+ const items = Object.keys(eff)
+ .map((key, index) => {
+ const row = eff[key];
+ if (!row || !row.cost) return null;
+ return {
+ key: key,
+ label: getChannelLabel(key),
+ cost: row.cost,
+ costValue: extractCostNumber(row.cost),
+ color: channelColors[index % channelColors.length]
+ };
+ })
+ .filter(Boolean)
+ .sort((a, b) => a.costValue - b.costValue); // Сортируем по стоимости (от меньшей к большей)
+
+ return {
+ labels: items.map((i) => i.label),
+ datasets: [
+ {
+ data: items.map((i) => i.costValue),
+ borderRadius: 12,
+ borderSkipped: false,
+ backgroundColor: (context) => {
+ const { chart, dataIndex } = context;
+ const { ctx, chartArea } = chart;
+ const base = items[dataIndex]?.color ?? PPT_BLUE;
+ if (!chartArea) return hexToRgba(base, 0.55);
+ return makeVerticalGradient(ctx, chartArea, hexToRgba(base, 0.88), hexToRgba(base, 0.24));
+ }
+ }
+ ]
+ };
+});
+
+const channelEfficiencyBarOptions = computed(() => {
+ const base = compactBarOptions.value;
+ return {
+ ...base,
+ indexAxis: 'y',
+ scales: {
+ x: {
+ ...base.scales.x,
+ grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false },
+ ticks: {
+ callback: function(value) {
+ return value.toLocaleString('ru-RU') + ' KZT';
+ }
+ }
+ },
+ y: { ...base.scales.y, grid: { display: false, drawBorder: false } }
+ },
+ plugins: {
+ ...base.plugins,
+ tooltip: {
+ ...base.plugins?.tooltip,
+ callbacks: {
+ label: function(context) {
+ return context.parsed.x.toLocaleString('ru-RU') + ' KZT';
+ }
+ }
+ }
+ }
+ };
+});
+
const channelEfficiencyRows = computed(() => {
const eff = report.value?.sections?.acquisition_channels?.channel_efficiency ?? {};
- const order = [
- { key: 'yandex', label: 'Яндекс' },
- { key: 'seo', label: 'SEO (сиб.тайт)' },
- { key: 'retargeting', label: 'Ретаргет' },
- { key: 'vkontakte', label: 'VKontakte' },
- { key: 'youtube', label: 'YouTube' },
- { key: 'instagram', label: 'Instagram' }
- ];
+
+ // Маппинг ключей каналов на читаемые названия
+ const channelLabels = {
+ 'yandex': 'Яндекс',
+ 'yandex_direct': 'Яндекс Директ',
+ 'seo': 'SEO',
+ 'retargeting': 'Ретаргет',
+ 'vkontakte': 'VKontakte',
+ 'youtube': 'YouTube',
+ 'instagram': 'Instagram',
+ 'tiktok': 'TikTok',
+ 'kaspi': 'Kaspi',
+ 'google_search': 'Google Поиск',
+ '2gis': '2GIS',
+ 'google_ads': 'Google Ads',
+ 'facebook': 'Facebook'
+ };
+
+ function getChannelLabel(key) {
+ return channelLabels[key] || key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+ }
function statusClass(status) {
const s = String(status || '').toLowerCase();
- if (s.includes('эффектив')) return 'text-green-700';
- if (s.includes('удовлетвор')) return 'text-amber-700';
- if (s.includes('завыш')) return 'text-red-700';
+ // Обработка английских статусов
+ if (s.includes('very high') || s.includes('очень высок') || s.includes('высок')) return 'text-green-700 font-semibold';
+ if (s.includes('high') || s.includes('эффектив')) return 'text-green-700';
+ if (s.includes('medium') || s.includes('средн') || s.includes('удовлетвор')) return 'text-amber-700';
+ if (s.includes('low') || s.includes('низк') || s.includes('завыш')) return 'text-red-700';
+ if (s.includes('local') || s.includes('локальн')) return 'text-blue-700';
return 'text-slate-600';
}
- return order
- .map((o) => {
- const row = eff[o.key];
+ // Получаем все каналы из данных и преобразуем в массив
+ return Object.keys(eff)
+ .map((key) => {
+ const row = eff[key];
if (!row) return null;
return {
- key: o.key,
- label: o.label,
+ key: key,
+ label: getChannelLabel(key),
cost: row.cost ?? '—',
status: row.status ?? '—',
statusClass: statusClass(row.status)
};
})
- .filter(Boolean);
+ .filter(Boolean)
+ .sort((a, b) => {
+ // Сортируем по статусу: Very High -> High -> Medium -> Local -> остальные
+ const statusOrder = { 'very high': 1, 'high': 2, 'medium': 3, 'local': 4 };
+ const aStatus = String(a.status || '').toLowerCase();
+ const bStatus = String(b.status || '').toLowerCase();
+ const aOrder = statusOrder[aStatus] || 5;
+ const bOrder = statusOrder[bStatus] || 5;
+ return aOrder - bOrder;
+ });
});
// Economics tables
diff --git a/src/views/pages/marketing/MarketingAnalysis.vue b/src/views/pages/marketing/MarketingAnalysis.vue
index 05d8f26..a7b88b4 100644
--- a/src/views/pages/marketing/MarketingAnalysis.vue
+++ b/src/views/pages/marketing/MarketingAnalysis.vue
@@ -110,6 +110,13 @@
{{ errors.weakSide }}
+
+
+
+ {{ errors.marketingBudget }}
+ Укажите ваш бюджет на маркетинг (опционально)
+
+
@@ -800,6 +807,7 @@ import markedKatex from 'marked-katex-extension';
import Button from 'primevue/button';
import Chart from 'primevue/chart';
import Checkbox from 'primevue/checkbox';
+import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import MultiSelect from 'primevue/multiselect';
import ProgressSpinner from 'primevue/progressspinner';
@@ -862,6 +870,7 @@ const formData = ref({
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
+ marketingBudget: null,
analysisType: []
});
@@ -1313,6 +1322,9 @@ const handleSubmit = async () => {
if (formData.value.weakSide && formData.value.weakSide.trim()) {
requestData.weakSide = formData.value.weakSide.trim();
}
+ if (formData.value.marketingBudget !== null && formData.value.marketingBudget !== undefined) {
+ requestData.marketing_budget = formData.value.marketingBudget;
+ }
const result = await MarketingService.startAnalysis(requestData, { generateV2: true });