1557 lines
106 KiB
HTML
1557 lines
106 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>KPI InDigiCo — Казахтелеком 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">
|
||
<!-- Токен DeepSeek: положите рядом файл config.js с содержимым: window.DS_KEY = 'sk-...' -->
|
||
<script src="config.js" onerror="console.info('config.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-orange: #F97316;
|
||
--color-orange-bg: #FFF7ED;
|
||
--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; }
|
||
|
||
/* ── LOADING / UPLOAD SCREENS ── */
|
||
#loading-screen {
|
||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%);
|
||
}
|
||
.loading-box { text-align: center; color: #fff; }
|
||
.spinner-lg { width: 48px; height: 48px; border: 4px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 20px; }
|
||
.loading-box p { font-size: 16px; font-weight: 500; opacity: 0.9; }
|
||
|
||
#upload-screen { min-height: 100vh; display: none; align-items: center; justify-content: center; background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%); }
|
||
#login-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%); }
|
||
.upload-box { background: #fff; border-radius: 20px; padding: 48px 56px; text-align: center; max-width: 480px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.15); }
|
||
.upload-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; }
|
||
.upload-box h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
||
.upload-box > p { color: var(--color-text-secondary); margin-bottom: 32px; font-size: 14px; }
|
||
.upload-note { margin-bottom: 24px; padding: 10px 14px; background: var(--color-yellow-bg); border-radius: 8px; font-size: 12px; color: #92400E; border: 1px solid rgba(245,158,11,0.3); text-align: left; line-height: 1.6; }
|
||
.upload-btn-label { display: inline-block; background: var(--color-brand); color: #fff; padding: 14px 32px; border-radius: var(--radius-btn); font-weight: 600; font-size: 15px; cursor: pointer; transition: background 0.2s; }
|
||
.upload-btn-label:hover { background: #003fa3; }
|
||
#file-input { display: none; }
|
||
.upload-hint { margin-top: 14px; font-size: 12px; color: var(--color-text-secondary); }
|
||
#upload-error { color: var(--color-red); margin-top: 10px; font-size: 13px; min-height: 18px; }
|
||
|
||
/* ── HEADER ── */
|
||
#dashboard { 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; }
|
||
#last-updated { 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; }
|
||
.btn-ghost { background: rgba(255,255,255,0.15); color: #fff; }
|
||
.btn-ghost:hover { background: rgba(255,255,255,0.25); }
|
||
.btn-outline { background: transparent; border: 1px solid rgba(255,255,255,0.4); color: #fff; }
|
||
.btn-outline:hover { background: rgba(255,255,255,0.1); }
|
||
|
||
/* ── SUMMARY BAR ── */
|
||
.summary-section { padding: 20px 24px 0; }
|
||
.summary-bar { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; }
|
||
.kpi-card { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px; box-shadow: var(--shadow-card); cursor: pointer; transition: box-shadow 0.25s, transform 0.25s; border-top: 4px solid var(--color-border); }
|
||
.kpi-card:hover { box-shadow: var(--shadow-card-hover); transform: scale(1.03); }
|
||
.kpi-card.status-green { border-top-color: var(--color-green); }
|
||
.kpi-card.status-yellow { border-top-color: var(--color-yellow); }
|
||
.kpi-card.status-red { border-top-color: var(--color-red); }
|
||
.kpi-card.status-orange { border-top-color: var(--color-orange); }
|
||
.kpi-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; }
|
||
.kpi-icon { font-size: 20px; }
|
||
.kpi-status-badge { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 20px; white-space: nowrap; }
|
||
.status-green .kpi-status-badge { background: var(--color-green-bg); color: var(--color-green); }
|
||
.status-yellow .kpi-status-badge { background: var(--color-yellow-bg); color: var(--color-yellow); }
|
||
.status-red .kpi-status-badge { background: var(--color-red-bg); color: var(--color-red); }
|
||
.status-orange .kpi-status-badge { background: var(--color-orange-bg); color: var(--color-orange); }
|
||
.kpi-card-value { font-size: 28px; font-weight: 700; font-family: var(--font-mono); line-height: 1; margin-bottom: 4px; }
|
||
.status-green .kpi-card-value { color: var(--color-green); }
|
||
.status-yellow .kpi-card-value { color: var(--color-yellow); }
|
||
.status-red .kpi-card-value { color: var(--color-red); }
|
||
.status-orange .kpi-card-value { color: var(--color-orange); }
|
||
.kpi-card-label { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 8px; }
|
||
.kpi-card-meta { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--color-text-secondary); margin-bottom: 4px; }
|
||
.kpi-delta { font-weight: 600; }
|
||
.kpi-delta.positive { color: var(--color-green); }
|
||
.kpi-delta.negative { color: var(--color-red); }
|
||
.kpi-risk-row { font-size: 10px; color: var(--color-orange); font-weight: 500; margin-bottom: 6px; min-height: 13px; }
|
||
|
||
/* ── TOOLBAR ── */
|
||
.toolbar { padding: 14px 24px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.toolbar-label { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); margin-right: 4px; }
|
||
.month-btn { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-text-secondary); font-family: var(--font-base); transition: all 0.15s; }
|
||
.month-btn:hover { border-color: var(--color-brand); color: var(--color-brand); }
|
||
.month-btn.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
|
||
|
||
/* ── LAYOUT ── */
|
||
.main-layout { display: grid; grid-template-columns: 1fr 360px; gap: 24px; padding: 0 24px 32px; align-items: start; }
|
||
.charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||
.charts-grid .chart-section:nth-child(5) { grid-column: 1 / -1; }
|
||
.chart-section { background: var(--color-surface); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-card); }
|
||
.chart-section-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
|
||
.chart-section-title { font-size: 14px; font-weight: 700; }
|
||
.chart-section-subtitle { font-size: 11px; color: var(--color-text-secondary); margin-top: 2px; }
|
||
.chart-tabs { display: flex; gap: 4px; }
|
||
.chart-tab { padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid var(--color-border); background: transparent; color: var(--color-text-secondary); font-family: var(--font-base); transition: all 0.15s; }
|
||
.chart-tab.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
|
||
.chart-canvas-wrap { position: relative; height: 220px; }
|
||
|
||
/* Risk banners */
|
||
.chart-risk-banner { margin-top: 10px; padding: 7px 12px; border-radius: 8px; font-size: 11px; font-weight: 500; background: var(--color-orange-bg); color: var(--color-orange); border: 1px solid rgba(249,115,22,0.2); }
|
||
.chart-risk-banner.hidden { display: none; }
|
||
|
||
/* FD progress */
|
||
.progress-bar-wrap { margin-top: 16px; }
|
||
.progress-bar-labels { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 6px; }
|
||
.progress-bar-labels b { font-family: var(--font-mono); }
|
||
.progress-track { height: 10px; background: var(--color-bg); border-radius: 10px; overflow: hidden; }
|
||
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--color-red) 0%, var(--color-yellow) 50%, var(--color-green) 100%); border-radius: 10px; transition: width 0.6s ease; min-width: 2px; }
|
||
.annotation-badge { margin-top: 10px; padding: 8px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; background: var(--color-red-bg); color: var(--color-red); display: flex; align-items: center; gap: 6px; }
|
||
.annotation-badge.hidden { display: none; }
|
||
|
||
/* FD trend info */
|
||
.fd-trend-info { margin-top: 10px; padding: 8px 12px; border-radius: 8px; font-size: 12px; background: var(--color-brand-light); color: var(--color-brand); border: 1px solid rgba(0,82,204,0.15); line-height: 1.6; }
|
||
.fd-trend-info.hidden { display: none; }
|
||
|
||
/* ── AI PANEL ── */
|
||
.ai-panel { background: var(--color-surface); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-card); position: sticky; top: 76px; }
|
||
.ai-panel-title { font-size: 15px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
||
.ai-api-row { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||
.api-key-input { width: 100%; padding: 9px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-btn); font-size: 13px; font-family: var(--font-base); color: var(--color-text-primary); outline: none; transition: border-color 0.15s; }
|
||
.api-key-input:focus { border-color: var(--color-brand); }
|
||
.api-key-input:disabled { background: var(--color-bg); }
|
||
.btn-primary { width: 100%; padding: 10px; 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; }
|
||
.btn-primary:disabled { background: #93B4DD; cursor: not-allowed; }
|
||
.ai-loading { display: flex; align-items: center; gap: 10px; color: var(--color-text-secondary); font-size: 13px; padding: 12px 0; }
|
||
.ai-loading.hidden { display: none; }
|
||
.spinner { width: 18px; height: 18px; border: 2px solid var(--color-border); border-top-color: var(--color-brand); border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.ai-result.hidden { display: none; }
|
||
#ai-text {
|
||
font-size: 13px; line-height: 1.65;
|
||
max-height: 460px; overflow-y: auto;
|
||
padding: 14px 16px; background: var(--color-bg);
|
||
border-radius: 8px; margin-bottom: 10px;
|
||
}
|
||
/* Markdown typography inside AI response */
|
||
#ai-text h1,#ai-text h2,#ai-text h3,#ai-text h4 {
|
||
font-weight: 700; color: var(--color-text-primary);
|
||
margin: 14px 0 6px; line-height: 1.3;
|
||
}
|
||
#ai-text h1 { font-size: 15px; border-bottom: 1px solid var(--color-border); padding-bottom: 4px; }
|
||
#ai-text h2 { font-size: 14px; }
|
||
#ai-text h3 { font-size: 13px; color: var(--color-brand); }
|
||
#ai-text h4 { font-size: 13px; color: var(--color-text-secondary); }
|
||
#ai-text p { margin: 6px 0; }
|
||
#ai-text ul,#ai-text ol { margin: 6px 0 6px 18px; }
|
||
#ai-text li { margin: 3px 0; }
|
||
#ai-text li::marker { color: var(--color-brand); }
|
||
#ai-text strong { font-weight: 700; color: var(--color-text-primary); }
|
||
#ai-text em { font-style: italic; color: var(--color-text-secondary); }
|
||
#ai-text code {
|
||
font-family: var(--font-mono); font-size: 11px;
|
||
background: rgba(0,82,204,0.08); color: var(--color-brand);
|
||
padding: 1px 5px; border-radius: 4px;
|
||
}
|
||
#ai-text blockquote {
|
||
border-left: 3px solid var(--color-brand);
|
||
margin: 8px 0; padding: 4px 12px;
|
||
color: var(--color-text-secondary); background: rgba(0,82,204,0.04);
|
||
border-radius: 0 6px 6px 0;
|
||
}
|
||
#ai-text hr { border: none; border-top: 1px solid var(--color-border); margin: 10px 0; }
|
||
#ai-text > *:first-child { margin-top: 0; }
|
||
#ai-text > *:last-child { margin-bottom: 0; }
|
||
.btn-copy { width: 100%; padding: 8px; background: var(--color-bg); color: var(--color-text-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-btn); font-size: 13px; font-weight: 500; cursor: pointer; font-family: var(--font-base); transition: all 0.15s; }
|
||
.btn-copy:hover { border-color: var(--color-brand); color: var(--color-brand); }
|
||
.ai-error { font-size: 13px; color: var(--color-red); background: var(--color-red-bg); padding: 10px 12px; border-radius: 8px; margin-top: 8px; }
|
||
.ai-error.hidden { display: none; }
|
||
.ai-divider { height: 1px; background: var(--color-border); margin: 12px 0; }
|
||
.ai-hint { font-size: 11px; color: var(--color-text-secondary); text-align: center; line-height: 1.5; }
|
||
.ai-key-note { font-size: 11px; color: var(--color-green); text-align: center; margin-bottom: 8px; font-weight: 500; }
|
||
.ai-cache-meta { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
|
||
.ai-update-details { margin-top: 4px; }
|
||
.ai-update-details[open] .ai-update-summary { color: var(--color-brand); }
|
||
.ai-update-summary {
|
||
font-size: 13px; font-weight: 600; color: var(--color-text-secondary);
|
||
cursor: pointer; list-style: none; padding: 6px 0; user-select: none;
|
||
}
|
||
.ai-update-summary::-webkit-details-marker { display: none; }
|
||
.ai-update-summary:hover { color: var(--color-brand); }
|
||
|
||
/* ── RESPONSIVE ── */
|
||
@media (max-width: 1200px) { .summary-bar { grid-template-columns: repeat(3,1fr); } }
|
||
@media (max-width: 1024px) { .main-layout { grid-template-columns: 1fr; } .ai-panel { position: static; } }
|
||
@media (max-width: 768px) { .charts-grid { grid-template-columns: 1fr; } .charts-grid .chart-section:nth-child(5) { grid-column: auto; } .summary-bar { grid-template-columns: 1fr 1fr; } .main-layout,.summary-section { padding-left: 12px; padding-right: 12px; } .toolbar { padding: 10px 12px; } }
|
||
@media (max-width: 480px) { .summary-bar { grid-template-columns: 1fr; } .header-left h1 { font-size: 14px; } }
|
||
|
||
/* ── KPI MODAL ── */
|
||
#kpi-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 999; display: none; align-items: center; justify-content: center; }
|
||
#kpi-modal-overlay.active { display: flex; }
|
||
#kpi-modal { background: var(--color-surface); border-radius: 16px; max-width: 840px; width: 94%; max-height: 92vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25); }
|
||
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 24px 24px 0; }
|
||
.modal-header h2 { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
||
.modal-close { background: var(--color-bg); color: var(--color-text-secondary); border: 1px solid var(--color-border); width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 18px; flex-shrink: 0; transition: all 0.15s; }
|
||
.modal-close:hover { background: var(--color-red-bg); color: var(--color-red); border-color: var(--color-red); }
|
||
.modal-body { padding: 20px 24px 24px; }
|
||
.modal-info { font-size: 13px; line-height: 1.7; color: var(--color-text-secondary); margin-bottom: 20px; }
|
||
.modal-info b { color: var(--color-text-primary); }
|
||
.modal-formula { background: var(--color-brand-light); border: 1px solid rgba(0,82,204,0.15); border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; font-family: var(--font-mono); font-size: 13px; }
|
||
.modal-formula .formula-val { color: var(--color-brand); font-weight: 700; }
|
||
.modal-link { display: inline-flex; align-items: center; gap: 6px; color: var(--color-brand); font-size: 12px; font-weight: 500; text-decoration: none; margin-bottom: 20px; }
|
||
.modal-link:hover { text-decoration: underline; }
|
||
.modal-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||
.modal-chart-box { background: var(--color-bg); border-radius: 10px; padding: 14px; }
|
||
.modal-chart-box h4 { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.03em; }
|
||
.modal-chart-box canvas { max-height: 210px; }
|
||
@media (max-width: 640px) { .modal-charts { grid-template-columns: 1fr; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOADING -->
|
||
<!-- LOGIN -->
|
||
<div id="login-screen">
|
||
<div class="upload-box">
|
||
<div class="upload-logo">KT</div>
|
||
<h1>KPI InDigiCo</h1>
|
||
<p>Казахтелеком · 2026</p>
|
||
<input type="password" id="login-password" class="api-key-input" placeholder="Введите пароль" style="text-align:center;font-size:16px;margin-bottom:12px;">
|
||
<button class="btn-primary" id="btn-login" style="width:auto;padding:12px 40px;">Войти</button>
|
||
<p id="login-error" style="color:var(--color-red);font-size:13px;margin-top:10px;min-height:18px;"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="loading-screen" style="display:none">
|
||
<div class="loading-box">
|
||
<div class="spinner-lg"></div>
|
||
<p>Загрузка данных KPI...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UPLOAD (резерв) -->
|
||
<div id="upload-screen">
|
||
<div class="upload-box">
|
||
<div class="upload-logo">KT</div>
|
||
<h1>KPI InDigiCo</h1>
|
||
<p id="upload-reason">Выберите CSV файл с данными KPI</p>
|
||
<div class="upload-note" id="upload-note" style="display:none"></div>
|
||
<label class="upload-btn-label" for="file-input">📂 Выбрать CSV файл</label>
|
||
<input type="file" id="file-input" accept=".csv">
|
||
<p class="upload-hint">Файл: <code>drb_iliyas_kpi_2026.csv</code> · Разделитель <code>;</code></p>
|
||
<p id="upload-error"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DASHBOARD -->
|
||
<div id="dashboard">
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<div class="header-logo">KT</div>
|
||
<div>
|
||
<h1>KPI InDigiCo</h1>
|
||
<p>Казахтелеком · 2026</p>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<span id="last-updated">—</span>
|
||
<button class="btn btn-ghost" id="btn-export-csv">↓ Скачать CSV</button>
|
||
<button class="btn btn-outline" id="btn-reload">Обновить данные</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="summary-section"><div class="summary-bar" id="summary-bar"></div></div>
|
||
<div class="toolbar" id="toolbar"><span class="toolbar-label">Период:</span></div>
|
||
|
||
<div class="main-layout">
|
||
<div class="charts-grid">
|
||
|
||
<div class="chart-section" id="section-kpi-registrations">
|
||
<div class="chart-section-header">
|
||
<div><div class="chart-section-title">👤 KPI 1 — Регистрации</div><div class="chart-section-subtitle">Доля зарег. пользователей · Цель: 60%</div></div>
|
||
</div>
|
||
<div class="chart-canvas-wrap"><canvas id="chart-registrations"></canvas></div>
|
||
<div class="chart-risk-banner hidden" id="risk-registrations"></div>
|
||
</div>
|
||
|
||
<div class="chart-section" id="section-kpi-mau">
|
||
<div class="chart-section-header">
|
||
<div><div class="chart-section-title">📱 KPI 2 — MAU</div><div class="chart-section-subtitle">Monthly Active Users · Цель: 30%</div></div>
|
||
<div class="chart-tabs">
|
||
<button class="chart-tab active" id="tab-mau-monthly" onclick="switchMauTab('monthly')">По месяцам</button>
|
||
<button class="chart-tab" id="tab-mau-daily" onclick="switchMauTab('daily')">По дням</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-canvas-wrap"><canvas id="chart-mau"></canvas></div>
|
||
<div class="chart-risk-banner hidden" id="risk-mau"></div>
|
||
</div>
|
||
|
||
<div class="chart-section" id="section-kpi-traditional">
|
||
<div class="chart-section-header">
|
||
<div><div class="chart-section-title">📉 KPI 3 — Традиционные обращения</div><div class="chart-section-subtitle">Снижение доли обращений · Цель: −10%</div></div>
|
||
</div>
|
||
<div class="chart-canvas-wrap"><canvas id="chart-traditional"></canvas></div>
|
||
<div class="chart-risk-banner hidden" id="risk-traditional"></div>
|
||
</div>
|
||
|
||
<div class="chart-section" id="section-kpi-digital-sales">
|
||
<div class="chart-section-header">
|
||
<div><div class="chart-section-title">💰 KPI 4 — Цифровые продажи</div><div class="chart-section-subtitle">Доля цифровой выручки · Цель: 45%</div></div>
|
||
</div>
|
||
<div class="chart-canvas-wrap"><canvas id="chart-digital-sales"></canvas></div>
|
||
<div class="chart-risk-banner hidden" id="risk-digital-sales"></div>
|
||
</div>
|
||
|
||
<div class="chart-section" id="section-kpi-fd-orders">
|
||
<div class="chart-section-header">
|
||
<div><div class="chart-section-title">⚡ KPI 5 — Full Digital установки</div><div class="chart-section-subtitle">FD заказы · Цель: 1 000/мес · 12 000/год</div></div>
|
||
</div>
|
||
<div class="chart-canvas-wrap"><canvas id="chart-fd-orders"></canvas></div>
|
||
<div class="progress-bar-wrap">
|
||
<div class="progress-bar-labels">
|
||
<span>Годовой прогресс: <b id="cum-fd-text">—</b></span>
|
||
<span id="cum-fd-pct" style="font-weight:600;font-family:var(--font-mono);">—</span>
|
||
</div>
|
||
<div class="progress-track"><div class="progress-fill" id="cum-fd-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="fd-trend-info hidden" id="fd-trend-info"></div>
|
||
<div class="annotation-badge hidden" id="fd-annotation">⚠️ Показатель существенно ниже плана — цель требует пересмотра</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- AI Panel -->
|
||
<div class="ai-panel">
|
||
<div class="ai-panel-title">🤖 AI-анализ KPI</div>
|
||
|
||
<!-- Cached response (shown to all users automatically) -->
|
||
<div class="ai-loading hidden" id="ai-loading"><div class="spinner"></div><span>Получаем анализ...</span></div>
|
||
<div class="ai-result hidden" id="ai-result">
|
||
<div class="ai-cache-meta">
|
||
<span id="ai-cache-date" style="font-size:11px;color:var(--color-text-secondary);">Загрузка...</span>
|
||
<span style="font-size:11px;color:var(--color-text-secondary);">· общий для всех</span>
|
||
</div>
|
||
<div id="ai-text"></div>
|
||
<button class="btn-copy" id="btn-ai-copy">📋 Скопировать</button>
|
||
</div>
|
||
<div class="ai-error hidden" id="ai-error"></div>
|
||
|
||
<!-- Update section: only useful if you have DeepSeek key -->
|
||
<div class="ai-divider"></div>
|
||
<details class="ai-update-details" id="ai-update-details">
|
||
<summary class="ai-update-summary">🔄 Обновить анализ</summary>
|
||
<div class="ai-api-row" style="margin-top:10px;">
|
||
<div id="ai-key-note" class="ai-key-note hidden">✓ DeepSeek токен загружен из config.js</div>
|
||
<input type="password" class="api-key-input" id="ai-api-key" placeholder="DeepSeek API ключ">
|
||
<button class="btn-primary" id="btn-ai-analyze">Запустить и сохранить для всех</button>
|
||
</div>
|
||
<p class="ai-hint" style="margin-top:8px;">Новый ответ сохранится в репо и сразу станет доступен всем пользователям. Обновляйте раз в месяц при новых данных.</p>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KPI DETAIL MODAL -->
|
||
<div id="kpi-modal-overlay" onclick="closeKpiModal(event)">
|
||
<div id="kpi-modal" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<h2 id="modal-title"></h2>
|
||
<button class="modal-close" onclick="closeKpiModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="modal-info" id="modal-info"></div>
|
||
<div class="modal-formula" id="modal-formula"></div>
|
||
<a class="modal-link" id="modal-link" href="#" target="_blank" rel="noopener">📊 Открыть детальный отчёт в Qlik Sense →</a>
|
||
<div class="modal-charts">
|
||
<div class="modal-chart-box"><h4 id="modal-chart1-label"></h4><canvas id="modal-chart1"></canvas></div>
|
||
<div class="modal-chart-box"><h4 id="modal-chart2-label"></h4><canvas id="modal-chart2"></canvas></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
|
||
<script>
|
||
// ═══════════════════ CONSTANTS ═══════════════════
|
||
// ─── Embedded CSV data (update this when the CSV changes) ───────────────────
|
||
// This makes the dashboard work via file:// AND on GitHub Pages without a server.
|
||
const EMBEDDED_CSV = `"report_period_id";"entry_date";"abons";"registered_total";"registered_pct";"mau_daily";"mau_per_registered";"traditional_comms_pct";"prev_yesr_traditional_comms_pct";"traditional_comms_decrease_pct";"cumulative_digital_rap_total";"cumulative_rap_total";"fd_rap_pct";"fd_orders";"fd_orders_goal";"fd_orders_pct";"cum_fd_orders";"cum_fd_orders_goal";"cum_fd_orders_pct"
|
||
202605;"2026-05-31";2101989;1068795;0.508;249700;0.234;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-30";2101989;1068493;0.508;246154;0.23;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-29";2101989;1068104;0.508;242298;0.227;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-28";2101989;1067532;0.508;237187;0.222;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-27";2101989;1066924;0.508;232155;0.218;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-26";2101989;1066631;0.507;228406;0.214;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-25";2101989;1066084;0.507;223215;0.209;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-24";2101989;1065569;0.507;217678;0.204;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-23";2101989;1065297;0.507;210932;0.198;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-22";2101989;1064996;0.507;207178;0.195;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-21";2101989;1064508;0.506;202306;0.19;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-20";2101989;1064000;0.506;196569;0.185;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-19";2101989;1063493;0.506;190808;0.179;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-18";2101989;1063028;0.506;185572;0.175;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-17";2101989;1062578;0.506;179812;0.169;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-16";2101989;1062398;0.505;176238;0.166;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-15";2101989;1062131;0.505;172113;0.162;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-14";2101989;1061692;0.505;165824;0.156;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-13";2101989;1061183;0.505;159101;0.15;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-12";2101989;1060626;0.505;151282;0.143;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-11";2101989;1060034;0.504;141221;0.133;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-10";2101989;1059741;0.504;134326;0.127;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-09";2101989;1059532;0.504;108714;0.103;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-08";2101989;1059344;0.504;103441;0.098;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-07";2101989;1058907;0.504;94130;0.089;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-06";2101989;1058693;0.504;87564;0.083;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-05";2101989;1058239;0.503;77931;0.074;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-04";2101989;1057748;0.503;61363;0.058;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-03";2101989;1057207;0.503;48135;0.046;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-02";2101989;1057009;0.503;38686;0.037;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202605;"2026-05-01";2101989;1056756;0.503;25919;0.025;0.5366890026123278;0.6678290454812975;0.1963676838500764;2175065186.3946314;4051850450.3362856;0.5368078642226567;180;1000;0.18;1359;5000;0.2718
|
||
202604;"2026-04-30";2106236;1056399;0.502;262808;0.249;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-29";2106236;1055833;0.501;258016;0.244;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-28";2106236;1055340;0.501;253771;0.24;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-27";2106236;1054774;0.501;249130;0.236;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-26";2106236;1054128;0.5;243625;0.231;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-25";2106236;1053911;0.5;240525;0.228;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-24";2106236;1053569;0.5;236698;0.225;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-23";2106236;1053042;0.5;229540;0.218;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-22";2106236;1052586;0.5;224061;0.213;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-21";2106236;1052162;0.5;218547;0.208;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-20";2106236;1051719;0.499;213277;0.203;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-19";2106236;1051232;0.499;207340;0.197;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-18";2106236;1051035;0.499;203917;0.194;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-17";2106236;1050776;0.499;199957;0.19;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-16";2106236;1050388;0.499;194325;0.185;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-15";2106236;1049972;0.499;189274;0.18;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-14";2106236;1049551;0.498;182926;0.174;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-13";2106236;1049115;0.498;176555;0.168;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-12";2106236;1048669;0.498;170102;0.162;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-11";2106236;1048506;0.498;165743;0.158;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-10";2106236;1048275;0.498;159771;0.152;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-09";2106236;1047857;0.498;135958;0.13;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-08";2106236;1047422;0.497;128075;0.122;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-07";2106236;1046968;0.497;119000;0.114;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-06";2106236;1046511;0.497;109350;0.104;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-05";2106236;1046023;0.497;100225;0.096;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-04";2106236;1045862;0.497;90067;0.086;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-03";2106236;1045604;0.496;79001;0.076;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-02";2106236;1045104;0.496;63359;0.061;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-01";2106236;1044565;0.496;42565;0.041;0.5338365169324176;0.6673869796669217;0.20010948190981526;1710926678.0289855;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202603;"2026-03-31";2111350;1043888;0.494;274427;0.263;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-30";2111350;1043214;0.494;269087;0.258;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-29";2111350;1042636;0.494;263532;0.253;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-28";2111350;1042415;0.494;259873;0.249;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-27";2111350;1042108;0.494;255833;0.245;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-26";2111350;1041554;0.493;250310;0.24;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-25";2111350;1040942;0.493;243951;0.234;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-24";2111350;1040609;0.493;239211;0.23;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-23";2111350;1040287;0.493;230641;0.222;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-22";2111350;1040068;0.493;227059;0.218;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-21";2111350;1039913;0.493;224197;0.216;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-20";2111350;1039702;0.492;220503;0.212;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-19";2111350;1039290;0.492;214096;0.206;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-18";2111350;1038815;0.492;207714;0.2;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-17";2111350;1038395;0.492;202057;0.195;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-16";2111350;1037939;0.492;195826;0.189;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-15";2111350;1037451;0.491;190501;0.184;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-14";2111350;1037272;0.491;186615;0.18;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-13";2111350;1036969;0.491;181952;0.175;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-12";2111350;1036447;0.491;175644;0.169;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-11";2111350;1035955;0.491;167925;0.162;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-10";2111350;1035390;0.49;152437;0.147;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-09";2111350;1034771;0.49;132245;0.128;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-08";2111350;1034511;0.49;124363;0.12;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-07";2111350;1034336;0.49;118815;0.115;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-06";2111350;1034044;0.49;107895;0.104;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-05";2111350;1033640;0.49;98398;0.095;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-04";2111350;1033166;0.489;81465;0.079;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-03";2111350;1032627;0.489;67294;0.065;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-02";2111350;1032038;0.489;53625;0.052;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-01";2111350;1031337;0.488;28679;0.028;0.5286574982050782;0.6650572246826678;0.2050947218003274;1277504399.5958557;1976256535.1164813;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202602;"2026-02-28";2115881;1030969;0.487;255217;0.248;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-27";2115881;1030503;0.487;251011;0.244;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-26";2115881;1029917;0.487;245838;0.239;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-25";2115881;1029314;0.486;240445;0.234;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-24";2115881;1028705;0.486;234523;0.228;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-23";2115881;1028056;0.486;223949;0.218;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-22";2115881;1027538;0.486;219211;0.213;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-21";2115881;1027337;0.486;215490;0.21;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-20";2115881;1026963;0.485;211533;0.206;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-19";2115881;1026457;0.485;206694;0.201;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-18";2115881;1026004;0.485;201406;0.196;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-17";2115881;1025488;0.485;195267;0.19;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-16";2115881;1025061;0.484;189110;0.184;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-15";2115881;1024577;0.484;184065;0.18;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-14";2115881;1024357;0.484;180002;0.176;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-13";2115881;1024081;0.484;175668;0.172;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-12";2115881;1023528;0.484;169515;0.166;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-11";2115881;1022916;0.483;162331;0.159;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-10";2115881;1022265;0.483;144649;0.141;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-09";2115881;1021630;0.483;120035;0.117;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-08";2115881;1021035;0.483;104886;0.103;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-07";2115881;1020800;0.482;98656;0.097;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-06";2115881;1020457;0.482;91233;0.089;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-05";2115881;1019955;0.482;81744;0.08;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-04";2115881;1019386;0.482;71505;0.07;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-03";2115881;1018799;0.482;60578;0.059;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-02";2115881;1018100;0.481;46960;0.046;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-01";2115881;1017353;0.481;28194;0.028;0.5281336608448282;0.6560073927988388;0.19492727270715748;823396217.7151799;1139791017.795312;0.7224098144832447;236;1000;0.236;610;2000;0.305
|
||
202601;"2026-01-31";2119709;1016924;0.48;258214;0.254;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-30";2119709;1016432;0.48;255158;0.251;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-29";2119709;1015746;0.479;251275;0.247;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-28";2119709;1015140;0.479;246374;0.243;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-27";2119709;1014520;0.479;242074;0.239;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-26";2119709;1013886;0.478;237592;0.234;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-25";2119709;1013248;0.478;232841;0.23;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-24";2119709;1012924;0.478;229330;0.226;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-23";2119709;1012500;0.478;222602;0.22;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-22";2119709;1011910;0.477;217013;0.214;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-21";2119709;1011318;0.477;211912;0.21;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-20";2119709;1010784;0.477;207216;0.205;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-19";2119709;1010244;0.477;202745;0.201;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-18";2119709;1009671;0.476;197515;0.196;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-17";2119709;1009441;0.476;194030;0.192;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-16";2119709;1009127;0.476;189569;0.188;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-15";2119709;1008640;0.476;184628;0.183;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-14";2119709;1008138;0.476;177018;0.176;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-13";2119709;1007587;0.475;171165;0.17;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-12";2119709;1007015;0.475;164491;0.163;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-11";2119709;1006355;0.475;156069;0.155;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-10";2119709;1006055;0.475;144880;0.144;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-09";2119709;1005603;0.474;125643;0.125;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-08";2119709;1004940;0.474;117031;0.116;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-07";2119709;1004268;0.474;107943;0.107;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-06";2119709;1003933;0.474;101001;0.101;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-05";2119709;1003255;0.473;89822;0.09;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-04";2119709;1002458;0.473;77284;0.077;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-03";2119709;1002142;0.473;68406;0.068;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-02";2119709;1001753;0.473;57714;0.058;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-01";2119709;1001226;0.472;40377;0.04;0.5051958171773308;0.6601094531400574;0.23467871157703024;415145489.39247936;492169296.8361089;0.8435013968998593;374;1000;0.374;374;1000;0.374`;
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
const MONTH_NAMES = ['','Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
|
||
const CSV_FILENAME = 'drb_iliyas_kpi_2026.csv';
|
||
const GITEA_API = 'https://git.vibe42.kz/api/v1';
|
||
const GITEA_OWNER = 'kyrykbaev';
|
||
const GITEA_REPO = 'kpi-dashboard';
|
||
const AI_CACHE_FILE = 'ai-cache.json';
|
||
const AI_CACHE_RAW = AI_CACHE_FILE;
|
||
|
||
const AppState = {
|
||
rawRows: [], snapshots: [], dailySeries: {},
|
||
selectedPeriod: null, rawCsvText: '', lastAiResponse: '', mauTab: 'monthly',
|
||
};
|
||
const charts = {};
|
||
|
||
Chart.defaults.font.family = 'Inter, sans-serif';
|
||
Chart.defaults.font.size = 12;
|
||
Chart.defaults.color = '#6B7280';
|
||
Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(17,24,39,0.92)';
|
||
Chart.defaults.plugins.tooltip.titleColor = '#F9FAFB';
|
||
Chart.defaults.plugins.tooltip.bodyColor = '#D1D5DB';
|
||
Chart.defaults.plugins.tooltip.padding = 10;
|
||
Chart.defaults.plugins.tooltip.cornerRadius = 8;
|
||
Chart.defaults.plugins.tooltip.borderColor = 'rgba(255,255,255,0.1)';
|
||
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
||
|
||
const KPI_CONFIG = [
|
||
{ id:'registrations', name:'Регистрации', icon:'👤', field:'registered_pct', target:0.60, targetLabel:'60%' },
|
||
{ id:'mau', name:'MAU', icon:'📱', field:'mau_per_registered', target:0.30, targetLabel:'30%' },
|
||
{ id:'traditional', name:'Снижение обращений', icon:'📉', field:'traditional_comms_decrease_pct',target:0.10, targetLabel:'10%' },
|
||
{ id:'digital-sales', name:'Цифровые продажи', icon:'💰', field:'fd_rap_pct', target:0.45, targetLabel:'45%' },
|
||
{ id:'fd-orders', name:'FD установки', icon:'⚡', field:'cum_fd_orders_pct', target:1.00, targetLabel:'12 000 шт.'},
|
||
];
|
||
|
||
const KPI_DETAIL = {
|
||
registrations: {
|
||
formula: 'Доля регистраций = 100% × (Зарегистрировано / Всего абонентов B2C)',
|
||
description: 'Используется вся активная абонентская база B2C компании Казахтелеком и полное количество зарегистрированных абонентов в системе <b>telecom.kz</b> за всё время.',
|
||
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
|
||
charts: [{field:'abons', label:'Абоненты B2C'}, {field:'registered_total', label:'Зарегистрировано'}],
|
||
},
|
||
mau: {
|
||
formula: 'MAU / Зарегистрированные = 100% × (MAU за месяц / Всего зарегистрированных)',
|
||
description: 'Используется полное количество зарегистрированных абонентов в системе <b>telecom.kz</b> и общее количество уникальных пользователей приложения <b>TelecomKz</b> за месяц.',
|
||
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
|
||
charts: [{field:'registered_total', label:'Зарегистрировано'}, {field:'mau_max', label:'MAU (макс. за месяц)'}],
|
||
},
|
||
traditional: {
|
||
formula: 'Снижение = (Доля трад. прошлого года − Доля трад. этого года) / Доля трад. прошлого года × 100%',
|
||
description: 'Берётся доля традиционных обращений текущего года и доля традиционных обращений прошлого года. Количество заказов по традиционным каналам — из отчётов <b>Qlik Sense</b>, предоставленных отделом Customer Service.',
|
||
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
|
||
charts: [{field:'traditional_comms_pct', label:'Традиционные обращения 2026 (%)', isPct:true}, {field:'prev_yesr_traditional_comms_pct', label:'Традиционные обращения 2025 (%)', isPct:true}],
|
||
},
|
||
'digital-sales': {
|
||
formula: 'Доля цифровых продаж = 100% × (Цифровая выручка / Общая выручка)',
|
||
description: 'Берутся доходы по всем каналам продаж компании и вычисляется доля цифровых каналов. Детальная разбивка по каждому каналу — в отчёте Qlik Sense.',
|
||
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/203416cd-f1e2-44dd-8b73-7afeb0f8037e/sheet/ajCCVt/state/analysis',
|
||
charts: [{field:'cumulative_digital_rap_total', label:'Цифровая выручка', isMoney:true}, {field:'cumulative_rap_total', label:'Общая выручка', isMoney:true}],
|
||
},
|
||
'fd-orders': {
|
||
formula: 'Годовой план FD = Накоплено заказов / Годовая цель (12 000) × 100%',
|
||
description: 'Учитываются успешно закрытые заказы CRM по core-услугам из канала продаж <b>«Мобильное приложение B2C»</b>. Учитывается накопительный итог с начала года.',
|
||
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/203416cd-f1e2-44dd-8b73-7afeb0f8037e/sheet/XZANwR/state/analysis',
|
||
charts: [{field:'fd_orders', label:'FD заказы (мес.)'}, {field:'cum_fd_orders', label:'FD заказы (накоп.)'}],
|
||
},
|
||
};
|
||
|
||
// ═══════════════════ FORMATTING ═══════════════════
|
||
const fmtPct = (v, d=1) => (v*100).toFixed(d)+'%';
|
||
const fmtInt = n => new Intl.NumberFormat('ru-RU').format(Math.round(n));
|
||
const fmtMoney = n => new Intl.NumberFormat('ru-KZ',{style:'currency',currency:'KZT',maximumFractionDigits:0}).format(n);
|
||
const periodLabel = pid => { const s=String(pid); return MONTH_NAMES[parseInt(s.slice(4),10)]+' '+s.slice(0,4); };
|
||
|
||
// ═══════════════════ MATH / REGRESSION ═══════════════════
|
||
function linearRegression(xs, ys) {
|
||
const n = xs.length;
|
||
const sx=xs.reduce((a,b)=>a+b,0), sy=ys.reduce((a,b)=>a+b,0);
|
||
const sxy=xs.reduce((s,x,i)=>s+x*ys[i],0), sxx=xs.reduce((s,x)=>s+x*x,0);
|
||
const den = n*sxx-sx*sx;
|
||
if (den===0) return {slope:0, intercept:sy/n};
|
||
const slope=(n*sxy-sx*sy)/den;
|
||
return {slope, intercept:(sy-slope*sx)/n};
|
||
}
|
||
|
||
/**
|
||
* Exponential regression: fits y = a * e^(bx)
|
||
* Works only when all y > 0. Returns null otherwise.
|
||
*/
|
||
function exponentialRegression(xs, ys) {
|
||
if (ys.some(y=>y<=0)) return null;
|
||
const logYs = ys.map(y=>Math.log(y));
|
||
const {slope:b, intercept:lna} = linearRegression(xs, logYs);
|
||
return {a: Math.exp(lna), b};
|
||
}
|
||
|
||
/** R² — coefficient of determination */
|
||
function rSquared(ys, predictedYs) {
|
||
const mean = ys.reduce((s,v)=>s+v,0)/ys.length;
|
||
const ssTot = ys.reduce((s,y)=>s+(y-mean)**2,0);
|
||
if (ssTot===0) return 1;
|
||
const ssRes = ys.reduce((s,y,i)=>s+(y-predictedYs[i])**2,0);
|
||
return 1-ssRes/ssTot;
|
||
}
|
||
|
||
/**
|
||
* Smart forecast: automatically picks linear vs exponential based on R².
|
||
* Returns { values: number[], modelLabel: string }
|
||
* where values is an array of length `totalMonths`.
|
||
*/
|
||
function smartForecast(snapshots, field, totalMonths=12) {
|
||
if (snapshots.length < 2) return {values: Array(totalMonths).fill(null), modelLabel:'недостаточно данных'};
|
||
|
||
const xs = snapshots.map((_,i)=>i);
|
||
const ys = snapshots.map(s=>s[field]);
|
||
|
||
// Linear model
|
||
const lin = linearRegression(xs, ys);
|
||
const linPred = xs.map(x=>lin.intercept+lin.slope*x);
|
||
const linR2 = rSquared(ys, linPred);
|
||
|
||
// Exponential model
|
||
const exp = exponentialRegression(xs, ys);
|
||
let expR2 = -Infinity;
|
||
if (exp) {
|
||
const expPred = xs.map(x=>exp.a*Math.exp(exp.b*x));
|
||
expR2 = rSquared(ys, expPred);
|
||
}
|
||
|
||
// Use exponential only if it meaningfully outperforms linear (threshold: 3pp)
|
||
// Use exponential when:
|
||
// a) it meaningfully outperforms linear (ΔR² > 0.01), OR
|
||
// b) linear gives a physically impossible (negative) December forecast
|
||
const linDecForecast = lin.intercept + lin.slope * (totalMonths - 1);
|
||
const linIsUnphysical = linDecForecast < 0;
|
||
const useExp = exp && (expR2 > linR2 + 0.01 || (linIsUnphysical && expR2 >= linR2 - 0.05));
|
||
|
||
const values = Array.from({length:totalMonths}, (_,m)=>{
|
||
if (useExp) return exp.a * Math.exp(exp.b*m);
|
||
return lin.intercept + lin.slope*m;
|
||
});
|
||
|
||
const modelLabel = useExp
|
||
? `экспоненциальная модель (R²=${expR2.toFixed(2)})`
|
||
: `линейная модель (R²=${linR2.toFixed(2)})`;
|
||
|
||
return {values, modelLabel, useExp, linR2, expR2: expR2===-Infinity?null:expR2};
|
||
}
|
||
|
||
// Build 12-month forecast array for a KPI (used by multiple places)
|
||
function buildSmartForecast(field) {
|
||
return smartForecast(AppState.snapshots, field, 12);
|
||
}
|
||
|
||
// ═══════════════════ STATUS WITH RISK ═══════════════════
|
||
function getKpiStatusWithRisk(value, target, field) {
|
||
const {values: fc, modelLabel} = smartForecast(AppState.snapshots, field, 12);
|
||
const dec = fc[11]; // December projection
|
||
|
||
const aboveTarget = value >= target;
|
||
const nearTarget = value >= target * 0.85;
|
||
|
||
let riskText = '';
|
||
|
||
if (aboveTarget) {
|
||
if (dec !== null && dec < target) {
|
||
riskText = `⚠ Прогноз к дек.: ${fmtPct(dec)} — риск падения ниже цели`;
|
||
return {status:'orange', riskText, modelLabel, decForecast: dec};
|
||
}
|
||
return {status:'green', riskText:'', modelLabel, decForecast: dec};
|
||
}
|
||
if (nearTarget) {
|
||
if (dec !== null) riskText = `⚠ Прогноз к дек.: ${fmtPct(dec)}`;
|
||
return {status:'yellow', riskText, modelLabel, decForecast: dec};
|
||
}
|
||
if (dec !== null) riskText = `Прогноз к дек.: ${fmtPct(dec)}`;
|
||
return {status:'red', riskText, modelLabel, decForecast: dec};
|
||
}
|
||
|
||
const statusLabel = s => ({green:'✅ Выполнено',orange:'⚠️ Риск снижения',yellow:'⚠️ Близко к цели',red:'🔴 Отставание'}[s]);
|
||
const getDelta = (v,t) => { const d=(v-t)*100; return (d>=0?'+':'')+d.toFixed(1)+' п.п.'; };
|
||
|
||
/**
|
||
* Compute a "nice" Y-axis range from a set of values.
|
||
* @param {number[]} values - all data points (nulls are ignored)
|
||
* @param {object} opts
|
||
* padFraction - fraction of range added as padding on each side (default 0.15)
|
||
* forceMin - hard floor (e.g. 0 for counts)
|
||
* forceMax - hard ceiling (e.g. 100 for %)
|
||
*/
|
||
function axisRange(values, {padFraction=0.15, forceMin=null, forceMax=null}={}) {
|
||
const valid = values.flat().filter(v => v !== null && v !== undefined && isFinite(v));
|
||
if (!valid.length) return {min:0, max:100};
|
||
let lo = Math.min(...valid);
|
||
let hi = Math.max(...valid);
|
||
if (lo === hi) { lo -= 5; hi += 5; }
|
||
const range = hi - lo;
|
||
lo -= range * padFraction;
|
||
hi += range * padFraction;
|
||
if (forceMin !== null) lo = Math.max(lo, forceMin);
|
||
if (forceMax !== null) hi = Math.min(hi, forceMax);
|
||
// Snap to a "nice" step so ticks land on round numbers
|
||
const roughStep = (hi - lo) / 5;
|
||
const mag = Math.pow(10, Math.floor(Math.log10(roughStep || 1)));
|
||
const norm = roughStep / mag;
|
||
const step = norm <= 1 ? mag : norm <= 2 ? 2*mag : norm <= 5 ? 5*mag : 10*mag;
|
||
return {
|
||
min: Math.floor(lo / step) * step,
|
||
max: Math.ceil(hi / step) * step,
|
||
};
|
||
}
|
||
|
||
// ═══════════════════ CSV PARSING ═══════════════════
|
||
function parseCSV(text) {
|
||
const lines = text.trim().split(/\r?\n/);
|
||
let sep = ';';
|
||
if (!lines[0].includes(';') && lines[0].includes(',')) sep = ',';
|
||
const headers = lines[0].split(sep).map(h=>h.replace(/"/g,'').trim());
|
||
return lines.slice(1).filter(l=>l.trim()).map(line=>{
|
||
const values=line.split(sep);
|
||
const row={};
|
||
headers.forEach((h,i)=>{ const v=(values[i]||'').replace(/"/g,'').trim(); row[h]=(v!==''&&!isNaN(v))?Number(v):v; });
|
||
return row;
|
||
});
|
||
}
|
||
function buildMonthlySnapshots(rows) {
|
||
const months={};
|
||
rows.forEach(r=>{ const p=r.report_period_id; if(!months[p]||r.entry_date>months[p].entry_date)months[p]=r; });
|
||
return Object.values(months).sort((a,b)=>a.report_period_id-b.report_period_id);
|
||
}
|
||
function buildDailySeries(rows) {
|
||
const s={};
|
||
rows.forEach(r=>{ const p=r.report_period_id; if(!s[p])s[p]=[]; s[p].push(r); });
|
||
Object.keys(s).forEach(p=>s[p].sort((a,b)=>a.entry_date.localeCompare(b.entry_date)));
|
||
return s;
|
||
}
|
||
function initWithData(text) {
|
||
const rows=parseCSV(text);
|
||
if(!rows.length||!rows[0].report_period_id) throw new Error('Неверный формат CSV');
|
||
AppState.rawCsvText=text; AppState.rawRows=rows;
|
||
AppState.snapshots=buildMonthlySnapshots(rows);
|
||
AppState.dailySeries=buildDailySeries(rows);
|
||
AppState.selectedPeriod=null; AppState.mauTab='monthly';
|
||
}
|
||
|
||
// ═══════════════════ AUTO-LOAD ═══════════════════
|
||
async function tryAutoLoad() {
|
||
// 1. On HTTP (GitHub Pages, local server): try to fetch the latest CSV first
|
||
if (window.location.protocol !== 'file:') {
|
||
try {
|
||
const resp = await fetch(CSV_FILENAME, {cache:'no-cache'});
|
||
if (resp.ok) {
|
||
initWithData(await resp.text());
|
||
showDashboard();
|
||
return;
|
||
}
|
||
} catch { /* fall through to embedded */ }
|
||
}
|
||
|
||
// 2. Use the embedded CSV (works on file://, GitHub Pages when CSV is missing, anywhere)
|
||
if (EMBEDDED_CSV && EMBEDDED_CSV.trim().length > 100) {
|
||
try {
|
||
initWithData(EMBEDDED_CSV);
|
||
showDashboard();
|
||
return;
|
||
} catch { /* fall through to upload screen */ }
|
||
}
|
||
|
||
// 3. Nothing worked — show manual upload
|
||
showUploadScreen(
|
||
'Данные не найдены',
|
||
'Загрузите файл drb_iliyas_kpi_2026.csv вручную.'
|
||
);
|
||
}
|
||
|
||
function showUploadScreen(reason, note) {
|
||
document.getElementById('loading-screen').style.display = 'none';
|
||
document.getElementById('upload-screen').style.display = 'flex';
|
||
if (reason) document.getElementById('upload-reason').textContent = reason;
|
||
if (note) {
|
||
const noteEl = document.getElementById('upload-note');
|
||
noteEl.textContent = note;
|
||
noteEl.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// ═══════════════════ RENDER ═══════════════════
|
||
function showDashboard() {
|
||
document.getElementById('loading-screen').style.display = 'none';
|
||
document.getElementById('upload-screen').style.display = 'none';
|
||
document.getElementById('dashboard').style.display = 'block';
|
||
renderHeader(); renderSummaryBar(); renderToolbar();
|
||
destroyAllCharts(); renderAllCharts();
|
||
loadAiCache(); initAiKey();
|
||
}
|
||
|
||
function renderHeader() {
|
||
const last=AppState.snapshots[AppState.snapshots.length-1];
|
||
document.getElementById('last-updated').textContent='Данные по: '+periodLabel(last.report_period_id);
|
||
}
|
||
|
||
function renderSummaryBar() {
|
||
const bar=document.getElementById('summary-bar');
|
||
const last=AppState.snapshots[AppState.snapshots.length-1];
|
||
bar.innerHTML='';
|
||
KPI_CONFIG.forEach(kpi=>{
|
||
const val=last[kpi.field];
|
||
const {status,riskText}=getKpiStatusWithRisk(val,kpi.target,kpi.field);
|
||
const delta=getDelta(val,kpi.target);
|
||
const card=document.createElement('div');
|
||
card.className=`kpi-card status-${status}`;
|
||
card.innerHTML=`
|
||
<div class="kpi-card-top"><span class="kpi-icon">${kpi.icon}</span><span class="kpi-status-badge">${statusLabel(status)}</span></div>
|
||
<div class="kpi-card-value">${fmtPct(val)}</div>
|
||
<div class="kpi-card-label">${kpi.name}</div>
|
||
<div class="kpi-card-meta"><span>Цель: ${kpi.targetLabel}</span><span class="kpi-delta ${val>=kpi.target?'positive':'negative'}">${delta}</span></div>
|
||
<div class="kpi-risk-row">${riskText}</div>
|
||
<canvas class="sparkline" id="spark-${kpi.id}" width="100" height="28"></canvas>`;
|
||
card.addEventListener('click',()=>openKpiModal(kpi.id));
|
||
bar.appendChild(card);
|
||
|
||
const sparkColor={green:'#10B981',orange:'#F97316',yellow:'#F59E0B',red:'#EF4444'}[status];
|
||
new Chart(document.getElementById(`spark-${kpi.id}`).getContext('2d'),{
|
||
type:'line',
|
||
data:{labels:AppState.snapshots.map(s=>s.report_period_id),datasets:[{data:AppState.snapshots.map(s=>+(s[kpi.field]*100).toFixed(2)),borderColor:sparkColor,borderWidth:2,pointRadius:0,tension:0.4}]},
|
||
options:{animation:false,plugins:{legend:{display:false},tooltip:{enabled:false}},scales:{x:{display:false},y:{display:false}},responsive:false}
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderToolbar() {
|
||
const toolbar=document.getElementById('toolbar');
|
||
toolbar.innerHTML='<span class="toolbar-label">Период:</span>';
|
||
const all=document.createElement('button');
|
||
all.className='month-btn active'; all.textContent='Все месяцы'; all.dataset.period='all';
|
||
all.addEventListener('click',()=>applyFilter('all'));
|
||
toolbar.appendChild(all);
|
||
AppState.snapshots.forEach(s=>{
|
||
const b=document.createElement('button');
|
||
b.className='month-btn'; b.textContent=periodLabel(s.report_period_id); b.dataset.period=String(s.report_period_id);
|
||
b.addEventListener('click',()=>applyFilter(String(s.report_period_id)));
|
||
toolbar.appendChild(b);
|
||
});
|
||
}
|
||
|
||
function applyFilter(period) {
|
||
AppState.selectedPeriod=period==='all'?null:parseInt(period);
|
||
document.querySelectorAll('.month-btn').forEach(b=>b.classList.toggle('active',b.dataset.period===period));
|
||
destroyAllCharts(); renderAllCharts();
|
||
}
|
||
|
||
// ═══════════════════ CHART HELPERS ═══════════════════
|
||
function destroyAllCharts() {
|
||
Object.keys(charts).forEach(k=>{if(charts[k]){charts[k].destroy();delete charts[k];}});
|
||
}
|
||
function getSnapshots() {
|
||
return AppState.selectedPeriod
|
||
? AppState.snapshots.filter(s=>s.report_period_id<=AppState.selectedPeriod)
|
||
: AppState.snapshots;
|
||
}
|
||
function allMonthLabels() {
|
||
const yr=String(AppState.snapshots[0].report_period_id).slice(0,4);
|
||
return MONTH_NAMES.slice(1).map(m=>m+' '+yr);
|
||
}
|
||
|
||
/** Build forecast dataset for a chart using smart model */
|
||
function makeForecastDataset(field, color, yAxisID='y') {
|
||
const {values:fc} = smartForecast(AppState.snapshots, field, 12);
|
||
const n = AppState.snapshots.length;
|
||
const data = Array(12).fill(null);
|
||
for(let i=n;i<12;i++) data[i] = fc[i]!==null ? +(fc[i]*100).toFixed(2) : null;
|
||
if(n<12 && fc[n-1]!==null) data[n-1] = +(fc[n-1]*100).toFixed(2); // connect to last actual
|
||
return {label:'Прогноз', data, type:'line', borderColor:color||'rgba(99,102,241,0.55)', borderDash:[5,4], pointRadius:0, borderWidth:2, yAxisID, tension:0.3};
|
||
}
|
||
|
||
/** Show risk banner below chart; returns the December forecast value */
|
||
function showRiskBanner(id, field, target) {
|
||
const el = document.getElementById(`risk-${id}`);
|
||
if (!el) return;
|
||
const {values:fc, modelLabel} = smartForecast(AppState.snapshots, field, 12);
|
||
const cur = AppState.snapshots[AppState.snapshots.length-1][field];
|
||
const dec = fc[11];
|
||
if (dec===null) { el.classList.add('hidden'); return; }
|
||
|
||
if (dec < target) {
|
||
const curAbove = cur >= target;
|
||
el.innerHTML = `${curAbove?'Сейчас выше цели, но при':'При'} текущем тренде прогноз на декабрь: <b>${fmtPct(dec)}</b> `+
|
||
`(цель ${fmtPct(target)}) — риск недовыполнения KPI по итогам года. <i>Модель: ${modelLabel}.</i>`;
|
||
el.classList.remove('hidden');
|
||
} else {
|
||
el.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// ═══════════════════ KPI 1 — REGISTRATIONS ═══════════════════
|
||
function renderChartRegistrations() {
|
||
const snaps=getSnapshots(), labels=allMonthLabels();
|
||
const actual=Array(12).fill(null), reg2=Array(12).fill(null);
|
||
snaps.forEach((s,i)=>{ actual[i]=+(s.registered_pct*100).toFixed(1); reg2[i]=s.registered_total; });
|
||
const ctx=document.getElementById('chart-registrations').getContext('2d');
|
||
charts['registrations']=new Chart(ctx,{
|
||
data:{labels,datasets:[
|
||
{label:'Доля регистраций (%)',data:actual,type:'bar',
|
||
backgroundColor:snaps.map(s=>s.registered_pct>=0.60?'rgba(16,185,129,0.75)':'rgba(59,130,246,0.75)').concat(Array(12-snaps.length).fill('rgba(59,130,246,0.15)')),borderRadius:6,yAxisID:'y'},
|
||
makeForecastDataset('registered_pct','rgba(59,130,246,0.5)'),
|
||
{label:'Цель (60%)',data:Array(12).fill(60),type:'line',borderColor:'#EF4444',borderDash:[6,3],pointRadius:0,borderWidth:2,yAxisID:'y'},
|
||
{label:'Зарег. пользователей',data:reg2,type:'line',borderColor:'#10B981',backgroundColor:'rgba(16,185,129,0.08)',pointRadius:4,pointBackgroundColor:'#10B981',borderWidth:2,yAxisID:'y2',tension:0.3},
|
||
]},
|
||
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:12,usePointStyle:true}},
|
||
tooltip:{callbacks:{label:ctx=>ctx.dataset.label.includes('пользователей')?` ${ctx.dataset.label}: ${fmtInt(ctx.raw)}`:ctx.raw!==null?` ${ctx.dataset.label}: ${ctx.raw}%`:null}}},
|
||
scales:{y:{...axisRange([actual,Array(12).fill(60),makeForecastDataset('registered_pct','').data].flat(),{forceMin:0}),
|
||
title:{display:true,text:'% регистраций'},ticks:{callback:v=>v+'%'}},
|
||
y2:{position:'right',title:{display:true,text:'Кол-во пользователей'},grid:{drawOnChartArea:false},ticks:{callback:v=>fmtInt(v)}}}}
|
||
});
|
||
showRiskBanner('registrations','registered_pct',0.60);
|
||
}
|
||
|
||
// ═══════════════════ KPI 2 — MAU ═══════════════════
|
||
function renderChartMau() { AppState.mauTab==='daily'?renderChartMauDaily():renderChartMauMonthly(); }
|
||
|
||
function renderChartMauMonthly() {
|
||
const snaps=getSnapshots(), labels=allMonthLabels();
|
||
const actual=Array(12).fill(null);
|
||
snaps.forEach((s,i)=>{ actual[i]=+(s.mau_per_registered*100).toFixed(1); });
|
||
const ctx=document.getElementById('chart-mau').getContext('2d');
|
||
charts['mau']=new Chart(ctx,{
|
||
data:{labels,datasets:[
|
||
{label:'MAU / Зарег. (%)',data:actual,type:'bar',
|
||
backgroundColor:snaps.map(s=>s.mau_per_registered>=0.30?'rgba(16,185,129,0.75)':s.mau_per_registered>=0.255?'rgba(245,158,11,0.75)':'rgba(139,92,246,0.75)').concat(Array(12-snaps.length).fill('rgba(139,92,246,0.15)')),borderRadius:6,yAxisID:'y'},
|
||
makeForecastDataset('mau_per_registered','rgba(139,92,246,0.5)'),
|
||
{label:'Цель (30%)',data:Array(12).fill(30),type:'line',borderColor:'#EF4444',borderDash:[6,3],pointRadius:0,borderWidth:2,yAxisID:'y'},
|
||
]},
|
||
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:12,usePointStyle:true}},
|
||
tooltip:{callbacks:{label:ctx=>ctx.raw!==null?` ${ctx.dataset.label}: ${ctx.raw}%`:null}}},
|
||
scales:{y:{...axisRange([actual,Array(12).fill(30),makeForecastDataset('mau_per_registered','').data].flat(),{forceMin:0}),
|
||
title:{display:true,text:'% MAU'},ticks:{callback:v=>v+'%'}}}}
|
||
});
|
||
showRiskBanner('mau','mau_per_registered',0.30);
|
||
}
|
||
|
||
function renderChartMauDaily() {
|
||
const pid=AppState.selectedPeriod||AppState.snapshots[AppState.snapshots.length-1].report_period_id;
|
||
const daily=AppState.dailySeries[pid]||[];
|
||
const ctx=document.getElementById('chart-mau').getContext('2d');
|
||
charts['mau']=new Chart(ctx,{type:'line',
|
||
data:{labels:daily.map(r=>r.entry_date.slice(5)),datasets:[{label:`MAU нарастающим итогом — ${periodLabel(pid)}`,data:daily.map(r=>r.mau_daily),borderColor:'#8B5CF6',backgroundColor:'rgba(139,92,246,0.1)',fill:true,tension:0.4,pointRadius:2,borderWidth:2}]},
|
||
options:{responsive:true,maintainAspectRatio:false,
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:12}},tooltip:{callbacks:{label:ctx=>` MAU: ${fmtInt(ctx.raw)}`}}},
|
||
scales:{y:{title:{display:true,text:'Уникальных пользователей'},ticks:{callback:v=>fmtInt(v)}},x:{title:{display:true,text:'Дата (MM-ДД)'}}}}
|
||
});
|
||
document.getElementById('risk-mau').classList.add('hidden');
|
||
}
|
||
|
||
function switchMauTab(tab) {
|
||
AppState.mauTab=tab;
|
||
document.getElementById('tab-mau-monthly').classList.toggle('active',tab==='monthly');
|
||
document.getElementById('tab-mau-daily').classList.toggle('active',tab==='daily');
|
||
if(charts['mau']){charts['mau'].destroy();delete charts['mau'];}
|
||
renderChartMau();
|
||
}
|
||
|
||
// ═══════════════════ KPI 3 — TRADITIONAL ═══════════════════
|
||
function renderChartTraditional() {
|
||
const snaps=getSnapshots(), labels=allMonthLabels();
|
||
const cur=[],prev=[],dec2=[];
|
||
Array(12).fill(0).forEach((_,i)=>{
|
||
const s=snaps[i];
|
||
cur.push(s?+(s.traditional_comms_pct*100).toFixed(2):null);
|
||
prev.push(s?+(s.prev_year_traditional_comms_pct*100).toFixed(2):null);
|
||
dec2.push(s?+(s.traditional_comms_decrease_pct*100).toFixed(2):null);
|
||
});
|
||
const ctx=document.getElementById('chart-traditional').getContext('2d');
|
||
charts['traditional']=new Chart(ctx,{
|
||
data:{labels,datasets:[
|
||
{label:'Традиц. обращения 2026 (%)',data:cur,type:'bar',backgroundColor:'rgba(239,68,68,0.65)',borderRadius:4,yAxisID:'y'},
|
||
{label:'Традиц. обращения 2025 (%)',data:prev,type:'bar',backgroundColor:'rgba(252,165,165,0.5)',borderRadius:4,yAxisID:'y'},
|
||
{label:'Снижение (%)',data:dec2,type:'line',borderColor:'#10B981',backgroundColor:'rgba(16,185,129,0.08)',pointRadius:5,pointBackgroundColor:'#10B981',borderWidth:2,yAxisID:'y2',tension:0.3},
|
||
{label:'Цель снижения (10%)',data:Array(12).fill(10),type:'line',borderColor:'#EF4444',borderDash:[6,3],pointRadius:0,borderWidth:2,yAxisID:'y2'},
|
||
]},
|
||
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:10,usePointStyle:true}},
|
||
tooltip:{callbacks:{label:ctx=>ctx.raw!==null?` ${ctx.dataset.label}: ${ctx.raw}%`:null}}},
|
||
scales:{y:{...axisRange([cur,prev].flat(),{forceMin:0}),
|
||
title:{display:true,text:'% обращений'},ticks:{callback:v=>v+'%'}},
|
||
y2:{position:'right',...axisRange([dec2,Array(12).fill(10)].flat(),{forceMin:0}),
|
||
title:{display:true,text:'% снижения'},grid:{drawOnChartArea:false},ticks:{callback:v=>v+'%'}}}}
|
||
});
|
||
showRiskBanner('traditional','traditional_comms_decrease_pct',0.10);
|
||
}
|
||
|
||
// ═══════════════════ KPI 4 — DIGITAL SALES ═══════════════════
|
||
function renderChartDigitalSales() {
|
||
const snaps=getSnapshots(), labels=allMonthLabels();
|
||
const data=[],digital=[],total=[];
|
||
Array(12).fill(0).forEach((_,i)=>{
|
||
const s=snaps[i];
|
||
data.push(s?+(s.fd_rap_pct*100).toFixed(1):null);
|
||
digital.push(s?s.cumulative_digital_rap_total:null);
|
||
total.push(s?s.cumulative_rap_total:null);
|
||
});
|
||
const fc=makeForecastDataset('fd_rap_pct','rgba(16,185,129,0.4)');
|
||
fc.fill=false;
|
||
const ctx=document.getElementById('chart-digital-sales').getContext('2d');
|
||
charts['digital-sales']=new Chart(ctx,{
|
||
data:{labels,datasets:[
|
||
{label:'Доля цифровых продаж (%)',data,type:'line',borderColor:'#10B981',backgroundColor:'rgba(16,185,129,0.12)',fill:true,tension:0.4,pointRadius:5,pointBackgroundColor:'#10B981',borderWidth:2.5,yAxisID:'y'},
|
||
fc,
|
||
{label:'Цель (45%)',data:Array(12).fill(45),type:'line',borderColor:'#EF4444',borderDash:[6,3],pointRadius:0,borderWidth:2,yAxisID:'y'},
|
||
]},
|
||
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:12,usePointStyle:true}},
|
||
tooltip:{callbacks:{label:ctx=>ctx.raw!==null?` ${ctx.dataset.label}: ${ctx.raw}%`:null,
|
||
afterBody:items=>{ const i=items[0].dataIndex; return digital[i]!==null?[` Цифр.: ${fmtMoney(digital[i])}`,' Все: '+fmtMoney(total[i])]:[];} }}},
|
||
scales:{y:{...axisRange([data,fc.data,Array(12).fill(45)].flat(),{forceMin:0,forceMax:100}),
|
||
title:{display:true,text:'% цифровых продаж'},ticks:{callback:v=>v+'%'}}}}
|
||
});
|
||
showRiskBanner('digital-sales','fd_rap_pct',0.45);
|
||
}
|
||
|
||
// ═══════════════════ KPI 5 — FD ORDERS ═══════════════════
|
||
function renderChartFdOrders() {
|
||
const snaps=getSnapshots(), labels=allMonthLabels();
|
||
const orders=[],goals=[],cumPct=[];
|
||
Array(12).fill(0).forEach((_,i)=>{
|
||
const s=snaps[i];
|
||
orders.push(s?s.fd_orders:null);
|
||
goals.push(s?s.fd_orders_goal:null);
|
||
cumPct.push(s?+(s.cum_fd_orders_pct*100).toFixed(1):null);
|
||
});
|
||
|
||
// ── Trend line for fd_orders (monthly count) ──
|
||
const orderXs = snaps.map((_,i)=>i);
|
||
const orderYs = snaps.map(s=>s.fd_orders);
|
||
const {slope:ordSlope, intercept:ordIntercept} = linearRegression(orderXs, orderYs);
|
||
const orderTrend = Array(12).fill(null);
|
||
for(let m=0;m<12;m++) orderTrend[m] = Math.max(0, ordIntercept + ordSlope*m);
|
||
|
||
// Average monthly orders
|
||
const avg = orderYs.reduce((a,b)=>a+b,0)/orderYs.length;
|
||
|
||
const ctx=document.getElementById('chart-fd-orders').getContext('2d');
|
||
charts['fd-orders']=new Chart(ctx,{
|
||
data:{labels,datasets:[
|
||
{label:'FD заказы (цель)',data:goals,type:'bar',backgroundColor:'rgba(99,102,241,0.18)',borderColor:'rgba(99,102,241,0.4)',borderWidth:1,borderRadius:6,yAxisID:'y'},
|
||
{label:'FD заказы (факт)',data:orders,type:'bar',
|
||
backgroundColor:snaps.map(s=>s.fd_orders>=s.fd_orders_goal?'rgba(16,185,129,0.75)':s.fd_orders>=s.fd_orders_goal*0.85?'rgba(245,158,11,0.75)':'rgba(99,102,241,0.75)').concat(Array(12-snaps.length).fill('rgba(99,102,241,0.15)')),borderRadius:6,yAxisID:'y'},
|
||
// Trend line for monthly orders
|
||
{label:'Тренд заказов',data:orderTrend,type:'line',borderColor:'#F97316',borderDash:[4,3],pointRadius:0,borderWidth:2,yAxisID:'y',tension:0.3},
|
||
// Average line
|
||
{label:`Среднее (${fmtInt(avg)} шт./мес)`,data:Array(12).fill(+avg.toFixed(0)),type:'line',borderColor:'#8B5CF6',borderDash:[6,2],pointRadius:0,borderWidth:1.5,yAxisID:'y'},
|
||
// Cumulative %
|
||
{label:'Накопл. выполнение (%)',data:cumPct,type:'line',borderColor:'#F59E0B',backgroundColor:'rgba(245,158,11,0.08)',pointRadius:5,pointBackgroundColor:'#F59E0B',borderWidth:2.5,yAxisID:'y2',tension:0.3},
|
||
]},
|
||
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
|
||
plugins:{legend:{position:'bottom',labels:{boxWidth:12,padding:10,usePointStyle:true}},
|
||
tooltip:{callbacks:{label:ctx=>{
|
||
if(ctx.raw===null)return null;
|
||
return ctx.dataset.yAxisID==='y2'?` ${ctx.dataset.label}: ${ctx.raw}%`:` ${ctx.dataset.label}: ${fmtInt(ctx.raw)} шт.`;
|
||
}}}},
|
||
scales:{y:{min:0,title:{display:true,text:'Заказы (шт.)'},ticks:{callback:v=>fmtInt(v)}},
|
||
y2:{position:'right',min:0,max:100,title:{display:true,text:'% выполнения (кум.)'},grid:{drawOnChartArea:false},ticks:{callback:v=>v+'%'}}}}
|
||
});
|
||
|
||
// Progress bar
|
||
const last=AppState.snapshots[AppState.snapshots.length-1];
|
||
const cumGoal=last.cum_fd_orders_goal||0, cumOrd=last.cum_fd_orders||0;
|
||
const pct=cumGoal>0?cumOrd/cumGoal:0;
|
||
document.getElementById('cum-fd-text').textContent=`${fmtInt(cumOrd)} из ${fmtInt(cumGoal)}`;
|
||
document.getElementById('cum-fd-pct').textContent=fmtPct(pct);
|
||
document.getElementById('cum-fd-bar').style.width=Math.min(pct*100,100)+'%';
|
||
document.getElementById('fd-annotation').classList.toggle('hidden',pct>=0.5);
|
||
|
||
// Trend info box
|
||
const decForecast = Math.max(0, ordIntercept + ordSlope*11);
|
||
const annualByTrend = orderTrend.slice(0,12).reduce((s,v)=>s+(v||0),0);
|
||
const tdEl = document.getElementById('fd-trend-info');
|
||
tdEl.innerHTML = `📈 <b>Анализ тренда:</b> среднее ${fmtInt(avg)} шт./мес · `+
|
||
`тренд к декабрю: ~${fmtInt(decForecast)} шт./мес · `+
|
||
`прогноз на год по тренду: ~${fmtInt(annualByTrend)} шт. `+
|
||
`<span style="color:var(--color-text-secondary)">(при текущей цели ${fmtInt(cumGoal)} шт.)</span>`;
|
||
tdEl.classList.remove('hidden');
|
||
}
|
||
|
||
function renderAllCharts() {
|
||
renderChartRegistrations();
|
||
renderChartMau();
|
||
renderChartTraditional();
|
||
renderChartDigitalSales();
|
||
renderChartFdOrders();
|
||
}
|
||
|
||
// ═══════════════════ AI KEY ═══════════════════
|
||
function initAiKey() {
|
||
const input=document.getElementById('ai-api-key'), note=document.getElementById('ai-key-note');
|
||
if(window.DS_KEY&&window.DS_KEY.trim()){
|
||
input.value=window.DS_KEY.trim(); input.disabled=true; note.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// ═══════════════════ AI — DeepSeek ═══════════════════
|
||
function buildPrompt(snapshots) {
|
||
// ── Full monthly snapshot data (all columns) ──────────────────────────────
|
||
const rawTable = snapshots.map(s => [
|
||
periodLabel(s.report_period_id),
|
||
`абонентов: ${fmtInt(s.abons)}`,
|
||
`зарег.: ${fmtInt(s.registered_total)} (${fmtPct(s.registered_pct)})`,
|
||
`MAU: ${fmtInt(s.mau_daily)} (${fmtPct(s.mau_per_registered)} от зарег.)`,
|
||
`традиц.обр.тек.год: ${fmtPct(s.traditional_comms_pct)}`,
|
||
`традиц.обр.пр.год: ${fmtPct(s.prev_year_traditional_comms_pct)}`,
|
||
`снижение обр.: ${fmtPct(s.traditional_comms_decrease_pct)}`,
|
||
`цифр.выручка: ${fmtMoney(s.cumulative_digital_rap_total)}`,
|
||
`общ.выручка: ${fmtMoney(s.cumulative_rap_total)}`,
|
||
`доля цифр.прод.: ${fmtPct(s.fd_rap_pct)}`,
|
||
`FD мес.: ${s.fd_orders}шт. из ${s.fd_orders_goal} (${fmtPct(s.fd_orders_pct)})`,
|
||
`FD накопл.: ${fmtInt(s.cum_fd_orders)}шт. из ${fmtInt(s.cum_fd_orders_goal)} (${fmtPct(s.cum_fd_orders_pct)})`,
|
||
].join(' | ')).join('\n');
|
||
|
||
// ── Daily MAU summary per month (first / mid / last day) ─────────────────
|
||
const mauSummary = Object.entries(AppState.dailySeries).sort().map(([pid, days]) => {
|
||
const first = days[0], mid = days[Math.floor(days.length/2)], last = days[days.length-1];
|
||
return ` ${periodLabel(Number(pid))}: день1=${fmtInt(first.mau_daily)}, сер.мес.=${fmtInt(mid.mau_daily)}, конец=${fmtInt(last.mau_daily)}`;
|
||
}).join('\n');
|
||
|
||
const rows=snapshots.map(s=>`${periodLabel(s.report_period_id)}: `+
|
||
`Рег=${fmtPct(s.registered_pct)}, MAU=${fmtPct(s.mau_per_registered)}, `+
|
||
`Снижение обр.=${fmtPct(s.traditional_comms_decrease_pct)}, `+
|
||
`Цифр.прод.=${fmtPct(s.fd_rap_pct)}, `+
|
||
`FD кум.=${fmtInt(s.cum_fd_orders)}шт.(${fmtPct(s.cum_fd_orders_pct)}), FD мес.=${s.fd_orders}шт.`
|
||
).join('\n');
|
||
|
||
// Smart forecasts for December
|
||
const forecastLines=KPI_CONFIG.map(kpi=>{
|
||
const {values:fc, modelLabel}=smartForecast(snapshots, kpi.field, 12);
|
||
return fc[11]!==null?` ${kpi.name}: ${fmtPct(fc[11])} (${modelLabel})`:null;
|
||
}).filter(Boolean).join('\n');
|
||
|
||
// FD orders trend info
|
||
const orderYs=snapshots.map(s=>s.fd_orders);
|
||
const {slope:os,intercept:oi}=linearRegression(snapshots.map((_,i)=>i),orderYs);
|
||
const avgOrders=orderYs.reduce((a,b)=>a+b,0)/orderYs.length;
|
||
const decFdOrders=Math.max(0,oi+os*11);
|
||
const annualFdByTrend=Array.from({length:12},(_,m)=>Math.max(0,oi+os*m)).reduce((a,b)=>a+b,0);
|
||
|
||
return `Ты аналитик KPI отдела цифровизации клиентских путей Казахтелеком.
|
||
|
||
══ ПОЛНЫЕ ДАННЫЕ ПО МЕСЯЦАМ (все показатели) ══
|
||
${rawTable}
|
||
|
||
══ ЕЖЕДНЕВНЫЙ РОСТ MAU ВНУТРИ МЕСЯЦА (начало / середина / конец) ══
|
||
${mauSummary}
|
||
|
||
══ СВОДКА KPI ПО МЕСЯЦАМ ══
|
||
${rows}
|
||
|
||
Прогноз на декабрь 2026 по каждому KPI:
|
||
${forecastLines}
|
||
|
||
Анализ KPI 5 (Full Digital установки):
|
||
Среднее за период: ${fmtInt(avgOrders)} шт./мес
|
||
Тренд к декабрю: ~${fmtInt(decFdOrders)} шт./мес
|
||
Прогноз суммарно за год по тренду: ~${fmtInt(annualFdByTrend)} шт.
|
||
Текущая годовая цель: 12 000 шт.
|
||
|
||
Целевые показатели:
|
||
- Регистрации: 60%
|
||
- MAU: 30%
|
||
- Снижение традиционных обращений: 10%
|
||
- Доля цифровых продаж: 45%
|
||
- Full Digital установки: 1 000/мес (12 000 за год)
|
||
|
||
Примечание по KPI 4 (Цифровые продажи): тренд нелинейный — снижение замедляется (объясняется природой данных: цифровые каналы содержат разовые платежи + подписки, традиционные — только подписки; к концу года ожидается стабилизация доли).
|
||
|
||
Задача: проанализируй выполнение KPI с учётом прогноза на конец года. Даже выполненные сейчас KPI оцени с точки зрения риска до декабря.
|
||
|
||
Ответ строго на русском языке. Структура:
|
||
1. Краткий вывод (2-3 предложения)
|
||
2. KPI в норме (с оценкой устойчивости тренда до конца года)
|
||
3. Рисковые и отстающие KPI — причины, прогноз, риски
|
||
4. Конкретные рекомендации (3-5 пунктов)
|
||
5. Предложение по пересмотру цели KPI 5 (Full Digital) на основе тренда`;
|
||
}
|
||
|
||
async function runAiAnalysis() {
|
||
const input=document.getElementById('ai-api-key');
|
||
const apiKey=(window.DS_KEY||input.value||'').trim();
|
||
if(!apiKey){showAiError('Введите DeepSeek API ключ');return;}
|
||
const btn=document.getElementById('btn-ai-analyze');
|
||
btn.disabled=true; showAiLoading(true); showAiError('');
|
||
document.getElementById('ai-result').classList.add('hidden');
|
||
const controller=new AbortController();
|
||
const t=setTimeout(()=>controller.abort(),25000);
|
||
try {
|
||
const resp=await fetch('https://api.deepseek.com/chat/completions',{
|
||
method:'POST', signal:controller.signal,
|
||
headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},
|
||
body:JSON.stringify({
|
||
model:'deepseek-chat', max_tokens:4000,
|
||
messages:[
|
||
{role:'system',content:'Ты опытный аналитик данных. Отвечай структурированно, конкретно, на русском языке.'},
|
||
{role:'user', content:buildPrompt(AppState.snapshots)},
|
||
],
|
||
}),
|
||
});
|
||
clearTimeout(t);
|
||
if(!resp.ok){
|
||
let m=`HTTP ${resp.status}`;
|
||
try{const e=await resp.json();m=e.error?.message||m;}catch{}
|
||
if(resp.status===401)m='Неверный API ключ';
|
||
throw new Error(m);
|
||
}
|
||
const data=await resp.json();
|
||
const text=data.choices?.[0]?.message?.content||'';
|
||
AppState.lastAiResponse=text;
|
||
sessionStorage.setItem('lastAiResponse',text);
|
||
showAiResult(text);
|
||
await saveAiCache(text); // save to git repo for all users (requires GITEA_TOKEN in config.js)
|
||
} catch(e) {
|
||
clearTimeout(t);
|
||
showAiError(e.name==='AbortError'?'Превышено время ожидания. Попробуйте снова.':'Ошибка: '+e.message);
|
||
} finally { showAiLoading(false); btn.disabled=false; }
|
||
}
|
||
|
||
function showAiLoading(show){document.getElementById('ai-loading').classList.toggle('hidden',!show);}
|
||
function showAiResult(text){
|
||
const el = document.getElementById('ai-text');
|
||
if (window.marked) {
|
||
// Configure marked: safe renderer, no mangling
|
||
marked.setOptions({ breaks: true, gfm: true });
|
||
el.innerHTML = marked.parse(text);
|
||
} else {
|
||
el.textContent = text; // fallback if CDN failed
|
||
}
|
||
document.getElementById('ai-result').classList.remove('hidden');
|
||
}
|
||
function showAiError(msg){const el=document.getElementById('ai-error');if(msg){el.textContent=msg;el.classList.remove('hidden');}else el.classList.add('hidden');}
|
||
function restoreAiCache(){const c=sessionStorage.getItem('lastAiResponse');if(c)showAiResult(c);}
|
||
|
||
// ═══════════════════ AI CACHE (git-based, shared for all users) ═══════════════════
|
||
|
||
/** Load AI response from ai-cache.json stored in the git repo */
|
||
async function loadAiCache() {
|
||
const dateEl = document.getElementById('ai-cache-date');
|
||
const details = document.getElementById('ai-update-details');
|
||
|
||
// 1. Try git-based shared cache
|
||
try {
|
||
const resp = await fetch(AI_CACHE_RAW, {cache:'no-cache'});
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
if (data.response) {
|
||
AppState.lastAiResponse = data.response;
|
||
showAiResult(data.response);
|
||
if (dateEl && data.generated_at) {
|
||
const d = new Date(data.generated_at);
|
||
dateEl.textContent = 'Обновлено: ' + d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
|
||
}
|
||
return; // ✓ got shared cache — done
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.info('AI git cache unavailable:', e.message);
|
||
}
|
||
|
||
// 2. Fallback: sessionStorage from this browser's previous session
|
||
const session = sessionStorage.getItem('lastAiResponse');
|
||
if (session) {
|
||
AppState.lastAiResponse = session;
|
||
showAiResult(session);
|
||
if (dateEl) dateEl.textContent = 'Из предыдущей сессии браузера';
|
||
return;
|
||
}
|
||
|
||
// 3. Nothing found — prompt user to generate
|
||
if (dateEl) dateEl.textContent = 'Анализ ещё не сгенерирован';
|
||
if (details) details.open = true; // auto-expand the refresh section
|
||
}
|
||
|
||
/** Save AI response to ai-cache.json in git via Gitea API.
|
||
* Requires window.GITEA_TOKEN (from config.js). */
|
||
async function saveAiCache(text) {
|
||
const token = window.GITEA_TOKEN;
|
||
if (!token) return; // no write access — cache stays local only
|
||
|
||
const lastSnap = AppState.snapshots[AppState.snapshots.length-1];
|
||
const payload = {
|
||
generated_at: new Date().toISOString(),
|
||
period: periodLabel(lastSnap.report_period_id),
|
||
response: text,
|
||
};
|
||
|
||
// base64-encode the JSON (Gitea API requires base64 content)
|
||
const content = btoa(unescape(encodeURIComponent(JSON.stringify(payload, null, 2))));
|
||
|
||
// Fetch current file SHA (needed for update; absent for first create)
|
||
let sha = null;
|
||
try {
|
||
const fileResp = await fetch(
|
||
`${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
|
||
{headers:{'Authorization':`token ${token}`}}
|
||
);
|
||
if (fileResp.ok) { sha = (await fileResp.json()).sha; }
|
||
} catch {}
|
||
|
||
const body = JSON.stringify({
|
||
message: `Update AI analysis cache — ${payload.period}`,
|
||
content,
|
||
...(sha ? {sha} : {}),
|
||
});
|
||
|
||
try {
|
||
const saveResp = await fetch(
|
||
`${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
|
||
{
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `token ${token}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body,
|
||
}
|
||
);
|
||
if (saveResp.ok) {
|
||
const dateEl = document.getElementById('ai-cache-date');
|
||
if (dateEl) dateEl.textContent = 'Обновлено: ' +
|
||
new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
|
||
console.info('AI cache saved to git ✓');
|
||
} else {
|
||
console.warn('AI cache save failed:', saveResp.status);
|
||
}
|
||
} catch(e) {
|
||
console.warn('AI cache save error:', e.message);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════ EVENTS ═══════════════════
|
||
document.getElementById('file-input').addEventListener('change',function(e){
|
||
const file=e.target.files[0]; if(!file)return;
|
||
const reader=new FileReader();
|
||
reader.onload=ev=>{try{initWithData(ev.target.result);showDashboard();}catch(err){document.getElementById('upload-error').textContent='Ошибка: '+err.message;}};
|
||
reader.readAsText(file,'UTF-8'); this.value='';
|
||
});
|
||
|
||
document.getElementById('btn-export-csv').addEventListener('click',()=>{
|
||
if(!AppState.rawCsvText)return;
|
||
const blob=new Blob([AppState.rawCsvText],{type:'text/csv;charset=utf-8;'});
|
||
const url=URL.createObjectURL(blob);
|
||
const a=document.createElement('a'); a.href=url; a.download=CSV_FILENAME; a.click(); URL.revokeObjectURL(url);
|
||
});
|
||
|
||
document.getElementById('btn-reload').addEventListener('click',async()=>{
|
||
destroyAllCharts();
|
||
document.getElementById('dashboard').style.display='none';
|
||
document.getElementById('loading-screen').style.display='flex';
|
||
Object.assign(AppState,{rawRows:[],snapshots:[],dailySeries:{},selectedPeriod:null,rawCsvText:'',lastAiResponse:''});
|
||
await tryAutoLoad();
|
||
});
|
||
|
||
document.getElementById('btn-ai-analyze').addEventListener('click',runAiAnalysis);
|
||
document.getElementById('btn-ai-copy').addEventListener('click',()=>{
|
||
if(!AppState.lastAiResponse)return;
|
||
navigator.clipboard.writeText(AppState.lastAiResponse).then(()=>{
|
||
const b=document.getElementById('btn-ai-copy'); b.textContent='✅ Скопировано!';
|
||
setTimeout(()=>b.textContent='📋 Скопировать',2000);
|
||
});
|
||
});
|
||
|
||
// ═══════════════════ KPI MODAL ═══════════════════
|
||
let modalCharts = [];
|
||
|
||
function openKpiModal(kpiId) {
|
||
const kpi = KPI_CONFIG.find(k=>k.id===kpiId);
|
||
const detail = KPI_DETAIL[kpiId];
|
||
const last = AppState.snapshots[AppState.snapshots.length-1];
|
||
const val = last[kpi.field];
|
||
|
||
document.getElementById('modal-title').innerHTML = `${kpi.icon} ${kpi.name}`;
|
||
document.getElementById('modal-info').innerHTML = detail.description;
|
||
document.getElementById('modal-formula').innerHTML =
|
||
`${detail.formula} = <span class="formula-val">${fmtPct(val)}</span>`;
|
||
const linkEl = document.getElementById('modal-link');
|
||
linkEl.href = detail.qlik;
|
||
linkEl.style.display = detail.qlik ? 'inline-flex' : 'none';
|
||
|
||
document.getElementById('kpi-modal-overlay').classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// Render charts after modal is visible
|
||
setTimeout(() => renderModalCharts(kpiId, detail), 100);
|
||
}
|
||
|
||
function closeKpiModal(e) {
|
||
if (e && e.target !== document.getElementById('kpi-modal-overlay')) return;
|
||
document.getElementById('kpi-modal-overlay').classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
modalCharts.forEach(c => c.destroy());
|
||
modalCharts = [];
|
||
}
|
||
|
||
function getMauMaxPerMonth() {
|
||
return AppState.snapshots.map((s, i) => {
|
||
const pid = s.report_period_id;
|
||
const days = AppState.dailySeries[pid] || [];
|
||
if (days.length === 0) return s.mau_daily;
|
||
return Math.max(...days.map(d => d.mau_daily));
|
||
});
|
||
}
|
||
|
||
function renderModalCharts(kpiId, detail) {
|
||
modalCharts.forEach(c => c.destroy());
|
||
modalCharts = [];
|
||
const labels = AppState.snapshots.map(s => periodLabel(s.report_period_id));
|
||
const colors = ['#0052CC', '#F59E0B'];
|
||
|
||
detail.charts.forEach((ch, idx) => {
|
||
let data;
|
||
if (ch.field === 'mau_max') {
|
||
data = getMauMaxPerMonth();
|
||
} else {
|
||
data = AppState.snapshots.map(s => s[ch.field]);
|
||
}
|
||
const canvasId = `modal-chart${idx+1}`;
|
||
|
||
document.getElementById(`modal-chart${idx+1}-label`).textContent = ch.label;
|
||
|
||
const dataset = {
|
||
label: ch.label,
|
||
data,
|
||
backgroundColor: colors[idx]+'20',
|
||
borderColor: colors[idx],
|
||
borderWidth: 2,
|
||
pointRadius: 4,
|
||
pointBackgroundColor: colors[idx],
|
||
tension: 0.3,
|
||
};
|
||
|
||
if (ch.isPct) {
|
||
dataset.data = data.map(v => v !== null ? +(v*100).toFixed(2) : null);
|
||
}
|
||
|
||
const yOpts = {};
|
||
if (ch.isMoney) {
|
||
yOpts.ticks = {callback: v => fmtMoney(v)};
|
||
} else if (ch.isPct) {
|
||
yOpts.ticks = {callback: v => v+'%'};
|
||
} else if (data.some(v => v > 10000)) {
|
||
yOpts.ticks = {callback: v => fmtInt(v)};
|
||
}
|
||
|
||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||
const chart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {labels, datasets: [dataset]},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: {legend: {display: false}},
|
||
scales: {
|
||
y: {...yOpts, beginAtZero: false},
|
||
x: {ticks: {font: {size: 10}}},
|
||
},
|
||
},
|
||
});
|
||
modalCharts.push(chart);
|
||
});
|
||
}
|
||
|
||
// ═══════════════════ LOGIN ═══════════════════
|
||
const LOGIN_PASSWORD = 'KTdash1';
|
||
|
||
function handleLogin() {
|
||
const input = document.getElementById('login-password');
|
||
const err = document.getElementById('login-error');
|
||
if (input.value === LOGIN_PASSWORD) {
|
||
document.getElementById('login-screen').style.display = 'none';
|
||
document.getElementById('loading-screen').style.display = 'flex';
|
||
if (sessionStorage) sessionStorage.setItem('kpi_auth', '1');
|
||
tryAutoLoad();
|
||
} else {
|
||
err.textContent = 'Неверный пароль';
|
||
input.value = '';
|
||
}
|
||
}
|
||
|
||
document.getElementById('btn-login').addEventListener('click', handleLogin);
|
||
document.getElementById('login-password').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') handleLogin();
|
||
});
|
||
|
||
// ═══════════════════ BOOT ═══════════════════
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
if (sessionStorage && sessionStorage.getItem('kpi_auth') === '1') {
|
||
document.getElementById('login-screen').style.display = 'none';
|
||
document.getElementById('loading-screen').style.display = 'flex';
|
||
tryAutoLoad();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|