Files
roi24/attribution-mockup.html
T

924 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="sum-row" id="rev-sum" style="margin-bottom:12px"></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 class="cd s" id="rev-channel"></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="sum-row" id="conv-rev" style="margin-bottom:12px"></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="sum-row" id="ltv-rev" style="margin-bottom:12px"></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,revenue:1072000},
{n:'VK Реклама',spend:280000,conv:65,cpa:4308,avg:862,revenue:560000},
{n:'SEO',spend:150000,conv:35,cpa:4286,avg:857,revenue:299857},
{n:'Ретаргетинг',spend:120000,conv:15,cpa:8000,avg:800,revenue:120000},
],
totalRevenue: '2 051 857 ₽',
totalSpend: '1 070 000 ₽',
totalProfit: '981 857 ₽',
roi: '91.8%',
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],
},
retCurve: {
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,revenue:775952},
{n:'VK Реклама',spend:75000,conv:22,cpa:3409,avg:18409,revenue:405000},
{n:'SEO',spend:60000,conv:22,cpa:2727,avg:18182,revenue:400004},
],
totalRevenue: '1 580 956 ₽',
totalSpend: '275 000 ₽',
totalProfit: '1 305 956 ₽',
roi: '474.9%',
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],
},
retCurve: {
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,revenue:4050000},
{n:'VK Реклама',spend:980000,conv:100,cpa:9800,avg:245000,revenue:2450000},
{n:'SEO',spend:520000,conv:50,cpa:10400,avg:260000,revenue:1300000},
{n:'Выставки',spend:600000,conv:40,cpa:15000,avg:300000,revenue:1200000},
],
totalRevenue: '10 050 000 ₽',
totalSpend: '4 200 000 ₽',
totalProfit: '5 850 000 ₽',
roi: '139.3%',
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],
},
retCurve: {
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},
]);
// Revenue summary
sumRow('rev-sum', [
{l:'Общий доход',v:c.totalRevenue,d:'+15%',t:'up',n:'за 30 дней'},
{l:'Расход',v:c.totalSpend,d:'+8%',t:'up',n:'на рекламу'},
{l:'Прибыль',v:c.totalProfit,d:'+12%',t:'up',n:'доход − расход'},
{l:'ROI',v:c.roi,d:'+5%',t:'up',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);
// Revenue by channel
const rc = document.getElementById('rev-channel');
if (!charts.revChannel) charts.revChannel = echarts.init(rc);
const revData = c.channels.map(ch => ({
name: ch.n,
spend: ch.spend,
revenue: ch.revenue,
profit: ch.revenue - ch.spend,
roi: ((ch.revenue - ch.spend) / ch.spend * 100).toFixed(0) + '%',
}));
const chNames = revData.map(r => r.name);
charts.revChannel.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
legend:{data:['Доход','Расход'],textStyle:{color:'#52525b',fontSize:10},top:0},
grid:{left:100,right:16,top:30,bottom:10},
xAxis:{type:'category',data:chNames,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:9}},
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:v=>v>=1e6?(v/1e6).toFixed(0)+'М':(v/1e3).toFixed(0)+'К'}},
series:[
{name:'Доход',type:'bar',data:revData.map(r=>r.revenue),barWidth:'35%',itemStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'#22c55e'},{offset:1,color:'rgba(34,197,94,.3)'}]),borderRadius:[4,4,0,0]}},
{name:'Расход',type:'bar',data:revData.map(r=>r.spend),barWidth:'35%',itemStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'#3b82f6'},{offset:1,color:'rgba(59,130,246,.3)'}]),borderRadius:[4,4,0,0]}},
],
animationDuration:600,
},true);
}
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},
]);
// Revenue in conv tab (using CPA data)
const rc = DATA[curSeg].cpa;
sumRow('conv-rev', [
{l:'Общий доход',v:rc.totalRevenue,d:'+15%',t:'up',n:'за 30 дней'},
{l:'Расход',v:rc.totalSpend,d:'+8%',t:'up',n:'на рекламу'},
{l:'Прибыль',v:rc.totalProfit,d:'+12%',t:'up',n:'доход − расход'},
{l:'ROI',v:rc.roi,d:'+5%',t:'up',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},
]);
// Revenue in LTV tab
const rc = DATA[curSeg].cpa;
sumRow('ltv-rev', [
{l:'Общий доход',v:rc.totalRevenue,d:'+15%',t:'up',n:'за 30 дней'},
{l:'Расход',v:rc.totalSpend,d:'+8%',t:'up',n:'на рекламу'},
{l:'Прибыль',v:rc.totalProfit,d:'+12%',t:'up',n:'доход − расход'},
{l:'ROI',v:rc.roi,d:'+5%',t:'up',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.retCurve.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.retCurve.seo,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#22c55e'}},
{name:'Директ',type:'line',data:c.retCurve.direct,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#3b82f6'}},
{name:'VK',type:'line',data:c.retCurve.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>