- updater: group by report_period_id (YYYYmm), current month capped to yesterday symmetrically for both years; per-year cumulative reset each Jan; adsl CTE now date-bounded for equal-period comparison - JSON adds months/month_labels + per-metric cur_cum/prev_cum - page: per-card cumulative sparkline (2025 dashed vs 2026 solid) + legend
1045 lines
28 KiB
HTML
1045 lines
28 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">
|
||
<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; }
|
||
|
||
/* loading / error */
|
||
#status-msg { text-align:center; padding: 60px 20px; color: var(--color-text-secondary); }
|
||
|
||
@media (max-width: 900px) { .summary-grid { grid-template-columns: repeat(2,1fr); } }
|
||
@media (max-width: 560px) { .summary-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="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>
|
||
|
||
<div class="cards" id="cards"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const LOGIN_PASSWORD = 'KTdash1';
|
||
const DATA_URL = 'app_metrics.json';
|
||
|
||
// Резервные данные (на случай file:// или недоступности файла). Обновляются скриптом updater.
|
||
const EMBEDDED_METRICS = /*__DATA__*/{
|
||
"generated_at": "2026-06-16T17:55:07",
|
||
"cur_year": 2026,
|
||
"prev_year": 2025,
|
||
"period_label": "с 1 января по 15 июня",
|
||
"range": {
|
||
"start": "2026-01-01",
|
||
"end": "2026-06-15"
|
||
},
|
||
"months": [
|
||
1,
|
||
2,
|
||
3,
|
||
4,
|
||
5,
|
||
6
|
||
],
|
||
"month_labels": [
|
||
"Янв",
|
||
"Фев",
|
||
"Мар",
|
||
"Апр",
|
||
"Май",
|
||
"Июн"
|
||
],
|
||
"cumulative": true,
|
||
"metrics": [
|
||
{
|
||
"key": "my_services",
|
||
"label": "Мои услуги",
|
||
"cur": 1769470,
|
||
"prev": 1464715,
|
||
"growth": 0.2080643674708049,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
321989,
|
||
621597,
|
||
980877,
|
||
1327884,
|
||
1676590,
|
||
1769470
|
||
],
|
||
"prev_cum": [
|
||
308264,
|
||
594530,
|
||
857453,
|
||
1108674,
|
||
1341165,
|
||
1464715
|
||
]
|
||
},
|
||
{
|
||
"key": "traffic",
|
||
"label": "Детализация трафика",
|
||
"cur": 1271563,
|
||
"prev": 1083912,
|
||
"growth": 0.17312383293108666,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
246604,
|
||
463349,
|
||
727262,
|
||
978019,
|
||
1179021,
|
||
1271563
|
||
],
|
||
"prev_cum": [
|
||
214948,
|
||
425004,
|
||
634594,
|
||
827602,
|
||
1001634,
|
||
1083912
|
||
]
|
||
},
|
||
{
|
||
"key": "payments",
|
||
"label": "Платежи",
|
||
"cur": 730185,
|
||
"prev": 555155,
|
||
"growth": 0.31528131783015556,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
111523,
|
||
203960,
|
||
358189,
|
||
523887,
|
||
653394,
|
||
730185
|
||
],
|
||
"prev_cum": [
|
||
120062,
|
||
232395,
|
||
331976,
|
||
427765,
|
||
512345,
|
||
555155
|
||
]
|
||
},
|
||
{
|
||
"key": "orders",
|
||
"label": "Заявки",
|
||
"cur": 826255,
|
||
"prev": 638026,
|
||
"growth": 0.2950177578970136,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
173075,
|
||
312944,
|
||
461361,
|
||
601607,
|
||
742766,
|
||
826255
|
||
],
|
||
"prev_cum": [
|
||
153918,
|
||
301147,
|
||
409051,
|
||
500862,
|
||
587480,
|
||
638026
|
||
]
|
||
},
|
||
{
|
||
"key": "loyalty",
|
||
"label": "Лояльность",
|
||
"cur": 470969,
|
||
"prev": 465354,
|
||
"growth": 0.012066083024965939,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
86429,
|
||
152587,
|
||
264979,
|
||
351819,
|
||
432695,
|
||
470969
|
||
],
|
||
"prev_cum": [
|
||
88583,
|
||
196751,
|
||
284932,
|
||
371536,
|
||
431175,
|
||
465354
|
||
]
|
||
},
|
||
{
|
||
"key": "pay",
|
||
"label": "Оплата",
|
||
"cur": 337103,
|
||
"prev": 303542,
|
||
"growth": 0.11056460061540084,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
73119,
|
||
141842,
|
||
198550,
|
||
248838,
|
||
304803,
|
||
337103
|
||
],
|
||
"prev_cum": [
|
||
50344,
|
||
101847,
|
||
150536,
|
||
205493,
|
||
269251,
|
||
303542
|
||
]
|
||
},
|
||
{
|
||
"key": "billing_detail",
|
||
"label": "Детали счета",
|
||
"cur": 496915,
|
||
"prev": 358855,
|
||
"growth": 0.38472363489431666,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
81176,
|
||
147137,
|
||
241789,
|
||
343223,
|
||
431096,
|
||
496915
|
||
],
|
||
"prev_cum": [
|
||
62482,
|
||
134395,
|
||
200427,
|
||
260559,
|
||
315518,
|
||
358855
|
||
]
|
||
},
|
||
{
|
||
"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": 197009,
|
||
"prev": 94796,
|
||
"growth": 1.0782416979619394,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
40566,
|
||
65001,
|
||
117646,
|
||
149938,
|
||
185887,
|
||
197009
|
||
],
|
||
"prev_cum": [
|
||
27730,
|
||
49011,
|
||
65023,
|
||
75359,
|
||
89282,
|
||
94796
|
||
]
|
||
},
|
||
{
|
||
"key": "tv_plus",
|
||
"label": "TV+",
|
||
"cur": 64104,
|
||
"prev": 95883,
|
||
"growth": -0.33143518663370986,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
11947,
|
||
20939,
|
||
33969,
|
||
45372,
|
||
56323,
|
||
64104
|
||
],
|
||
"prev_cum": [
|
||
24830,
|
||
44479,
|
||
61671,
|
||
76824,
|
||
90692,
|
||
95883
|
||
]
|
||
},
|
||
{
|
||
"key": "boosters",
|
||
"label": "Бустеры",
|
||
"cur": 121065,
|
||
"prev": 54053,
|
||
"growth": 1.2397461750504135,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
15882,
|
||
33486,
|
||
61530,
|
||
88553,
|
||
114321,
|
||
121065
|
||
],
|
||
"prev_cum": [
|
||
7244,
|
||
12487,
|
||
18093,
|
||
31284,
|
||
49552,
|
||
54053
|
||
]
|
||
},
|
||
{
|
||
"key": "roaming",
|
||
"label": "Роуминг",
|
||
"cur": 22160,
|
||
"prev": 39403,
|
||
"growth": -0.43760627363398724,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
2924,
|
||
5494,
|
||
8974,
|
||
11894,
|
||
18915,
|
||
22160
|
||
],
|
||
"prev_cum": [
|
||
6678,
|
||
13021,
|
||
19649,
|
||
27201,
|
||
35474,
|
||
39403
|
||
]
|
||
},
|
||
{
|
||
"key": "pereoform",
|
||
"label": "Переоформление",
|
||
"cur": 34570,
|
||
"prev": 23644,
|
||
"growth": 0.46210455083742175,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
7556,
|
||
12691,
|
||
18718,
|
||
24710,
|
||
30986,
|
||
34570
|
||
],
|
||
"prev_cum": [
|
||
4876,
|
||
9244,
|
||
13556,
|
||
17422,
|
||
21572,
|
||
23644
|
||
]
|
||
},
|
||
{
|
||
"key": "aitu_music",
|
||
"label": "Aitu Music",
|
||
"cur": 8651,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
1048,
|
||
4000,
|
||
5553,
|
||
7270,
|
||
8651
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "online_booking",
|
||
"label": "Онлайн очередь",
|
||
"cur": 22144,
|
||
"prev": 5449,
|
||
"growth": 3.063864929344834,
|
||
"is_new": false,
|
||
"cur_cum": [
|
||
5636,
|
||
10095,
|
||
13892,
|
||
17262,
|
||
20514,
|
||
22144
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
1848,
|
||
4407,
|
||
5449
|
||
]
|
||
},
|
||
{
|
||
"key": "my_docs",
|
||
"label": "Мои документы",
|
||
"cur": 53376,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
14980,
|
||
24658,
|
||
33590,
|
||
41765,
|
||
49552,
|
||
53376
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "dz_statement",
|
||
"label": "Справка о ДЗ",
|
||
"cur": 132795,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
35454,
|
||
64672,
|
||
85004,
|
||
100574,
|
||
119910,
|
||
132795
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "new_boosters_roaming_kcell",
|
||
"label": "Новая линейка бустеров и роумингов Кселл",
|
||
"cur": 28626,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
1927,
|
||
4523,
|
||
13318,
|
||
21056,
|
||
28239,
|
||
28626
|
||
],
|
||
"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": 1555,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
232,
|
||
671,
|
||
1034,
|
||
1377,
|
||
1555
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "acs",
|
||
"label": "ACS",
|
||
"cur": 9154,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
2329,
|
||
7499,
|
||
7800,
|
||
8174,
|
||
9154
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "kaspi_freedom_pay",
|
||
"label": "Прием платежей через Freedom и Kaspi",
|
||
"cur": 61481,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
8427,
|
||
24816,
|
||
39893,
|
||
53444,
|
||
61481
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "csat",
|
||
"label": "CSAT",
|
||
"cur": 2486,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2486
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "multicustomer",
|
||
"label": "Мультикастомер",
|
||
"cur": 164,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
99,
|
||
164
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "tv_plus_setup",
|
||
"label": "Настройка TV+",
|
||
"cur": 4545,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2628,
|
||
4545
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "static_ip",
|
||
"label": "Статический IP",
|
||
"cur": 108,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
35,
|
||
75,
|
||
108
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "turbo_button",
|
||
"label": "Turbo кнопка",
|
||
"cur": 4312,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
498,
|
||
2789,
|
||
4312
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
{
|
||
"key": "real_estate_docs",
|
||
"label": "Справка о недвижимости",
|
||
"cur": 78,
|
||
"prev": 0,
|
||
"growth": null,
|
||
"is_new": true,
|
||
"cur_cum": [
|
||
0,
|
||
23,
|
||
44,
|
||
52,
|
||
70,
|
||
78
|
||
],
|
||
"prev_cum": [
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
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}">
|
||
<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>`;
|
||
}
|
||
|
||
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') : '—');
|
||
renderSummary();
|
||
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).';
|
||
}
|
||
|
||
['search', 'sort', 'only-new'].forEach(id =>
|
||
document.addEventListener('input', e => { if (e.target.id === id) renderCards(); }));
|
||
|
||
// ───────── 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>
|