Add attribution dashboard — CPA by keywords/conversion by managers/LTV by channel

This commit is contained in:
neo committed 2026-06-20 20:48:19 +05:00
1 parent 43d7cf49cf
commit d5fb5b4949
1 file changed
+850
+850
View File
@@ -0,0 +1,850 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI24 — Attribution Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#09090b;--card:#111113;--border:rgba(255,255,255,.06);--border-h:rgba(255,255,255,.1);
--text:#fafafa;--sec:#a1a1aa;--muted:#52525b;
--g:#22c55e;--gd:rgba(34,197,94,.12);
--b:#3b82f6;--bd:rgba(59,130,246,.12);
--a:#f59e0b;--ad:rgba(245,158,11,.12);
--r:#ef4444;--rd:rgba(239,68,68,.12);
--p:#a855f7;--pd:rgba(168,85,247,.12);
--c:#06b6d4;--cd:rgba(6,182,212,.12);
--mono:'JetBrains Mono',monospace;--ease:cubic-bezier(.4,0,.2,1);
}
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;min-height:100vh}
.hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 28px;background:rgba(17,17,19,.85);backdrop-filter:blur(16px);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100}
.logo{font-family:var(--mono);font-weight:700;font-size:15px;display:flex;align-items:center;gap:8px}
.dot{width:8px;height:8px;background:var(--g);border-radius:50%;box-shadow:0 0 8px rgba(34,197,94,.4);animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 8px rgba(34,197,94,.4)}50%{box-shadow:0 0 16px rgba(34,197,94,.15)}}
.hdr-right{display:flex;align-items:center;gap:16px;font-size:12px}
.live{display:flex;align-items:center;gap:6px;color:var(--g);font-weight:600}
.live .dot{width:6px;height:6px}
.clock{font-family:var(--mono);color:var(--muted);font-size:11px}
/* TABS */
.tabs-wrap{display:flex;justify-content:center;padding:32px 24px 0}
.tabs{display:flex;gap:4px;padding:5px;background:var(--card);border:1px solid var(--border);border-radius:14px}
.tab{padding:10px 24px;border:none;background:transparent;color:var(--sec);font-family:var(--mono);font-size:12px;font-weight:500;border-radius:10px;cursor:pointer;transition:all .25s var(--ease);white-space:nowrap;display:flex;align-items:center;gap:6px}
.tab:hover{color:var(--text)}
.tab.active{background:var(--gd);color:var(--g);box-shadow:0 0 0 1px rgba(34,197,94,.2)}
/* BUSINESS SWITCHER */
.bswitch-wrap{display:flex;justify-content:center;padding:16px 24px 0}
.bswitch{display:flex;gap:4px;padding:4px;background:var(--card);border:1px solid var(--border);border-radius:12px}
.bbtn{padding:8px 16px;border:none;background:transparent;color:var(--sec);font-family:var(--mono);font-size:11px;font-weight:500;border-radius:8px;cursor:pointer;transition:all .25s var(--ease);white-space:nowrap}
.bbtn:hover{color:var(--text)}
.bbtn.active{background:var(--bd);color:var(--b);box-shadow:0 0 0 1px rgba(59,130,246,.2)}
/* SECTION */
.section{display:none;padding:20px 24px;max-width:1280px;margin:0 auto;animation:fadeIn .4s ease}
.section.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.s-head{text-align:center;margin-bottom:20px}
.s-head h2{font-size:clamp(1.4rem,3vw,1.8rem);font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
.s-head p{color:var(--sec);font-size:13px}
/* SUMMARY CARDS */
.sum-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
.sc{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;overflow:hidden}
.sc:hover{border-color:var(--border-h)}
.sc-accent{position:absolute;top:0;left:0;width:3px;height:100%;opacity:0;transition:opacity .25s}
.sc:hover .sc-accent{opacity:1}
.sc-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;margin-bottom:6px}
.sc-val{font-family:var(--mono);font-size:24px;font-weight:700;margin-bottom:4px;line-height:1.1}
.sc-trend{display:inline-flex;align-items:center;gap:3px;padding:2px 6px;border-radius:5px;font-family:var(--mono);font-weight:600;font-size:10px}
.sc-trend.up{color:var(--g);background:var(--gd)}
.sc-trend.down{color:var(--r);background:var(--rd)}
.sc-note{font-size:10px;color:var(--muted);margin-top:4px}
/* CHART ROWS */
.cr{display:grid;gap:12px;margin-bottom:12px}
.cr2{grid-template-columns:2fr 1fr}
.cr3{grid-template-columns:1fr 1fr 1fr}
.cc{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:20px;transition:border-color .25s}
.cc:hover{border-color:var(--border-h)}
.ch{margin-bottom:4px}
.ct{font-size:13px;font-weight:600;color:var(--text)}
.cs{font-size:11px;color:var(--muted)}
.cd{width:100%;height:260px}
.cd.s{height:200px}
/* DATA TABLE */
.dt{width:100%;border-collapse:collapse;font-size:12px}
.dt th{text-align:left;padding:8px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;border-bottom:1px solid var(--border)}
.dt td{padding:8px 10px;border-bottom:1px solid var(--border);color:var(--sec);font-family:var(--mono);font-size:11px}
.dt td:first-child{color:var(--text);font-family:var(--mono)}
.dt tr:hover td{background:rgba(255,255,255,.02)}
.badge{display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;font-family:var(--mono)}
.badge.g{color:var(--g);background:var(--gd)}
.badge.b{color:var(--b);background:var(--bd)}
.badge.a{color:var(--a);background:var(--ad)}
.badge.r{color:var(--r);background:var(--rd)}
@media(max-width:1024px){.sum-row{grid-template-columns:repeat(2,1fr)}.cr2,.cr3{grid-template-columns:1fr}}
@media(max-width:640px){.sum-row{grid-template-columns:1fr}.tab{padding:8px 12px;font-size:11px}.tab .lbl{display:none}}
</style>
</head>
<body>
<div class="hdr">
<div class="logo"><span class="dot"></span>ROI24</div>
<div class="hdr-right">
<div class="live"><span class="dot"></span>Live</div>
<div class="clock" id="clock"></div>
</div>
</div>
<div class="tabs-wrap">
<div class="tabs" id="tabs">
<button class="tab active" data-t="cpa">💰 CPA</button>
<button class="tab" data-t="conv">📊 Конверсия</button>
<button class="tab" data-t="ltv">📈 LTV</button>
</div>
</div>
<div class="bswitch-wrap">
<div class="bswitch" id="bswitch">
<button class="bbtn active" data-b="e-com">Интернет-магазин</button>
<button class="bbtn" data-b="services">Услуги</button>
<button class="bbtn" data-b="production">Производство</button>
</div>
</div>
<div id="dashboard">
<!-- ===================== CPA SECTION ===================== -->
<div class="section active" id="sec-cpa">
<div class="s-head"><h2>Стоимость за продажу (CPA)</h2><p>Сколько стоит привлечение платящего клиента</p></div>
<div class="sum-row" id="cpa-sum"></div>
<div class="cr cr2">
<div class="cc">
<div class="ch"><div class="ct" id="cpa-main-t"></div><div class="cs" id="cpa-main-s"></div></div>
<div class="cd" id="cpa-main"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Стоимость канала</div><div class="cs">Расход / количество продаж</div></div>
<div class="cd s" id="cpa-side"></div>
</div>
</div>
<div class="cc" style="margin-bottom:12px">
<div class="ch"><div class="ct">Раскрытие: ключевые запросы</div><div class="cs">Нажмите на канал для детализации</div></div>
<div id="cpa-table-wrap" style="max-height:350px;overflow:auto;margin-top:12px"></div>
</div>
</div>
<!-- ===================== CONV SECTION ===================== -->
<div class="section" id="sec-conv">
<div class="s-head"><h2>Конверсия в продажу</h2><p>Какой трафик и какие менеджеры конвертят лучше</p></div>
<div class="sum-row" id="conv-sum"></div>
<div class="cr cr2">
<div class="cc">
<div class="ch"><div class="ct" id="cv-main-t"></div><div class="cs" id="cv-main-s"></div></div>
<div class="cd" id="cv-main"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Конверсия по каналам</div><div class="cs">Заявка → Продажа</div></div>
<div class="cd s" id="cv-side"></div>
</div>
</div>
<div class="cr cr3">
<div class="cc">
<div class="ch"><div class="ct">Конверсия по ключевым запросам</div><div class="cs">Кликабельный список</div></div>
<div id="cv-keywords" style="max-height:250px;overflow:auto;margin-top:12px"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Конверсия по менеджерам</div><div class="cs">Обработка заявок</div></div>
<div id="cv-managers" style="max-height:250px;overflow:auto;margin-top:12px"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Воронка продаж</div><div class="cs">Этапы: трафик → заявка → продажа</div></div>
<div class="cd s" id="cv-funnel"></div>
</div>
</div>
</div>
<!-- ===================== LTV SECTION ===================== -->
<div class="section" id="sec-ltv">
<div class="s-head"><h2>LTV клиента</h2><p>Сколько клиент платит за всё время и как растёт</p></div>
<div class="sum-row" id="ltv-sum"></div>
<div class="cr cr2">
<div class="cc">
<div class="ch"><div class="ct" id="ltv-main-t"></div><div class="cs" id="ltv-main-s"></div></div>
<div class="cd" id="ltv-main"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">LTV по каналам</div><div class="cs">Кумулятивный доход от клиента</div></div>
<div class="cd s" id="ltv-side"></div>
</div>
</div>
<div class="cr cr2">
<div class="cc">
<div class="ch"><div class="ct">Кривая удержания</div><div class="cs">Какой % клиентов остаётся</div></div>
<div class="cd s" id="ltv-retention"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Средний чек по каналам</div><div class="cs">Сколько платит клиент за 1 заказ</div></div>
<div class="cd s" id="ltv-check"></div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script>
/* ============================================
DATA — Attribution Dashboard
CPA / Conversion / LTV по каналам и ключам
============================================ */
const COLORS = ['#22c55e','#3b82f6','#f59e0b','#a855f7','#ef4444','#06b6d4'];
const DATA = {
/* ==================== E-COM ==================== */
'e-com': {
/* CPA */
cpa: {
avg: {v:'890 ₽',d:'+5%',t:'up',n:'средняя стоимость за продажу'},
total: {v:'1 247',d:'+12%',t:'up',n:'продаж за 30 дней'},
spend: {v:'1.1М ₽',d:'+8%',t:'up',n:'расход на рекламу'},
ratio: {v:'62%',d:'−3%',t:'down',n:'CPA / средний чек'},
channels: [
{n:'Яндекс Директ',spend:520000,conv:120,cpa:4333,avg:863},
{n:'VK Реклама',spend:280000,conv:65,cpa:4308,avg:862},
{n:'SEO',spend:150000,conv:35,cpa:4286,avg:857},
{n:'Ретаргетинг',spend:120000,conv:15,cpa:8000,avg:800},
],
keywords: {
'Яндекс Директ': [
{q:'купить [товар] москва', clicks:3200, conv:85, cpa:3200, cost_per_click:45},
{q:'[товар] цена', clicks:5100, conv:110, cpa:3800, cost_per_click:38},
{q:'[товар] недорого', clicks:7800, conv:95, cpa:5100, cost_per_click:42},
{q:'[товар] отзывы', clicks:2100, conv:62, cpa:2900, cost_per_click:55},
{q:'[бренд] купить', clicks:1800, conv:78, cpa:2200, cost_per_click:48},
{q:'[товар] дешево', clicks:9200, conv:42, cpa:6800, cost_per_click:35},
],
'VK Реклама': [
{q:'пост о [товаре]', clicks:4100, conv:38, cpa:6200, cost_per_click:42},
{q:'таргет [товар]', clicks:2800, conv:22, cpa:7500, cost_per_click:52},
{q:'реклама [товар]', clicks:1900, conv:18, cpa:8100, cost_per_click:48},
{q:'подбор [товара]', clicks:3200, conv:28, cpa:5800, cost_per_click:38},
],
'SEO': [
{q:'[товар] обзор', clicks:2800, conv:42, cpa:2800, cost_per_click:32},
{q:'[товар] сравнение', clicks:1500, conv:35, cpa:3100, cost_per_click:38},
{q:'лучший [товар]', clicks:1900, conv:38, cpa:2900, cost_per_click:35},
],
},
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[1200,1100,980,920,890,870,850,830,810,790,780,760], spend:[350,380,420,450,480,520,490,510,470,460,500,550]},
},
/* Conv */
conv: {
avg: {v:'2.8%',d:'+0.3%',t:'up',n:'заявка → продажа'},
leads: {v:'44 500',d:'+15%',t:'up',n:'лидов за 30 дней'},
sales: {v:'1 247',d:'+12%',t:'up',n:'продаж за 30 дней'},
cycle: {v:'4 дн',d:'−1дн',t:'down',n:'средний цикл'},
channels: [
{n:'Яндекс Директ',leads:28500,conv:260,cpv:0.91},
{n:'VK Реклама',leads:8200,conv:95,cpv:1.16},
{n:'SEO',leads:5800,conv:180,cpv:3.10},
{n:'Ретаргетинг',leads:2000,conv:220,cpv:11.0},
],
keywords: [
{q:'[бренд] купить', leads:4200, sales:380, cv:9.0,c:'var(--g)'},
{q:'[товар] отзывы', leads:2100, sales:190, cv:9.0,c:'var(--g)'},
{q:'купить [товар] москва', leads:3200, sales:260, cv:8.1,c:'var(--g)'},
{q:'[товар] цена', leads:5100, sales:380, cv:7.5,c:'var(--g)'},
{q:'[товар] обзор', leads:2800, sales:180, cv:6.4,c:'var(--b)'},
{q:'[товар] сравнение', leads:1500, sales:85, cv:5.7,c:'var(--b)'},
{q:'[товар] недорого', leads:7800, sales:380, cv:4.9,c:'var(--a)'},
{q:'[товар] дешево', leads:9200, sales:310, cv:3.4,c:'var(--r)'},
],
managers: [
{n:'Козлов А.', leads:820, sales:260, cv:31.7,c:'var(--g)'},
{n:'Иванова М.', leads:940, sales:280, cv:29.8,c:'var(--g)'},
{n:'Петров Д.', leads:780, sales:210, cv:26.9,c:'var(--b)'},
{n:'Сидорова Е.', leads:690, sales:170, cv:24.6,c:'var(--a)'},
{n:'Новиков К.', leads:1100, sales:250, cv:22.7,c:'var(--a)'},
{n:'Волкова Т.', leads:620, sales:120, cv:19.4,c:'var(--r)'},
],
funnel: [
{n:'Посетители',v:45000},
{n:'Оформление',v:8100},
{n:'Корзина',v:4950},
{n:'Оплата',v:2723},
],
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[2.1,2.2,2.3,2.4,2.5,2.6,2.7,2.7,2.8,2.8,2.9,2.8]},
},
/* LTV */
ltv: {
avg: {v:'12 400 ₽',d:'+18%',t:'up',n:'за 6 мес'},
repeat: {v:'3.2×',d:'+0.3',t:'up',n:'ср. покупок'},
retention: {v:'68%',d:'+5%',t:'up',n:'через 3 мес'},
avgCheck: {v:'1 850 ₽',d:'−2%',t:'down',n:'за 1 заказ'},
channels: [
{n:'SEO',ltv:18200,checks:7,retention:78},
{n:'Ретаргетинг',ltv:15800,checks:5,retention:72},
{n:'Яндекс Директ',ltv:12400,checks:4,retention:68},
{n:'VK Реклама',ltv:9800,checks:3,retention:58},
],
curve: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[180,380,620,900,1200,1500,1800,2100,2400,2700,3000,3300],
direct:[160,320,520,780,1050,1350,1650,1900,2200,2500,2800,3100],
vk:[140,260,420,620,850,1100,1350,1600,1850,2100,2400,2700],
},
retention: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[100,78,65,56,50,45,40,36,33,30,28,26],
direct:[100,72,58,48,42,37,33,30,28,25,23,22],
vk:[100,65,50,38,30,24,20,17,15,13,12,11],
},
checks: [
{n:'Яндекс Директ',avg:1600,c:'var(--b)'},
{n:'SEO',avg:1850,c:'var(--g)'},
{n:'VK Реклама',avg:1350,c:'var(--a)'},
{n:'Ретаргетинг',avg:2100,c:'var(--p)'},
],
},
},
/* ==================== SERVICES ==================== */
'services': {
cpa: {
avg: {v:'3 200 ₽',d:'+2%',t:'up',n:'средняя стоимость за продажу'},
total: {v:'86',d:'+8%',t:'up',n:'продаж за 30 дней'},
spend: {v:'275К ₽',d:'+5%',t:'up',n:'расход на рекламу'},
ratio: {v:'18%',d:'−2%',t:'down',n:'CPA / средний чек'},
channels: [
{n:'Яндекс Директ',spend:140000,conv:42,cpa:3333,avg:18476},
{n:'VK Реклама',spend:75000,conv:22,cpa:3409,avg:18409},
{n:'SEO',spend:60000,conv:22,cpa:2727,avg:18182},
],
keywords: {
'Яндекс Директ': [
{q:'создать сайт цена', clicks:1800, conv:42, cpa:2800, cost_per_click:52},
{q:'заказать лендинг', clicks:2200, conv:38, cpa:3100, cost_per_click:48},
{q:'разработка сайта', clicks:3100, conv:28, cpa:4200, cost_per_click:55},
{q:'сайт под ключ', clicks:1400, conv:18, cpa:5200, cost_per_click:62},
],
'VK Реклама': [
{q:'лендинг дизайн', clicks:1200, conv:12, cpa:5500, cost_per_click:58},
{q:'сайт для бизнеса', clicks:900, conv:8, cpa:7200, cost_per_click:65},
],
'SEO': [
{q:'создание сайтов москва', clicks:2400, conv:22, cpa:2400, cost_per_click:28},
],
},
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[3800,3600,3500,3400,3350,3200,3100,3000,2900,2850,2800,2750], spend:[80,85,90,100,105,110,108,115,112,118,120,125]},
},
conv: {
avg: {v:'5.2%',d:'+0.4%',t:'up',n:'заявка → продажа'},
leads: {v:'1 650',d:'+10%',t:'up',n:'лидов за 30 дней'},
sales: {v:'86',d:'+8%',t:'up',n:'продаж за 30 дней'},
cycle: {v:'12 дн',d:'−2дн',t:'down',n:'средний цикл'},
channels: [
{n:'Яндекс Директ',leads:820,conv:48,cpv:5.85},
{n:'VK Реклама',leads:380,conv:22,cpv:5.79},
{n:'SEO',leads:450,conv:16,cpv:3.56},
],
keywords: [
{q:'создать сайт цена', leads:1800, sales:92, cv:5.1,c:'var(--g)'},
{q:'заказать лендинг', leads:2200, sales:105, cv:4.8,c:'var(--g)'},
{q:'разработка сайта', leads:3100, sales:120, cv:3.9,c:'var(--b)'},
{q:'сайт под ключ', leads:1400, sales:48, cv:3.4,c:'var(--a)'},
],
managers: [
{n:'Смирнов О.', leads:420, sales:32, cv:7.6,c:'var(--g)'},
{n:'Кузнецова Н.', leads:380, sales:28, cv:7.4,c:'var(--g)'},
{n:'Фёдоров В.', leads:480, sales:30, cv:6.3,c:'var(--b)'},
{n:'Морозова И.', leads:370, sales:18, cv:4.9,c:'var(--a)'},
],
funnel: [
{n:'Посетители',v:3200},
{n:'Заявки',v:166},
{n:'Контракт',v:86},
],
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[3.8,4.0,4.2,4.5,4.7,4.8,4.9,5.0,5.1,5.1,5.2,5.2]},
},
ltv: {
avg: {v:'89 000 ₽',d:'+22%',t:'up',n:'за 12 мес'},
repeat: {v:'1.4×',d:'+0.1',t:'up',n:'повторных заказов'},
retention: {v:'72%',d:'+4%',t:'up',n:'через 6 мес'},
avgCheck: {v:'63 500 ₽',d:'+5%',t:'up',n:'за 1 заказ'},
channels: [
{n:'SEO',ltv:125000,checks:2.1,retention:82},
{n:'Яндекс Директ',ltv:89000,checks:1.4,retention:72},
{n:'VK Реклама',ltv:58000,checks:1.1,retention:58},
],
curve: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[12000,24000,38000,52000,68000,82000,96000,110000,120000,128000,135000,142000],
direct:[10000,18000,28000,40000,55000,68000,80000,92000,100000,108000,115000,120000],
vk:[7000,12000,18000,26000,35000,45000,54000,62000,70000,78000,84000,90000],
},
retention: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[100,85,78,72,68,65,62,60,58,56,55,54],
direct:[100,78,65,58,52,48,44,42,40,38,37,36],
vk:[100,68,52,42,35,30,26,23,21,19,18,17],
},
checks: [
{n:'Яндекс Директ',avg:63500,c:'var(--b)'},
{n:'SEO',avg:67500,c:'var(--g)'},
{n:'VK Реклама',avg:48000,c:'var(--a)'},
],
},
},
/* ==================== PRODUCTION ==================== */
'production': {
cpa: {
avg: {v:'12 500 ₽',d:'−8%',t:'down',n:'средняя стоимость за заказ'},
total: {v:'340',d:'+6%',t:'up',n:'заказов за 30 дней'},
spend: {v:'4.2М ₽',d:'+4%',t:'up',n:'расход на рекламу'},
ratio: {v:'5.2%',d:'−1%',t:'down',n:'CPA / средний чек'},
channels: [
{n:'Яндекс Директ',spend:2100000,conv:150,cpa:14000,avg:270000},
{n:'VK Реклама',spend:980000,conv:100,cpa:9800,avg:245000},
{n:'SEO',spend:520000,conv:50,cpa:10400,avg:260000},
{n:'Выставки',spend:600000,conv:40,cpa:15000,avg:300000},
],
keywords: {
'Яндекс Директ': [
{q:'купить [оборудование] оптом', clicks:2800, conv:55, cpa:11200, cost_per_click:48},
{q:'производство [оборудования]', clicks:3500, conv:38, cpa:15200, cost_per_click:52},
{q:'[оборудование] цена опт', clicks:4200, conv:32, cpa:17800, cost_per_click:58},
{q:'поставщик [оборудования]', clicks:1900, conv:28, cpa:13500, cost_per_click:55},
{q:'[оборудование] каталог', clicks:2200, conv:22, cpa:18500, cost_per_click:45},
],
'VK Реклама': [
{q:'партнёр [оборудование]', clicks:1800, conv:22, cpa:12500, cost_per_click:42},
{q:'опт [оборудование]', clicks:1400, conv:18, cpa:14200, cost_per_click:48},
],
'SEO': [
{q:'производитель [оборудования]', clicks:1600, conv:32, cpa:9800, cost_per_click:32},
],
},
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[18000,16500,15200,14000,13500,12800,12200,11800,11500,11200,11000,10800], spend:[280,320,350,380,400,420,410,430,420,440,460,480]},
},
conv: {
avg: {v:'2.1%',d:'+0.2%',t:'up',n:'заявка → заказ'},
leads: {v:'16 200',d:'+8%',t:'up',n:'заявок за 30 дней'},
sales: {v:'340',d:'+6%',t:'up',n:'заказов за 30 дней'},
cycle: {v:'18 дн',d:'−3дн',t:'down',n:'средний цикл'},
channels: [
{n:'Яндекс Директ',leads:8200,conv:150,cpv:1.83},
{n:'VK Реклама',leads:4800,conv:100,cpv:2.08},
{n:'SEO',leads:2400,conv:50,cpv:2.08},
{n:'Выставки',leads:800,conv:40,cpv:5.0},
],
keywords: [
{q:'[оборудование] каталог', leads:2200, sales:55, cv:2.5,c:'var(--g)'},
{q:'купить [оборудование] опт', leads:2800, sales:62, cv:2.2,c:'var(--g)'},
{q:'производство [оборудования]', leads:3500, sales:72, cv:2.1,c:'var(--b)'},
{q:'поставщик [оборудования]', leads:1900, sales:38, cv:2.0,c:'var(--b)'},
{q:'[оборудование] цена опт', leads:4200, sales:75, cv:1.8,c:'var(--a)'},
{q:'[оборудование] каталог', leads:2200, sales:32, cv:1.5,c:'var(--r)'},
],
managers: [
{n:'Борисов Р.', leads:4200, sales:110, cv:2.6,c:'var(--g)'},
{n:'Тимофеева А.', leads:3800, sales:95, cv:2.5,c:'var(--g)'},
{n:'Орлов С.', leads:4500, sales:98, cv:2.2,c:'var(--b)'},
{n:'Лебедева О.', leads:2800, sales:55, cv:2.0,c:'var(--a)'},
{n:'Егоров П.', leads:2200, sales:42, cv:1.9,c:'var(--a)'},
{n:'Соколова Н.', leads:1800, sales:28, cv:1.6,c:'var(--r)'},
],
funnel: [
{n:'Посетители',v:85000},
{n:'Заявки',v:1785},
{n:'Контракт',v:340},
],
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[1.4,1.5,1.6,1.7,1.8,1.8,1.9,1.9,2.0,2.0,2.1,2.1]},
},
ltv: {
avg: {v:'1 250 000 ₽',d:'+15%',t:'up',n:'за 12 мес'},
repeat: {v:'2.3×',d:'+0.2',t:'up',n:'повторных заказов'},
retention: {v:'55%',d:'+3%',t:'up',n:'через 6 мес'},
avgCheck: {v:'543 000 ₽',d:'+4%',t:'up',n:'за 1 заказ'},
channels: [
{n:'SEO',ltv:1850000,checks:3.5,retention:68},
{n:'Выставки',ltv:1420000,checks:2.8,retention:60},
{n:'Яндекс Директ',ltv:1100000,checks:2.0,retention:52},
{n:'VK Реклама',ltv:780000,checks:1.4,retention:38},
],
curve: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[180000,380000,580000,780000,980000,1180000,1380000,1580000,1750000,1900000,2050000,2200000],
direct:[160000,320000,480000,640000,820000,1000000,1180000,1350000,1500000,1650000,1800000,1950000],
vk:[100000,200000,320000,450000,600000,750000,900000,1050000,1200000,1350000,1500000,1650000],
},
retention: {
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
seo:[100,82,72,65,60,56,53,51,50,49,48,48],
direct:[100,70,55,45,38,34,30,28,26,25,24,24],
vk:[100,60,42,30,22,16,12,10,9,8,7,7],
},
checks: [
{n:'Яндекс Директ',avg:543000,c:'var(--b)'},
{n:'SEO',avg:528000,c:'var(--g)'},
{n:'VK Реклама',avg:420000,c:'var(--a)'},
{n:'Выставки',avg:620000,c:'var(--p)'},
],
},
},
};
let charts = {};
let curSeg = 'e-com', curTab = 'cpa';
/* ============================================
HELPERS
============================================ */
function sumRow(id, items) {
const el = document.getElementById(id);
el.innerHTML = items.map((s,i) => `
<div class="sc">
<div class="sc-accent" style="background:${['var(--g)','var(--b)','var(--p)','var(--a)'][i%4]}"></div>
<div class="sc-label">${s.l}</div>
<div class="sc-val">${s.v}</div>
<span class="sc-trend ${s.t}">${s.t==='up'?'↑':s.t==='down'?'↓':'→'} ${s.d}</span>
<div class="sc-note">${s.n}</div>
</div>`).join('');
}
function makeLineChart(domId, title, opt, sub) {
const dom = document.getElementById(domId);
if (!charts[domId]) charts[domId] = echarts.init(dom);
if (title) document.getElementById(title)?.querySelector('.ct') && (document.getElementById(title).querySelector('.ct').textContent = title);
charts[domId].setOption(opt, true);
}
/* ============================================
CPA RENDER
============================================ */
function renderCPA(d) {
const c = d.cpa;
// Summary
sumRow('cpa-sum', [
{l:'CPA средний',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
{l:'Продажи',v:c.total.v,d:c.total.d,t:c.total.t,n:c.total.n},
{l:'Расход',v:c.spend.v,d:c.spend.d,t:c.spend.d,t:c.spend.t,n:c.spend.n},
{l:'CPA / чек',v:c.ratio.v,d:c.ratio.d,t:c.ratio.t,n:c.ratio.n},
]);
// Main: CPA trend
const mc = document.getElementById('cpa-main');
if (!charts.cpaMain) charts.cpaMain = echarts.init(mc);
document.getElementById('cpa-main-t').textContent = c.trend.labels.map((_,i)=>{
const months=['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
return '';
}).join('');
document.getElementById('cpa-main-t').textContent = 'CPA и расход на рекламу';
document.getElementById('cpa-main-s').textContent = 'Тренд за 12 месяцев';
charts.cpaMain.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
legend:{data:['CPA','Расход (×100)'],textStyle:{color:'#52525b',fontSize:10},top:0},
grid:{left:44,right:44,top:28,bottom:28},
xAxis:{type:'category',data:c.trend.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'}},
series:[
{name:'CPA',type:'line',data:c.trend.cpa.map(v=>v*100/100),smooth:.4,symbol:'none',lineStyle:{width:2.5,color:'#22c55e'},
yAxisIndex:0,
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(34,197,94,.2)'},{offset:1,color:'rgba(34,197,94,.01)'}])},
},
{name:'Расход',type:'bar',data:c.trend.spend,barWidth:'35%',itemStyle:{color:'rgba(59,130,246,.2)',borderRadius:[4,4,0,0]}},
],
animationDuration:600,
},true);
// Side: Channel CPA
const sc = document.getElementById('cpa-side');
if (!charts.cpaSide) charts.cpaSide = echarts.init(sc);
const chs = c.channels.sort((a,b)=>a.cpa-b.cpa);
charts.cpaSide.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
grid:{left:80,right:30,top:10,bottom:10},
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}₽'}},
yAxis:{type:'category',data:chs.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
series:[{
type:'bar',data:chs.map(x=>x.cpa).reverse(),barWidth:38,
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
}],
animationDuration:600,
},true);
// Table: Keywords
renderKeywordTable(c.keywords);
}
function renderKeywordTable(keywords) {
const wrap = document.getElementById('cpa-table-wrap');
let html = '<table class="dt"><thead><tr><th>Канал</th><th>Ключевой запрос</th><th>Клики</th><th>Продажи</th><th>CPA</th><th>Цена клика</th></tr></thead><tbody>';
for (const [channel, kws] of Object.entries(keywords)) {
kws.forEach(kw => {
const badge = kw.cpa < 3000 ? 'g' : kw.cpa < 5000 ? 'b' : kw.cpa < 6000 ? 'a' : 'r';
html += `<tr>
<td><span class="badge ${badge}">${channel}</span></td>
<td style="color:var(--text);font-family:var(--mono)">${kw.q}</td>
<td>${kw.clicks.toLocaleString('ru')}</td>
<td>${kw.conv}</td>
<td>${kw.cpa.toLocaleString('ru')} ₽</td>
<td>${kw.cost_per_click} ₽</td>
</tr>`;
});
}
html += '</tbody></table>';
wrap.innerHTML = html;
}
/* ============================================
CONV RENDER
============================================ */
function renderConv(d) {
const c = d.conv;
sumRow('conv-sum', [
{l:'Конверсия',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
{l:'Лиды',v:c.leads.v,d:c.leads.d,t:c.leads.t,n:c.leads.n},
{l:'Продажи',v:c.sales.v,d:c.sales.d,t:c.sales.t,n:c.sales.n},
{l:'Цикл',v:c.cycle.v,d:c.cycle.d,t:c.cycle.t,n:c.cycle.n},
]);
// Main: Conv trend
const mc = document.getElementById('cv-main');
if (!charts.convMain) charts.convMain = echarts.init(mc);
document.getElementById('cv-main-t').textContent = 'Конверсия в продажу';
document.getElementById('cv-main-s').textContent = 'Тренд за 12 месяцев';
charts.convMain.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
grid:{left:44,right:16,top:28,bottom:28},
xAxis:{type:'category',data:c.trend.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}%'}},
series:[{
type:'line',data:c.trend.data,smooth:.4,symbol:'none',lineStyle:{width:2.5,color:'#22c55e'},
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(34,197,94,.2)'},{offset:1,color:'rgba(34,197,94,.01)'}])},
}],
animationDuration:600,
},true);
// Side: Conv by channel
const sc = document.getElementById('cv-side');
if (!charts.convSide) charts.convSide = echarts.init(sc);
const chs = c.channels.sort((a,b)=>a.conv-b.conv);
charts.convSide.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
grid:{left:80,right:30,top:10,bottom:10},
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'}},
yAxis:{type:'category',data:chs.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
series:[{
type:'bar',data:chs.map(x=>x.conv).reverse(),barWidth:38,
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
}],
animationDuration:600,
},true);
// Keywords
const kwEl = document.getElementById('cv-keywords');
kwEl.innerHTML = `<table class="dt"><thead><tr><th>Запрос</th><th>Лиды</th><th>Продажи</th><th>CVR</th></tr></thead><tbody>` +
c.keywords.map(k => `<tr>
<td style="color:var(--text)">${k.q}</td>
<td>${k.leads.toLocaleString('ru')}</td>
<td>${k.sales}</td>
<td><span class="badge ${k.c==='var(--g)'?'g':k.c==='var(--b)'?'b':k.c==='var(--a)'?'a':'r'}">${k.cv}%</span></td>
</tr>`).join('') + '</tbody></table>';
// Managers
const mgEl = document.getElementById('cv-managers');
mgEl.innerHTML = `<table class="dt"><thead><tr><th>Менеджер</th><th>Лиды</th><th>Продажи</th><th>CVR</th></tr></thead><tbody>` +
c.managers.map(m => `<tr>
<td style="color:var(--text)">${m.n}</td>
<td>${m.leads.toLocaleString('ru')}</td>
<td>${m.sales}</td>
<td><span class="badge ${m.c==='var(--g)'?'g':m.c==='var(--b)'?'b':m.c==='var(--a)'?'a':'r'}">${m.cv}%</span></td>
</tr>`).join('') + '</tbody></table>';
// Funnel
const fn = document.getElementById('cv-funnel');
if (!charts.convFunnel) charts.convFunnel = echarts.init(fn);
const fc = ['#22c55e','#3b82f6','#a855f7'];
const funnelData = c.funnel.map((f,i)=>({name:f.n,value:f.v,itemStyle:{color:fc[i%fc.length]}}));
charts.convFunnel.setOption({
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
series:[{
type:'funnel',left:'10%',bottom:5,width:'80%',min:0,max:c.funnel[0].v,
minSize:'0%',maxSize:'100%',sort:'descending',gap:4,
label:{show:true,position:'inside',color:'#fff',fontSize:11,formatter:'{b}\n{d}%'},
labelLine:{show:false},itemStyle:{borderColor:'#111',borderWidth:2},
data:funnelData,
}],
animationDuration:600,
},true);
}
/* ============================================
LTV RENDER
============================================ */
function renderLTV(d) {
const c = d.ltv;
sumRow('ltv-sum', [
{l:'LTV средний',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
{l:'Повторные',v:c.repeat.v,d:c.repeat.d,t:c.repeat.t,n:c.repeat.n},
{l:'Удержание',v:c.retention.v,d:c.retention.d,t:c.retention.t,n:c.retention.n},
{l:'Ср. чек',v:c.avgCheck.v,d:c.avgCheck.d,t:c.avgCheck.t,n:c.avgCheck.n},
]);
// Main: LTV curve
const mc = document.getElementById('ltv-main');
if (!charts.ltvMain) charts.ltvMain = echarts.init(mc);
document.getElementById('ltv-main-t').textContent = 'Кумулятивный LTV';
document.getElementById('ltv-main-s').textContent = 'Накопленный доход от клиента по месяцам';
charts.ltvMain.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
legend:{data:['SEO','Директ','VK'],textStyle:{color:'#52525b',fontSize:10},top:0},
grid:{left:44,right:16,top:28,bottom:28},
xAxis:{type:'category',data:c.curve.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'}},
series:[
{name:'SEO',type:'line',data:c.curve.seo,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#22c55e'}},
{name:'Директ',type:'line',data:c.curve.direct,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#3b82f6'}},
{name:'VK',type:'line',data:c.curve.vk,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#f59e0b'}},
],
animationDuration:600,
},true);
// Side: LTV by channel
const sc = document.getElementById('ltv-side');
if (!charts.ltvSide) charts.ltvSide = echarts.init(sc);
const ltvChannels = c.channels.sort((a,b)=>a.ltv-b.ltv);
charts.ltvSide.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
grid:{left:80,right:30,top:10,bottom:10},
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}К'}},
yAxis:{type:'category',data:ltvChannels.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
series:[{
type:'bar',data:ltvChannels.map(x=>x.ltv/1000).reverse(),barWidth:38,
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
}],
animationDuration:600,
},true);
// Retention curve
const rt = document.getElementById('ltv-retention');
if (!charts.ltvRet) charts.ltvRet = echarts.init(rt);
charts.ltvRet.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
legend:{data:['SEO','Директ','VK'],textStyle:{color:'#52525b',fontSize:10},top:0},
grid:{left:44,right:16,top:28,bottom:28},
xAxis:{type:'category',data:c.retention.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}%'}},
series:[
{name:'SEO',type:'line',data:c.retention.seo,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#22c55e'}},
{name:'Директ',type:'line',data:c.retention.direct,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#3b82f6'}},
{name:'VK',type:'line',data:c.retention.vk,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#f59e0b'}},
],
animationDuration:600,
},true);
// Average check by channel
const ck = document.getElementById('ltv-check');
if (!charts.ltvCheck) charts.ltvCheck = echarts.init(ck);
const checks = [...c.checks].sort((a,b)=>parseInt(a.avg)-parseInt(b.avg));
charts.ltvCheck.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
grid:{left:80,right:30,top:10,bottom:10},
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}К'}},
yAxis:{type:'category',data:checks.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
series:[{
type:'bar',data:checks.map(x=>parseInt(x.avg)/1000).reverse(),barWidth:38,
itemStyle:{borderRadius:[0,5,5,0],color:p=>{
const ch = checks[p.dataIndex];
return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:ch.c+'88'},{offset:1,color:ch.c}]);
}}
}],
animationDuration:600,
},true);
}
/* ============================================
MAIN RENDER
============================================ */
function render() {
const d = DATA[curSeg];
if (curTab === 'cpa') {
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
document.getElementById('sec-cpa').classList.add('active');
renderCPA(d);
} else if (curTab === 'conv') {
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
document.getElementById('sec-conv').classList.add('active');
renderConv(d);
} else {
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
document.getElementById('sec-ltv').classList.add('active');
renderLTV(d);
}
}
/* ============================================
EVENTS
============================================ */
document.addEventListener('DOMContentLoaded', () => {
// Tabs
document.getElementById('tabs').addEventListener('click', e => {
const tab = e.target.closest('.tab');
if (!tab) return;
curTab = tab.dataset.t;
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.t===curTab));
render();
});
// Business switcher
document.getElementById('bswitch').addEventListener('click', e => {
const btn = e.target.closest('.bbtn');
if (!btn) return;
curSeg = btn.dataset.b;
document.querySelectorAll('.bbtn').forEach(b=>b.classList.toggle('active', b.dataset.b===curSeg));
render();
});
render();
// Clock
function tick() {
const n = new Date();
document.getElementById('clock').textContent =
String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0')+':'+String(n.getSeconds()).padStart(2,'0');
}
tick(); setInterval(tick, 1000);
// Resize
new ResizeObserver(() => Object.values(charts).forEach(c=>c.resize())).observe(document.getElementById('dashboard'));
});
</script>
</body>
</html>