- 4 top KPI cards (registrations, downloads by OS+total, MAU, DAU) from
drb_iliyas_telecomkz_daily_info + telecomkz_mau_stats; JSON gets top{} block
- grid/list view toggle (list = full-width cards), persisted in localStorage
- click any card -> modal with Chart.js line chart + per-month table; tooltip
shows monthly delta for both years; ESC/overlay/✕ to close
1353 lines
42 KiB
HTML
1353 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Метрики МП — Казахтелеком 2026</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--color-bg: #F4F6F9;
|
||
--color-surface: #FFFFFF;
|
||
--color-border: #E5E7EB;
|
||
--color-text-primary: #111827;
|
||
--color-text-secondary: #6B7280;
|
||
--color-brand: #0052CC;
|
||
--color-brand-light: #EFF6FF;
|
||
--color-green: #10B981;
|
||
--color-green-bg: #ECFDF5;
|
||
--color-yellow: #F59E0B;
|
||
--color-yellow-bg: #FFFBEB;
|
||
--color-red: #EF4444;
|
||
--color-red-bg: #FEF2F2;
|
||
--color-purple: #8B5CF6;
|
||
--color-purple-bg: #F5F3FF;
|
||
--radius-card: 12px;
|
||
--radius-btn: 8px;
|
||
--shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
|
||
--shadow-card-hover: 0 4px 12px rgba(0,0,0,0.1);
|
||
--font-base: 'Inter', system-ui, sans-serif;
|
||
--font-mono: 'Roboto Mono', monospace;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: var(--font-base); background: var(--color-bg); color: var(--color-text-primary); font-size: 14px; line-height: 1.5; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* LOGIN */
|
||
#login-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%); }
|
||
.login-box { background: #fff; border-radius: 20px; padding: 48px 56px; text-align: center; max-width: 420px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.15); }
|
||
.login-logo { display: inline-flex; align-items: center; justify-content: center; width: 64px; height: 64px; background: var(--color-brand); color: #fff; font-size: 24px; font-weight: 700; border-radius: 16px; margin-bottom: 20px; }
|
||
.login-box h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
||
.login-box > p { color: var(--color-text-secondary); margin-bottom: 28px; font-size: 14px; }
|
||
.txt-input { width: 100%; padding: 11px 14px; border: 1px solid var(--color-border); border-radius: var(--radius-btn); font-size: 15px; font-family: var(--font-base); outline: none; transition: border-color 0.15s; }
|
||
.txt-input:focus { border-color: var(--color-brand); }
|
||
.btn-primary { padding: 11px 40px; background: var(--color-brand); color: #fff; border: none; border-radius: var(--radius-btn); font-size: 14px; font-weight: 600; cursor: pointer; font-family: var(--font-base); transition: background 0.15s; }
|
||
.btn-primary:hover { background: #003fa3; }
|
||
#login-error { color: var(--color-red); font-size: 13px; margin-top: 10px; min-height: 18px; }
|
||
|
||
/* HEADER */
|
||
#app { display: none; }
|
||
.header { background: var(--color-brand); color: #fff; padding: 0 24px; height: 64px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 8px rgba(0,82,204,0.3); }
|
||
.header-left { display: flex; align-items: center; gap: 14px; }
|
||
.header-logo { width: 40px; height: 40px; background: rgba(255,255,255,0.2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; flex-shrink: 0; }
|
||
.header-left h1 { font-size: 16px; font-weight: 700; line-height: 1.2; }
|
||
.header-left p { font-size: 12px; opacity: 0.75; }
|
||
.header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
#generated-at { font-size: 12px; opacity: 0.85; }
|
||
.btn { padding: 7px 14px; border-radius: var(--radius-btn); font-size: 13px; font-weight: 500; cursor: pointer; border: none; font-family: var(--font-base); transition: all 0.15s; text-decoration: none; display: inline-block; }
|
||
.btn-ghost { background: rgba(255,255,255,0.15); color: #fff; }
|
||
.btn-ghost:hover { background: rgba(255,255,255,0.25); }
|
||
|
||
/* SUMMARY */
|
||
.wrap { padding: 20px 24px 40px; max-width: 1400px; margin: 0 auto; }
|
||
.summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 22px; }
|
||
.sum-card { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px 18px; box-shadow: var(--shadow-card); border-top: 4px solid var(--color-brand); }
|
||
.sum-card.accent-green { border-top-color: var(--color-green); }
|
||
.sum-card.accent-purple { border-top-color: var(--color-purple); }
|
||
.sum-card.accent-yellow { border-top-color: var(--color-yellow); }
|
||
.sum-label { font-size: 11px; font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 8px; }
|
||
.sum-value { font-size: 26px; font-weight: 700; font-family: var(--font-mono); line-height: 1.1; }
|
||
.sum-sub { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; }
|
||
|
||
/* CONTROLS */
|
||
.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
|
||
.controls .grow { flex: 1 1 220px; }
|
||
.controls label { font-size: 13px; color: var(--color-text-secondary); display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
||
select.txt-input, input.txt-input { font-size: 13px; padding: 9px 12px; }
|
||
select.txt-input { cursor: pointer; background: #fff; }
|
||
|
||
/* METRIC CARDS */
|
||
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
||
.card { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px 18px; box-shadow: var(--shadow-card); border-left: 4px solid var(--color-border); transition: box-shadow 0.2s, transform 0.2s; }
|
||
.card:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-2px); }
|
||
.card.up { border-left-color: var(--color-green); }
|
||
.card.down { border-left-color: var(--color-red); }
|
||
.card.flat { border-left-color: var(--color-yellow); }
|
||
.card.new { border-left-color: var(--color-purple); }
|
||
.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
|
||
.card-title { font-size: 13px; font-weight: 600; line-height: 1.35; }
|
||
.badge { font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 20px; white-space: nowrap; flex-shrink: 0; }
|
||
.badge.up { background: var(--color-green-bg); color: var(--color-green); }
|
||
.badge.down { background: var(--color-red-bg); color: var(--color-red); }
|
||
.badge.flat { background: var(--color-yellow-bg); color: #92400E; }
|
||
.badge.new { background: var(--color-purple-bg); color: var(--color-purple); }
|
||
.card-value { font-size: 30px; font-weight: 700; font-family: var(--font-mono); line-height: 1; }
|
||
.card-value-year { font-size: 11px; color: var(--color-text-secondary); font-weight: 600; margin-left: 2px; }
|
||
.card-sub { font-size: 11px; color: var(--color-text-secondary); margin-top: 2px; }
|
||
/* накопительный мини-график (нарастающим итогом по месяцам) */
|
||
.spark { width: 100%; height: 52px; display: block; margin-top: 12px; overflow: visible; }
|
||
.spark polyline { fill: none; stroke-width: 2; vector-effect: non-scaling-stroke; stroke-linecap: round; stroke-linejoin: round; }
|
||
.spark .sp-cur { stroke: var(--color-brand); }
|
||
.spark .sp-prev { stroke: #C7D2E8; stroke-dasharray: 3 3; }
|
||
.spark .dot-cur { fill: var(--color-brand); }
|
||
.spark-x { display: flex; justify-content: space-between; font-size: 9px; color: var(--color-text-secondary); font-family: var(--font-mono); margin-top: 2px; }
|
||
.legend { display: flex; gap: 14px; margin-top: 10px; font-size: 11px; }
|
||
.legend span { display: flex; align-items: center; gap: 5px; color: var(--color-text-secondary); }
|
||
.legend b { font-family: var(--font-mono); color: var(--color-text-primary); font-weight: 600; }
|
||
.legend .dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
|
||
.legend .dot.cur { background: var(--color-brand); }
|
||
.legend .dot.prev { background: #C7D2E8; }
|
||
.empty-note { text-align: center; color: var(--color-text-secondary); padding: 40px; grid-column: 1 / -1; }
|
||
|
||
/* ── TOP KPI CARDS ── */
|
||
.section-title { font-size: 13px; font-weight: 700; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; margin: 4px 0 12px; }
|
||
.top-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 24px; }
|
||
.kpi-stat { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px 18px; box-shadow: var(--shadow-card); border-top: 4px solid var(--color-brand); cursor: pointer; transition: box-shadow 0.2s, transform 0.2s; }
|
||
.kpi-stat:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-2px); }
|
||
.kpi-stat.accent-green { border-top-color: var(--color-green); }
|
||
.kpi-stat.accent-purple { border-top-color: var(--color-purple); }
|
||
.kpi-stat.accent-yellow { border-top-color: var(--color-orange); }
|
||
.kpi-stat-value { font-size: 26px; font-weight: 700; font-family: var(--font-mono); line-height: 1.1; margin-top: 2px; }
|
||
.kpi-stat-sub { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; min-height: 16px; }
|
||
|
||
/* ── VIEW TOGGLE ── */
|
||
.view-toggle { display: flex; gap: 4px; }
|
||
.view-toggle button { padding: 8px 11px; border: 1px solid var(--color-border); background: #fff; cursor: pointer; border-radius: var(--radius-btn); color: var(--color-text-secondary); font-family: var(--font-base); font-size: 14px; line-height: 1; transition: all 0.15s; }
|
||
.view-toggle button:hover { border-color: var(--color-brand); color: var(--color-brand); }
|
||
.view-toggle button.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
|
||
|
||
/* ── LIST VIEW ── */
|
||
.card { cursor: pointer; }
|
||
.cards.list { grid-template-columns: 1fr; }
|
||
.cards.list .card { display: grid; grid-template-columns: minmax(190px, 1.1fr) 130px minmax(160px, 2fr) minmax(150px, auto); align-items: center; gap: 22px; }
|
||
.cards.list .card-head { margin-bottom: 0; }
|
||
.cards.list .card-value { font-size: 24px; }
|
||
.cards.list .spark { margin-top: 0; height: 44px; }
|
||
.cards.list .spark-x { display: none; }
|
||
.cards.list .legend { margin-top: 0; flex-direction: column; gap: 5px; }
|
||
|
||
/* ── MODAL ── */
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(17,24,39,0.55); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
|
||
.modal-overlay.open { display: flex; }
|
||
.modal-box { background: #fff; border-radius: 16px; max-width: 880px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25); }
|
||
.modal-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; padding: 20px 24px; border-bottom: 1px solid var(--color-border); position: sticky; top: 0; background: #fff; z-index: 1; }
|
||
.modal-title { font-size: 18px; font-weight: 700; }
|
||
.modal-sub { font-size: 12px; color: var(--color-text-secondary); margin-top: 3px; }
|
||
.modal-close { background: var(--color-bg); border: none; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 16px; color: var(--color-text-secondary); flex-shrink: 0; }
|
||
.modal-close:hover { background: var(--color-border); }
|
||
.modal-body { padding: 20px 24px; }
|
||
.modal-chart-wrap { position: relative; height: 300px; margin-bottom: 20px; }
|
||
.mtable { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.mtable th, .mtable td { padding: 7px 10px; text-align: right; border-bottom: 1px solid var(--color-border); font-family: var(--font-mono); white-space: nowrap; }
|
||
.mtable th:first-child, .mtable td:first-child { text-align: left; font-family: var(--font-base); }
|
||
.mtable th { font-weight: 600; color: var(--color-text-secondary); font-size: 11px; text-transform: uppercase; letter-spacing: 0.02em; }
|
||
.mtable tbody tr:last-child td { font-weight: 700; }
|
||
|
||
/* loading / error */
|
||
#status-msg { text-align:center; padding: 60px 20px; color: var(--color-text-secondary); }
|
||
|
||
@media (max-width: 900px) { .summary-grid, .top-grid { grid-template-columns: repeat(2,1fr); } .cards.list .card { grid-template-columns: 1fr; gap: 10px; } }
|
||
@media (max-width: 560px) { .summary-grid, .top-grid { grid-template-columns: 1fr; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOGIN -->
|
||
<div id="login-screen">
|
||
<div class="login-box">
|
||
<div class="login-logo">KT</div>
|
||
<h1>Метрики МП</h1>
|
||
<p>Казахтелеком · 2026</p>
|
||
<input type="password" id="login-password" class="txt-input" placeholder="Введите пароль" style="text-align:center;margin-bottom:12px;">
|
||
<button class="btn-primary" id="btn-login">Войти</button>
|
||
<p id="login-error"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- APP -->
|
||
<div id="app">
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<div class="header-logo">KT</div>
|
||
<div>
|
||
<h1>📱 Метрики МП</h1>
|
||
<p>Использование функций МП · нарастающим итогом, год к году</p>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<span id="generated-at">—</span>
|
||
<a class="btn btn-ghost" href="../index.html">← KPI Dashboard</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
<div id="status-msg">Загрузка данных…</div>
|
||
|
||
<div id="content" style="display:none">
|
||
<div class="section-title">Ключевые показатели приложения</div>
|
||
<div class="top-grid" id="top-grid"></div>
|
||
|
||
<div class="section-title">Сравнение функций · год к году</div>
|
||
<div class="summary-grid" id="summary-grid"></div>
|
||
|
||
<div class="controls">
|
||
<input type="text" id="search" class="txt-input grow" placeholder="🔍 Поиск метрики…">
|
||
<select id="sort" class="txt-input">
|
||
<option value="cur">Сортировка: по использованию ↓</option>
|
||
<option value="growth">по приросту ↓</option>
|
||
<option value="label">по названию (А→Я)</option>
|
||
</select>
|
||
<label><input type="checkbox" id="only-new"> только новые</label>
|
||
<div class="view-toggle">
|
||
<button data-view="grid" title="Сетка">▦</button>
|
||
<button data-view="list" title="Список">☰</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cards" id="cards"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL -->
|
||
<div class="modal-overlay" id="modal">
|
||
<div class="modal-box">
|
||
<div class="modal-head">
|
||
<div>
|
||
<div class="modal-title" id="modal-title"></div>
|
||
<div class="modal-sub" id="modal-sub"></div>
|
||
</div>
|
||
<button class="modal-close" id="modal-close" title="Закрыть">✕</button>
|
||
</div>
|
||
<div class="modal-body" id="modal-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const LOGIN_PASSWORD = 'KTdash1';
|
||
const DATA_URL = 'app_metrics.json';
|
||
|
||
// Резервные данные (на случай file:// или недоступности файла). Обновляются скриптом updater.
|
||
const EMBEDDED_METRICS = /*__DATA__*/{
|
||
"generated_at": "2026-06-17T13:02:59",
|
||
"cur_year": 2026,
|
||
"prev_year": 2025,
|
||
"period_label": "с 1 января по 16 июня",
|
||
"range": {
|
||
"start": "2026-01-01",
|
||
"end": "2026-06-16"
|
||
},
|
||
"months": [
|
||
1,
|
||
2,
|
||
3,
|
||
4,
|
||
5,
|
||
6
|
||
],
|
||
"month_labels": [
|
||
"Янв",
|
||
"Фев",
|
||
"Мар",
|
||
"Апр",
|
||
"Май",
|
||
"Июн"
|
||
],
|
||
"cumulative": true,
|
||
"metrics": [
|
||
{
|
||
"key": "my_services",
|
||
"label": "Мои услуги",
|
||
"cur": 1796228,
|
||
"prev": 1472562,
|
||
"growth": 0.21979787608263693,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
321989,
|
||
621597,
|
||
980877,
|
||
1327890,
|
||
1676682,
|
||
1796228
|
||
],
|
||
"prev_cum": [
|
||
308264,
|
||
594530,
|
||
857453,
|
||
1108674,
|
||
1341165,
|
||
1472562
|
||
]
|
||
},
|
||
{
|
||
"key": "traffic",
|
||
"label": "Детализация трафика",
|
||
"cur": 1290610,
|
||
"prev": 1089617,
|
||
"growth": 0.18446206327544448,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
246604,
|
||
463349,
|
||
727262,
|
||
978030,
|
||
1179069,
|
||
1290610
|
||
],
|
||
"prev_cum": [
|
||
214948,
|
||
425004,
|
||
634594,
|
||
827602,
|
||
1001634,
|
||
1089617
|
||
]
|
||
},
|
||
{
|
||
"key": "payments",
|
||
"label": "Платежи",
|
||
"cur": 741888,
|
||
"prev": 557497,
|
||
"growth": 0.3307479681505013,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
111523,
|
||
203960,
|
||
358189,
|
||
523890,
|
||
653416,
|
||
741888
|
||
],
|
||
"prev_cum": [
|
||
120062,
|
||
232395,
|
||
331976,
|
||
427765,
|
||
512345,
|
||
557497
|
||
]
|
||
},
|
||
{
|
||
"key": "orders",
|
||
"label": "Заявки",
|
||
"cur": 845340,
|
||
"prev": 642020,
|
||
"growth": 0.31668795364630387,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
173075,
|
||
312944,
|
||
461361,
|
||
601613,
|
||
742794,
|
||
845340
|
||
],
|
||
"prev_cum": [
|
||
153918,
|
||
301147,
|
||
409051,
|
||
500862,
|
||
587480,
|
||
642020
|
||
]
|
||
},
|
||
{
|
||
"key": "loyalty",
|
||
"label": "Лояльность",
|
||
"cur": 474289,
|
||
"prev": 466803,
|
||
"growth": 0.016036743551348213,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
86429,
|
||
152587,
|
||
264979,
|
||
351823,
|
||
432724,
|
||
474289
|
||
],
|
||
"prev_cum": [
|
||
88583,
|
||
196751,
|
||
284932,
|
||
371536,
|
||
431175,
|
||
466803
|
||
]
|
||
},
|
||
{
|
||
"key": "pay",
|
||
"label": "Оплата",
|
||
"cur": 341470,
|
||
"prev": 305025,
|
||
"growth": 0.11948200967133842,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
73119,
|
||
141842,
|
||
198550,
|
||
248839,
|
||
304810,
|
||
341470
|
||
],
|
||
"prev_cum": [
|
||
50344,
|
||
101847,
|
||
150536,
|
||
205493,
|
||
269251,
|
||
305025
|
||
]
|
||
},
|
||
{
|
||
"key": "billing_detail",
|
||
"label": "Детали счета",
|
||
"cur": 502390,
|
||
"prev": 359719,
|
||
"growth": 0.39661791565082744,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
81176,
|
||
147137,
|
||
241789,
|
||
343225,
|
||
431112,
|
||
502390
|
||
],
|
||
"prev_cum": [
|
||
62482,
|
||
134395,
|
||
200427,
|
||
260559,
|
||
315518,
|
||
359719
|
||
]
|
||
},
|
||
{
|
||
"key": "viktorina",
|
||
"label": "Викторина KT Club",
|
||
"cur": 213879,
|
||
"prev": 298475,
|
||
"growth": -0.28342742273222216,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
0,
|
||
1,
|
||
118526,
|
||
213879,
|
||
213879,
|
||
213879
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
49414,
|
||
298475,
|
||
298475,
|
||
298475
|
||
]
|
||
},
|
||
{
|
||
"key": "partners",
|
||
"label": "Акции партнеров",
|
||
"cur": 197777,
|
||
"prev": 95016,
|
||
"growth": 1.0815125873537088,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
40566,
|
||
65001,
|
||
117646,
|
||
149940,
|
||
185907,
|
||
197777
|
||
],
|
||
"prev_cum": [
|
||
27730,
|
||
49011,
|
||
65023,
|
||
75359,
|
||
89282,
|
||
95016
|
||
]
|
||
},
|
||
{
|
||
"key": "tv_plus",
|
||
"label": "TV+",
|
||
"cur": 65461,
|
||
"prev": 96208,
|
||
"growth": -0.31958880758356895,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
11947,
|
||
20939,
|
||
33969,
|
||
45372,
|
||
56326,
|
||
65461
|
||
],
|
||
"prev_cum": [
|
||
24830,
|
||
44479,
|
||
61671,
|
||
76824,
|
||
90692,
|
||
96208
|
||
]
|
||
},
|
||
{
|
||
"key": "boosters",
|
||
"label": "Бустеры",
|
||
"cur": 123015,
|
||
"prev": 54525,
|
||
"growth": 1.256121045392022,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
15882,
|
||
33486,
|
||
61530,
|
||
88555,
|
||
114328,
|
||
123015
|
||
],
|
||
"prev_cum": [
|
||
7244,
|
||
12487,
|
||
18093,
|
||
31284,
|
||
49552,
|
||
54525
|
||
]
|
||
},
|
||
{
|
||
"key": "roaming",
|
||
"label": "Роуминг",
|
||
"cur": 22826,
|
||
"prev": 39835,
|
||
"growth": -0.4269863185640768,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
2924,
|
||
5494,
|
||
8974,
|
||
11894,
|
||
18923,
|
||
22826
|
||
],
|
||
"prev_cum": [
|
||
6678,
|
||
13021,
|
||
19649,
|
||
27201,
|
||
35474,
|
||
39835
|
||
]
|
||
},
|
||
{
|
||
"key": "pereoform",
|
||
"label": "Переоформление",
|
||
"cur": 35385,
|
||
"prev": 23802,
|
||
"growth": 0.48663977816990167,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
7556,
|
||
12691,
|
||
18718,
|
||
24711,
|
||
30988,
|
||
35385
|
||
],
|
||
"prev_cum": [
|
||
4876,
|
||
9244,
|
||
13556,
|
||
17422,
|
||
21572,
|
||
23802
|
||
]
|
||
},
|
||
{
|
||
"key": "aitu_music",
|
||
"label": "Aitu Music",
|
||
"cur": 8872,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
1048,
|
||
4000,
|
||
5553,
|
||
7271,
|
||
8872
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "online_booking",
|
||
"label": "Онлайн очередь",
|
||
"cur": 22529,
|
||
"prev": 5523,
|
||
"growth": 3.0791236646749955,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
5636,
|
||
10095,
|
||
13892,
|
||
17262,
|
||
20514,
|
||
22529
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
1848,
|
||
4407,
|
||
5523
|
||
]
|
||
},
|
||
{
|
||
"key": "my_docs",
|
||
"label": "Мои документы",
|
||
"cur": 54148,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
14980,
|
||
24658,
|
||
33590,
|
||
41765,
|
||
49555,
|
||
54148
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "dz_statement",
|
||
"label": "Справка о ДЗ",
|
||
"cur": 134463,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
35454,
|
||
64672,
|
||
85004,
|
||
100575,
|
||
119914,
|
||
134463
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "new_boosters_roaming_kcell",
|
||
"label": "Новая линейка бустеров и роумингов Кселл",
|
||
"cur": 28858,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
1927,
|
||
4523,
|
||
13318,
|
||
21056,
|
||
28240,
|
||
28858
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "adsl",
|
||
"label": "ADSL отключение услуги",
|
||
"cur": 69,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
20,
|
||
33,
|
||
44,
|
||
55,
|
||
65,
|
||
69
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "law_and_order",
|
||
"label": "Закон и порядок",
|
||
"cur": 1598,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
232,
|
||
671,
|
||
1034,
|
||
1377,
|
||
1598
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "acs",
|
||
"label": "ACS",
|
||
"cur": 9447,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
2329,
|
||
7499,
|
||
7800,
|
||
8174,
|
||
9447
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "kaspi_freedom_pay",
|
||
"label": "Прием платежей через Freedom и Kaspi",
|
||
"cur": 62724,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
8427,
|
||
24816,
|
||
39893,
|
||
53445,
|
||
62724
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "csat",
|
||
"label": "CSAT",
|
||
"cur": 4057,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
4057
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "multicustomer",
|
||
"label": "Мультикастомер",
|
||
"cur": 169,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
99,
|
||
169
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "tv_plus_setup",
|
||
"label": "Настройка TV+",
|
||
"cur": 4908,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2630,
|
||
4908
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "static_ip",
|
||
"label": "Статический IP",
|
||
"cur": 119,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
35,
|
||
75,
|
||
119
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "turbo_button",
|
||
"label": "Turbo кнопка",
|
||
"cur": 4638,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
498,
|
||
2789,
|
||
4638
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "real_estate_docs",
|
||
"label": "Справка о недвижимости",
|
||
"cur": 79,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
23,
|
||
44,
|
||
52,
|
||
70,
|
||
79
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
}
|
||
],
|
||
"top": {
|
||
"as_of": "2026-06-16",
|
||
"registered_total": 1078231,
|
||
"installs_total": 2307490,
|
||
"installs_ios": 924083,
|
||
"installs_android": 1383407,
|
||
"mau": 185155,
|
||
"dau": 17757,
|
||
"trend_labels": [
|
||
"Июн 25",
|
||
"Июл 25",
|
||
"Авг 25",
|
||
"Сен 25",
|
||
"Окт 25",
|
||
"Ноя 25",
|
||
"Дек 25",
|
||
"Янв 26",
|
||
"Фев 26",
|
||
"Мар 26",
|
||
"Апр 26",
|
||
"Май 26",
|
||
"Июн 26"
|
||
],
|
||
"registered_series": [
|
||
925945,
|
||
937312,
|
||
949404,
|
||
961772,
|
||
973331,
|
||
985194,
|
||
1000768,
|
||
1016923,
|
||
1030968,
|
||
1043887,
|
||
1056398,
|
||
1068794,
|
||
1078231
|
||
],
|
||
"installs_series": [
|
||
1485273,
|
||
1519017,
|
||
1552922,
|
||
1722057,
|
||
1803173,
|
||
1879957,
|
||
1973234,
|
||
2053773,
|
||
2131472,
|
||
2176215,
|
||
2215453,
|
||
2260248,
|
||
2307490
|
||
],
|
||
"mau_series": [
|
||
203025,
|
||
137297,
|
||
147812,
|
||
123950,
|
||
92999,
|
||
92384,
|
||
117661,
|
||
174283,
|
||
165292,
|
||
182520,
|
||
178778,
|
||
162540,
|
||
121894
|
||
],
|
||
"dau_series": [
|
||
35538,
|
||
18569,
|
||
19517,
|
||
15670,
|
||
11862,
|
||
12247,
|
||
17946,
|
||
27955,
|
||
28326,
|
||
29972,
|
||
28007,
|
||
25391,
|
||
26288
|
||
]
|
||
}
|
||
};
|
||
|
||
const App = { data: null };
|
||
|
||
// ───────── helpers ─────────
|
||
const fmtInt = n => (n || 0).toLocaleString('ru-RU');
|
||
function fmtPct(g) {
|
||
if (g === null || g === undefined) return '';
|
||
const s = (g * 100);
|
||
return (s >= 0 ? '+' : '') + s.toFixed(1) + '%';
|
||
}
|
||
function classify(m) {
|
||
if (m.is_new) return 'new';
|
||
if (m.growth === null) return 'flat';
|
||
if (m.growth > 0.0005) return 'up';
|
||
if (m.growth < -0.0005) return 'down';
|
||
return 'flat';
|
||
}
|
||
function badgeText(m, cls) {
|
||
if (cls === 'new') return 'NEW';
|
||
if (cls === 'flat') return '≈ 0%';
|
||
return fmtPct(m.growth);
|
||
}
|
||
|
||
// ───────── render ─────────
|
||
function renderSummary() {
|
||
const d = App.data;
|
||
const totCur = d.metrics.reduce((s, m) => s + m.cur, 0);
|
||
const totPrev = d.metrics.reduce((s, m) => s + m.prev, 0);
|
||
const overall = totPrev > 0 ? (totCur - totPrev) / totPrev : null;
|
||
const nNew = d.metrics.filter(m => m.is_new).length;
|
||
const nUp = d.metrics.filter(m => !m.is_new && m.growth > 0).length;
|
||
const nDown = d.metrics.filter(m => !m.is_new && m.growth < 0).length;
|
||
|
||
const cards = [
|
||
{ cls: '', label: 'Период', value: d.cur_year + ' / ' + d.prev_year, sub: d.period_label },
|
||
{ cls: 'accent-green', label: 'Обращений · ' + d.cur_year, value: fmtInt(totCur),
|
||
sub: 'против ' + fmtInt(totPrev) + ' в ' + d.prev_year + (overall !== null ? ' · ' + fmtPct(overall) : '') },
|
||
{ cls: 'accent-yellow', label: 'Динамика', value: '▲ ' + nUp + ' ▼ ' + nDown,
|
||
sub: nUp + ' выросли, ' + nDown + ' снизились' },
|
||
{ cls: 'accent-purple', label: 'Новых функций', value: String(nNew),
|
||
sub: 'появились в ' + d.cur_year + ' году' },
|
||
];
|
||
document.getElementById('summary-grid').innerHTML = cards.map(c => `
|
||
<div class="sum-card ${c.cls}">
|
||
<div class="sum-label">${c.label}</div>
|
||
<div class="sum-value">${c.value}</div>
|
||
<div class="sum-sub">${c.sub}</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderCards() {
|
||
const d = App.data;
|
||
const q = document.getElementById('search').value.trim().toLowerCase();
|
||
const sort = document.getElementById('sort').value;
|
||
const onlyNew = document.getElementById('only-new').checked;
|
||
|
||
let list = d.metrics.slice();
|
||
if (onlyNew) list = list.filter(m => m.is_new);
|
||
if (q) list = list.filter(m => m.label.toLowerCase().includes(q));
|
||
|
||
list.sort((a, b) => {
|
||
if (sort === 'label') return a.label.localeCompare(b.label, 'ru');
|
||
if (sort === 'growth') {
|
||
// новые — в конце при сортировке по приросту (у них нет базы сравнения)
|
||
const ga = a.is_new ? -Infinity : (a.growth ?? -Infinity);
|
||
const gb = b.is_new ? -Infinity : (b.growth ?? -Infinity);
|
||
return gb - ga;
|
||
}
|
||
return b.cur - a.cur; // по использованию
|
||
});
|
||
|
||
const el = document.getElementById('cards');
|
||
if (!list.length) { el.innerHTML = '<div class="empty-note">Ничего не найдено</div>'; return; }
|
||
|
||
const labels = d.month_labels || [];
|
||
el.innerHTML = list.map(m => {
|
||
const cls = classify(m);
|
||
const curCum = m.cur_cum || [m.cur];
|
||
const prevCum = m.prev_cum || [m.prev];
|
||
return `
|
||
<div class="card ${cls}" data-key="${m.key}">
|
||
<div class="card-head">
|
||
<div>
|
||
<div class="card-title">${m.label}</div>
|
||
<div class="card-sub">нарастающим итогом · ${d.cur_year} vs ${d.prev_year}</div>
|
||
</div>
|
||
<span class="badge ${cls}">${badgeText(m, cls)}</span>
|
||
</div>
|
||
<div class="card-value">${fmtInt(m.cur)}<span class="card-value-year"> · ${d.cur_year}</span></div>
|
||
${sparkline(curCum, prevCum)}
|
||
<div class="spark-x"><span>${labels[0] || ''}</span><span>${labels[labels.length - 1] || ''}</span></div>
|
||
<div class="legend">
|
||
<span><i class="dot cur"></i>${d.cur_year}: <b>${fmtInt(m.cur)}</b></span>
|
||
<span><i class="dot prev"></i>${d.prev_year}: <b>${fmtInt(m.prev)}</b></span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Накопительный мини-график: две кривые (пунктир — прошлый год, сплошная — текущий)
|
||
function sparkline(curCum, prevCum) {
|
||
const W = 100, H = 34, pad = 3;
|
||
const n = Math.max(curCum.length, prevCum.length, 1);
|
||
const max = Math.max(1, ...curCum, ...prevCum);
|
||
const x = i => n <= 1 ? W / 2 : pad + i * (W - 2 * pad) / (n - 1);
|
||
const y = v => H - pad - (v / max) * (H - 2 * pad);
|
||
const pts = arr => arr.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
||
const lastI = curCum.length - 1;
|
||
return `<svg class="spark" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
|
||
<polyline class="sp-prev" points="${pts(prevCum)}"/>
|
||
<polyline class="sp-cur" points="${pts(curCum)}"/>
|
||
<circle class="dot-cur" cx="${x(lastI).toFixed(1)}" cy="${y(curCum[lastI] || 0).toFixed(1)}" r="2.2"/>
|
||
</svg>`;
|
||
}
|
||
|
||
// ───────── верхние KPI ─────────
|
||
const ACCENT_COLOR = { '': '#0052CC', 'accent-green': '#10B981', 'accent-purple': '#8B5CF6', 'accent-yellow': '#F97316' };
|
||
|
||
function fmtDate(s) {
|
||
if (!s) return '—';
|
||
const p = String(s).slice(0, 10).split('-');
|
||
return p.length === 3 ? `${p[2]}.${p[1]}.${p[0]}` : s;
|
||
}
|
||
function delta(cum) { return cum.map((v, i) => i === 0 ? v : v - cum[i - 1]); }
|
||
|
||
// одиночный мини-график (одна линия)
|
||
function miniline(series, color) {
|
||
if (!series || !series.length) return '';
|
||
const W = 100, H = 30, pad = 3, n = series.length;
|
||
const max = Math.max(...series), min = Math.min(...series), span = (max - min) || 1;
|
||
const x = i => n <= 1 ? W / 2 : pad + i * (W - 2 * pad) / (n - 1);
|
||
const y = v => H - pad - ((v - min) / span) * (H - 2 * pad);
|
||
const pts = series.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
||
return `<svg class="spark" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
|
||
<polyline points="${pts}" style="fill:none;stroke:${color};stroke-width:2;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round"/></svg>`;
|
||
}
|
||
|
||
function renderTop() {
|
||
const t = App.data.top;
|
||
const grid = document.getElementById('top-grid');
|
||
const title = grid.previousElementSibling;
|
||
if (!t) { grid.style.display = 'none'; if (title) title.style.display = 'none'; return; }
|
||
const cards = [
|
||
{ key: 'registered', accent: '', label: 'Регистрации', value: fmtInt(t.registered_total), sub: 'всего зарегистрировано', series: t.registered_series },
|
||
{ key: 'installs', accent: 'accent-green', label: 'Скачивания', value: fmtInt(t.installs_total), sub: `iOS ${fmtInt(t.installs_ios)} · Android ${fmtInt(t.installs_android)}`, series: t.installs_series },
|
||
{ key: 'mau', accent: 'accent-purple', label: 'MAU', value: fmtInt(t.mau), sub: 'на ' + fmtDate(t.as_of), series: t.mau_series },
|
||
{ key: 'dau', accent: 'accent-yellow', label: 'DAU', value: fmtInt(t.dau), sub: 'на ' + fmtDate(t.as_of), series: t.dau_series },
|
||
];
|
||
grid.innerHTML = cards.map(c => `
|
||
<div class="kpi-stat ${c.accent}" data-top="${c.key}">
|
||
<div class="sum-label">${c.label}</div>
|
||
<div class="kpi-stat-value">${c.value}</div>
|
||
<div class="kpi-stat-sub">${c.sub}</div>
|
||
${miniline(c.series, ACCENT_COLOR[c.accent])}
|
||
</div>`).join('');
|
||
}
|
||
|
||
// ───────── вид: сетка / список ─────────
|
||
function setView(v) {
|
||
App.view = v;
|
||
try { localStorage.setItem('mp_view', v); } catch (e) {}
|
||
document.getElementById('cards').classList.toggle('list', v === 'list');
|
||
document.querySelectorAll('.view-toggle button').forEach(b => b.classList.toggle('active', b.dataset.view === v));
|
||
}
|
||
|
||
// ───────── модалка ─────────
|
||
let modalChart = null;
|
||
function closeModal() {
|
||
document.getElementById('modal').classList.remove('open');
|
||
if (modalChart) { modalChart.destroy(); modalChart = null; }
|
||
}
|
||
function openModal(title, sub, bodyHTML) {
|
||
document.getElementById('modal-title').textContent = title;
|
||
document.getElementById('modal-sub').textContent = sub;
|
||
document.getElementById('modal-body').innerHTML = bodyHTML;
|
||
document.getElementById('modal').classList.add('open');
|
||
}
|
||
function drawChart(labels, datasets, monthlyMap) {
|
||
if (typeof Chart === 'undefined') return;
|
||
const ctx = document.getElementById('modal-canvas');
|
||
if (!ctx) return;
|
||
modalChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: { labels, datasets: datasets.map(s => ({
|
||
label: s.label, data: s.data, borderColor: s.color, backgroundColor: s.color,
|
||
borderWidth: 2.5, tension: 0.3, pointRadius: 3, pointHoverRadius: 5, fill: false,
|
||
borderDash: s.dash || [],
|
||
})) },
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
plugins: {
|
||
legend: { position: 'bottom', labels: { usePointStyle: true, boxWidth: 8 } },
|
||
tooltip: { callbacks: { label: (c) => {
|
||
const base = `${c.dataset.label}: ${fmtInt(c.parsed.y)}`;
|
||
const mo = monthlyMap && monthlyMap[c.dataset.label];
|
||
return mo ? `${base} (за месяц +${fmtInt(mo[c.dataIndex])})` : base;
|
||
} } },
|
||
},
|
||
scales: { y: { ticks: { callback: v => fmtInt(v) }, grid: { color: '#F0F2F5' } }, x: { grid: { display: false } } },
|
||
},
|
||
});
|
||
}
|
||
|
||
function openMetricModal(key) {
|
||
const d = App.data, m = d.metrics.find(x => x.key === key);
|
||
if (!m) return;
|
||
const labels = d.month_labels || [];
|
||
const curCum = m.cur_cum || [m.cur], prevCum = m.prev_cum || [m.prev];
|
||
const curMo = delta(curCum), prevMo = delta(prevCum);
|
||
const cy = String(d.cur_year), py = String(d.prev_year);
|
||
let rows = '';
|
||
for (let i = 0; i < labels.length; i++) {
|
||
rows += `<tr><td>${labels[i]}</td><td>${fmtInt(curCum[i])}</td><td>+${fmtInt(curMo[i])}</td><td>${fmtInt(prevCum[i])}</td><td>+${fmtInt(prevMo[i])}</td></tr>`;
|
||
}
|
||
const body = `
|
||
<div class="modal-chart-wrap"><canvas id="modal-canvas"></canvas></div>
|
||
<table class="mtable">
|
||
<thead><tr><th>Месяц</th><th>${cy} ∑</th><th>${cy} /мес</th><th>${py} ∑</th><th>${py} /мес</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>`;
|
||
openModal(m.label, `Нарастающим итогом · ${d.period_label}`, body);
|
||
drawChart(labels, [
|
||
{ label: cy, data: curCum, color: '#0052CC' },
|
||
{ label: py, data: prevCum, color: '#9CB4D9', dash: [5, 4] },
|
||
], { [cy]: curMo, [py]: prevMo });
|
||
}
|
||
|
||
function openTopModal(key) {
|
||
const t = App.data.top;
|
||
if (!t) return;
|
||
const labels = t.trend_labels || [];
|
||
const conf = {
|
||
registered: { title: 'Регистрации', color: '#0052CC', data: t.registered_series, note: 'Накопительно (всего зарегистрировано)' },
|
||
installs: { title: 'Скачивания', color: '#10B981', data: t.installs_series, note: `Накопительно · iOS ${fmtInt(t.installs_ios)} · Android ${fmtInt(t.installs_android)}` },
|
||
mau: { title: 'MAU', color: '#8B5CF6', data: t.mau_series, note: 'Среднемесячное число активных за месяц' },
|
||
dau: { title: 'DAU', color: '#F97316', data: t.dau_series, note: 'Среднесуточное число активных за месяц' },
|
||
}[key];
|
||
if (!conf) return;
|
||
let rows = '';
|
||
for (let i = 0; i < labels.length; i++) rows += `<tr><td>${labels[i]}</td><td>${fmtInt(conf.data[i])}</td></tr>`;
|
||
const body = `
|
||
<div class="modal-chart-wrap"><canvas id="modal-canvas"></canvas></div>
|
||
<table class="mtable">
|
||
<thead><tr><th>Месяц</th><th>${conf.title}</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>`;
|
||
openModal(conf.title, conf.note + ' · по ' + fmtDate(t.as_of), body);
|
||
drawChart(labels, [{ label: conf.title, data: conf.data, color: conf.color }], null);
|
||
}
|
||
|
||
function renderAll() {
|
||
document.getElementById('status-msg').style.display = 'none';
|
||
document.getElementById('content').style.display = 'block';
|
||
const gen = App.data.generated_at ? new Date(App.data.generated_at) : null;
|
||
document.getElementById('generated-at').textContent =
|
||
'Обновлено: ' + (gen ? gen.toLocaleDateString('ru-RU') : '—');
|
||
renderTop();
|
||
renderSummary();
|
||
let v = 'grid';
|
||
try { v = localStorage.getItem('mp_view') || 'grid'; } catch (e) {}
|
||
setView(v);
|
||
renderCards();
|
||
}
|
||
|
||
// ───────── load ─────────
|
||
async function loadData() {
|
||
if (window.location.protocol !== 'file:') {
|
||
try {
|
||
const r = await fetch(DATA_URL, { cache: 'no-cache' });
|
||
if (r.ok) { App.data = await r.json(); renderAll(); return; }
|
||
} catch (e) { /* fall through */ }
|
||
}
|
||
if (EMBEDDED_METRICS && EMBEDDED_METRICS.metrics) {
|
||
App.data = EMBEDDED_METRICS; renderAll(); return;
|
||
}
|
||
document.getElementById('status-msg').textContent =
|
||
'Не удалось загрузить данные (app_metrics.json).';
|
||
}
|
||
|
||
document.addEventListener('input', e => {
|
||
if (['search', 'sort', 'only-new'].includes(e.target.id)) renderCards();
|
||
});
|
||
|
||
// переключатель вида
|
||
document.addEventListener('click', e => {
|
||
const vb = e.target.closest('.view-toggle button');
|
||
if (vb) { setView(vb.dataset.view); return; }
|
||
// клик по карточке метрики или верхней KPI
|
||
const top = e.target.closest('.kpi-stat');
|
||
if (top) { openTopModal(top.dataset.top); return; }
|
||
const card = e.target.closest('.card[data-key]');
|
||
if (card) { openMetricModal(card.dataset.key); return; }
|
||
});
|
||
|
||
// закрытие модалки
|
||
document.getElementById('modal-close').addEventListener('click', closeModal);
|
||
document.getElementById('modal').addEventListener('click', e => { if (e.target.id === 'modal') closeModal(); });
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
||
|
||
// ───────── login ─────────
|
||
function showApp() {
|
||
document.getElementById('login-screen').style.display = 'none';
|
||
document.getElementById('app').style.display = 'block';
|
||
loadData();
|
||
}
|
||
function handleLogin() {
|
||
const inp = document.getElementById('login-password');
|
||
if (inp.value === LOGIN_PASSWORD) {
|
||
if (window.sessionStorage) sessionStorage.setItem('kpi_auth', '1');
|
||
showApp();
|
||
} else {
|
||
document.getElementById('login-error').textContent = 'Неверный пароль';
|
||
inp.value = '';
|
||
}
|
||
}
|
||
document.getElementById('btn-login').addEventListener('click', handleLogin);
|
||
document.getElementById('login-password').addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); });
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (window.sessionStorage && sessionStorage.getItem('kpi_auth') === '1') showApp();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|