fix
This commit is contained in:
@@ -160,7 +160,7 @@
|
|||||||
<div class="text-[14px] font-semibold text-green-700">{{ marketYoYGrowthValue }}</div>
|
<div class="text-[14px] font-semibold text-green-700">{{ marketYoYGrowthValue }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[260px]">
|
<div class="h-[260px]">
|
||||||
<Chart type="line" :data="marketDynamicsLineData" :options="marketDynamicsLineOptions" />
|
<Chart type="line" :data="marketDynamicsLineData" :options="marketDynamicsLineOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="mt-4 pt-4 border-t border-slate-200/70">
|
<!-- <div class="mt-4 pt-4 border-t border-slate-200/70">
|
||||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[240px]">
|
<div class="h-[240px]">
|
||||||
<Chart type="bar" :data="ageBarData" :options="compactBarOptions" />
|
<Chart type="bar" :data="ageBarData" :options="compactBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[220px]">
|
<div class="h-[220px]">
|
||||||
<Chart type="bar" :data="incomeBarData" :options="compactBarOptions" />
|
<Chart type="bar" :data="incomeBarData" :options="compactBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
<div class="text-[12px] text-slate-500">Бенчмарк</div>
|
<div class="text-[12px] text-slate-500">Бенчмарк</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[280px]">
|
<div class="h-[280px]">
|
||||||
<Chart type="bar" :data="visibilityBarData" :options="visibilityBarOptions" />
|
<Chart type="bar" :data="visibilityBarData" :options="visibilityBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,7 +437,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[260px]">
|
<div class="h-[260px]">
|
||||||
<Chart type="doughnut" :data="queryTypesDonutData" :options="queryTypesDonutOptions" />
|
<Chart type="doughnut" :data="queryTypesDonutData" :options="queryTypesDonutOptions" :plugins="donutPlugins" />
|
||||||
</div>
|
</div>
|
||||||
<!-- Метрики под диаграммой -->
|
<!-- Метрики под диаграммой -->
|
||||||
<div v-if="queryTypesMetrics.length > 0" class="mt-4 pt-4 border-t border-slate-200/70">
|
<div v-if="queryTypesMetrics.length > 0" class="mt-4 pt-4 border-t border-slate-200/70">
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[220px]">
|
<div class="h-[220px]">
|
||||||
<Chart type="bar" :data="decisionFactorsBarData" :options="compactBarOptions" />
|
<Chart type="bar" :data="decisionFactorsBarData" :options="compactBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[200px]">
|
<div class="h-[200px]">
|
||||||
<Chart type="bar" :data="refusalReasonsBarData" :options="compactBarOptions" />
|
<Chart type="bar" :data="refusalReasonsBarData" :options="compactBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -528,37 +528,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-12 lg:col-span-8">
|
<div class="col-span-12">
|
||||||
<PptFunnel
|
|
||||||
:steps="pptFunnelSteps"
|
|
||||||
title="Потери на воронке"
|
|
||||||
description="Абсолютные показатели воронки: сколько человек пришло с рекламы, сколько стали лидами, сколько купили. Визуализирует потери клиентов на каждом этапе."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-12 lg:col-span-4">
|
|
||||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4 h-full">
|
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4 h-full">
|
||||||
<div class="flex items-center gap-1.5 mb-3">
|
<div class="flex items-center gap-1.5 mb-2">
|
||||||
<div class="text-[16px] font-semibold text-slate-800">Основные проблемы на воронке</div>
|
<div class="text-[16px] font-semibold text-slate-800">Воронка продаж</div>
|
||||||
<i
|
<i
|
||||||
class="pi pi-question-circle text-[12px] text-slate-400 cursor-help"
|
class="pi pi-question-circle text-[12px] text-slate-400 cursor-help"
|
||||||
v-tooltip.top="'Текстовая расшифровка причин потерь клиентов на каждом этапе. Помогает понять, где именно теряются клиенты и что нужно исправить.'"
|
v-tooltip.top="'Абсолютные показатели воронки: сколько человек пришло с рекламы, сколько стали лидами, сколько купили. Визуализирует потери клиентов на каждом этапе.'"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
<div class="h-[320px]">
|
||||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
<Chart type="bar" :data="funnelBarData" :options="funnelBarOptions" :plugins="barPlugins" />
|
||||||
<div class="col-span-5 px-3 py-2">Этап</div>
|
|
||||||
<div class="col-span-7 px-3 py-2">Проблема</div>
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-slate-200/70">
|
|
||||||
<div class="grid grid-cols-12 px-3 py-3">
|
|
||||||
<div class="col-span-5 text-[13px] text-slate-800">Лид → Запись</div>
|
|
||||||
<div class="col-span-7 text-[13px] text-slate-600">{{ report?.sections?.customer_journey?.funnel_bottlenecks?.lead_to_booking }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-12 px-3 py-3">
|
|
||||||
<div class="col-span-5 text-[13px] text-slate-800">Запись → Оплата</div>
|
|
||||||
<div class="col-span-7 text-[13px] text-slate-600">{{ report?.sections?.customer_journey?.funnel_bottlenecks?.booking_to_payment }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -615,7 +595,7 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[280px]">
|
<div class="h-[280px]">
|
||||||
<Chart type="bar" :data="channelSharesBarData" :options="channelSharesBarOptions" />
|
<Chart type="bar" :data="channelSharesBarData" :options="channelSharesBarOptions" :plugins="barPlugins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1265,16 +1245,12 @@ const ageRows = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const compactBarOptions = computed(() => ({
|
const barValuesPlugin = {
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
barValues: {
|
|
||||||
id: 'barValues',
|
id: 'barValues',
|
||||||
afterDatasetsDraw(chart) {
|
afterDatasetsDraw(chart) {
|
||||||
const { ctx, data } = chart;
|
const { ctx, data } = chart;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = 'bold 12px Inter, system-ui, sans-serif';
|
ctx.font = 'bold 15px Inter, system-ui, sans-serif';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
data.datasets.forEach((dataset, i) => {
|
data.datasets.forEach((dataset, i) => {
|
||||||
@@ -1284,33 +1260,35 @@ const compactBarOptions = computed(() => ({
|
|||||||
const value = dataset.data[index];
|
const value = dataset.data[index];
|
||||||
if (value !== null && value !== undefined && value !== 0) {
|
if (value !== null && value !== undefined && value !== 0) {
|
||||||
const unit = dataset.unit || '';
|
const unit = dataset.unit || '';
|
||||||
const label = value.toLocaleString() + unit;
|
let label = value.toLocaleString();
|
||||||
|
if (typeof value === 'number' && !Number.isInteger(value) && value < 10) {
|
||||||
|
label = value.toFixed(2);
|
||||||
|
}
|
||||||
|
label += unit;
|
||||||
const isHorizontal = chart.options.indexAxis === 'y';
|
const isHorizontal = chart.options.indexAxis === 'y';
|
||||||
|
|
||||||
if (isHorizontal) {
|
if (isHorizontal) {
|
||||||
const barWidth = Math.abs(bar.x - bar.base);
|
const barWidth = Math.abs(bar.x - bar.base);
|
||||||
if (barWidth > 40) {
|
if (barWidth > 60) {
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.font = 'bold 12px Inter, system-ui, sans-serif';
|
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(label, bar.x - 12, bar.y);
|
ctx.fillText(label, bar.x - 12, bar.y);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#64748b';
|
ctx.fillStyle = '#64748b';
|
||||||
ctx.font = 'bold 11px Inter, system-ui, sans-serif';
|
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(label, bar.x + 10, bar.y);
|
ctx.fillText(label, bar.x + 10, bar.y);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const barHeight = Math.abs(bar.base - bar.y);
|
const barHeight = Math.abs(bar.base - bar.y);
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
if (barHeight > 25) {
|
if (barHeight > 35) {
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
ctx.font = 'bold 18px Inter, system-ui, sans-serif';
|
||||||
ctx.fillText(label, bar.x, bar.y + 16);
|
ctx.fillText(label, bar.x, bar.y + 20);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#64748b';
|
ctx.fillStyle = '#64748b';
|
||||||
ctx.font = 'bold 12px Inter, system-ui, sans-serif';
|
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
||||||
ctx.fillText(label, bar.x, bar.y - 12);
|
ctx.fillText(label, bar.x, bar.y - 15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1319,7 +1297,57 @@ const compactBarOptions = computed(() => ({
|
|||||||
});
|
});
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const donutPercentPlugin = {
|
||||||
|
id: 'donutPercent',
|
||||||
|
afterDatasetsDraw(chart) {
|
||||||
|
const ctx = chart.ctx;
|
||||||
|
const data = chart.data.datasets[0]?.data || [];
|
||||||
|
const total = data.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (total === 0) return;
|
||||||
|
|
||||||
|
chart.data.datasets.forEach((dataset, i) => {
|
||||||
|
const meta = chart.getDatasetMeta(i);
|
||||||
|
meta.data.forEach((element, index) => {
|
||||||
|
const value = dataset.data[index];
|
||||||
|
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
if (parseFloat(percentage) < 3) return;
|
||||||
|
|
||||||
|
const arc = element;
|
||||||
|
const { x, y, startAngle, endAngle, innerRadius, outerRadius } = arc;
|
||||||
|
|
||||||
|
const angle = (startAngle + endAngle) / 2;
|
||||||
|
const radius = (innerRadius + outerRadius) / 2;
|
||||||
|
|
||||||
|
const centerX = x + Math.cos(angle) * radius;
|
||||||
|
const centerY = y + Math.sin(angle) * radius;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
ctx.shadowBlur = 4;
|
||||||
|
ctx.shadowOffsetX = 1;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
ctx.fillText(`${percentage}%`, centerX, centerY);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const barPlugins = [barValuesPlugin];
|
||||||
|
const donutPlugins = [donutPercentPlugin];
|
||||||
|
|
||||||
|
const compactBarOptions = computed(() => ({
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { display: false, drawBorder: false } },
|
x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { display: false, drawBorder: false } },
|
||||||
@@ -1831,6 +1859,43 @@ const channelSharesTableRows = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const funnelBarData = computed(() => {
|
||||||
|
const fm = report.value?.sections?.customer_journey?.funnel_metrics ?? {};
|
||||||
|
const labels = ['Охват/Трафик', 'Визиты на сайт', 'Лиды', 'Записи/Бронь', 'Оплаты'];
|
||||||
|
const values = [fm.ad_traffic, fm.site_visitors, fm.leads, fm.bookings, fm.payments].map((v) => {
|
||||||
|
if (typeof v === 'string') return Number(v.replace(/[^\d.]/g, ''));
|
||||||
|
return Number(v) || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: values,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: (context) => {
|
||||||
|
const { chart } = context;
|
||||||
|
const { ctx, chartArea } = chart;
|
||||||
|
if (!chartArea) return PPT_BLUE;
|
||||||
|
return makeVerticalGradient(ctx, chartArea, PPT_GRADIENT_BLUE_START, PPT_GRADIENT_BLUE_END);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const funnelBarOptions = computed(() => {
|
||||||
|
const base = compactBarOptions.value;
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
indexAxis: 'y',
|
||||||
|
scales: {
|
||||||
|
x: { ...base.scales.x, grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false } },
|
||||||
|
y: { ...base.scales.y, grid: { display: false, drawBorder: false } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const channelSharesBarOptions = computed(() => {
|
const channelSharesBarOptions = computed(() => {
|
||||||
const base = compactBarOptions.value;
|
const base = compactBarOptions.value;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user