1342 lines
94 KiB
HTML
1342 lines
94 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%); }
|
||
.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.2s, transform 0.15s; border-top: 4px solid var(--color-border); }
|
||
.kpi-card:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-2px); }
|
||
.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; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOADING -->
|
||
<div id="loading-screen">
|
||
<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>
|
||
|
||
<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_year_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-29";2100718;1067535;0.508;236392;0.221;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-28";2100718;1067532;0.508;236392;0.221;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-27";2100718;1066924;0.508;231473;0.217;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-26";2100718;1066631;0.508;227794;0.214;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-25";2100718;1066084;0.507;222648;0.209;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-24";2100718;1065569;0.507;217146;0.204;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-23";2100718;1065297;0.507;210438;0.198;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-22";2100718;1064996;0.507;206702;0.194;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-21";2100718;1064508;0.507;201861;0.19;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-20";2100718;1064000;0.506;196136;0.184;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-19";2100718;1063493;0.506;190391;0.179;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-18";2100718;1063028;0.506;185169;0.174;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-17";2100718;1062578;0.506;179417;0.169;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-16";2100718;1062398;0.506;175852;0.166;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-15";2100718;1062131;0.506;171740;0.162;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-14";2100718;1061692;0.505;165454;0.156;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-13";2100718;1061183;0.505;158742;0.15;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-12";2100718;1060626;0.505;150930;0.142;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-11";2100718;1060034;0.505;140882;0.133;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-10";2100718;1059741;0.504;133994;0.126;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-09";2100718;1059532;0.504;108417;0.102;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-08";2100718;1059344;0.504;103158;0.097;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-07";2100718;1058907;0.504;93860;0.089;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-06";2100718;1058693;0.504;87307;0.082;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-05";2100718;1058239;0.504;77687;0.073;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-04";2100718;1057748;0.504;61144;0.058;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-03";2100718;1057207;0.503;47949;0.045;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-02";2100718;1057009;0.503;38528;0.036;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202605;"2026-05-01";2100718;1056756;0.503;25816;0.024;0.5346041830749427;0.6678292219732731;0.19948968166544545;2124943881.8517177;4001729145.793371;0.531006423582032;164;1000;0.164;1343;5000;0.2686
|
||
202604;"2026-04-30";2106236;1056399;0.502;262629;0.249;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-29";2106236;1055833;0.501;257844;0.244;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-28";2106236;1055340;0.501;253602;0.24;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-27";2106236;1054774;0.501;248967;0.236;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-26";2106236;1054128;0.5;243467;0.231;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-25";2106236;1053911;0.5;240371;0.228;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-24";2106236;1053569;0.5;236545;0.225;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-23";2106236;1053042;0.5;229391;0.218;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-22";2106236;1052586;0.5;223915;0.213;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-21";2106236;1052162;0.5;218407;0.208;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-20";2106236;1051719;0.499;213139;0.203;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-19";2106236;1051232;0.499;207205;0.197;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-18";2106236;1051035;0.499;203784;0.194;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-17";2106236;1050776;0.499;199826;0.19;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-16";2106236;1050388;0.499;194196;0.185;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-15";2106236;1049972;0.499;189147;0.18;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-14";2106236;1049551;0.498;182802;0.174;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-13";2106236;1049115;0.498;176433;0.168;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-12";2106236;1048669;0.498;169981;0.162;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-11";2106236;1048506;0.498;165624;0.158;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-10";2106236;1048275;0.498;159654;0.152;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-09";2106236;1047857;0.498;135848;0.13;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-08";2106236;1047422;0.497;127968;0.122;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-07";2106236;1046968;0.497;118896;0.114;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-06";2106236;1046511;0.497;109255;0.104;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-05";2106236;1046023;0.497;100140;0.096;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-04";2106236;1045862;0.497;89993;0.086;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-03";2106236;1045604;0.496;78934;0.075;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-02";2106236;1045104;0.496;63301;0.061;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202604;"2026-04-01";2106236;1044565;0.496;42523;0.041;0.5338425639244234;0.6673874142558902;0.20010094208978135;1710926678.0289857;2933216654.6453385;0.5832936599890735;267;1000;0.267;1179;4000;0.29475
|
||
202603;"2026-03-31";2111350;1043888;0.494;274372;0.263;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-30";2111350;1043214;0.494;269033;0.258;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-29";2111350;1042636;0.494;263479;0.253;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-28";2111350;1042415;0.494;259821;0.249;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-27";2111350;1042108;0.494;255783;0.245;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-26";2111350;1041554;0.493;250261;0.24;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-25";2111350;1040942;0.493;243903;0.234;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-24";2111350;1040609;0.493;239163;0.23;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-23";2111350;1040287;0.493;230597;0.222;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-22";2111350;1040068;0.493;227016;0.218;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-21";2111350;1039913;0.493;224156;0.216;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-20";2111350;1039702;0.492;220464;0.212;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-19";2111350;1039290;0.492;214057;0.206;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-18";2111350;1038815;0.492;207676;0.2;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-17";2111350;1038395;0.492;202020;0.195;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-16";2111350;1037939;0.492;195789;0.189;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-15";2111350;1037451;0.491;190464;0.184;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-14";2111350;1037272;0.491;186579;0.18;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-13";2111350;1036969;0.491;181916;0.175;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-12";2111350;1036447;0.491;175609;0.169;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-11";2111350;1035955;0.491;167891;0.162;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-10";2111350;1035390;0.49;152405;0.147;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-09";2111350;1034771;0.49;132216;0.128;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-08";2111350;1034511;0.49;124335;0.12;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-07";2111350;1034336;0.49;118788;0.115;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-06";2111350;1034044;0.49;107871;0.104;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-05";2111350;1033640;0.49;98376;0.095;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-04";2111350;1033166;0.489;81444;0.079;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-03";2111350;1032627;0.489;67276;0.065;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-02";2111350;1032038;0.489;53611;0.052;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202603;"2026-03-01";2111350;1031337;0.488;28670;0.028;0.5286579372563773;0.6650577906177735;0.20509473805982192;1277504399.5958557;1976256535.1164815;0.646426401074777;302;1000;0.302;912;3000;0.304
|
||
202602;"2026-02-28";2115881;1030969;0.487;255186;0.248;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-27";2115881;1030503;0.487;250982;0.244;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-26";2115881;1029917;0.487;245809;0.239;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-25";2115881;1029314;0.486;240418;0.234;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-24";2115881;1028705;0.486;234498;0.228;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-23";2115881;1028056;0.486;223926;0.218;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-22";2115881;1027538;0.486;219188;0.213;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-21";2115881;1027337;0.486;215467;0.21;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-20";2115881;1026963;0.485;211510;0.206;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-19";2115881;1026457;0.485;206671;0.201;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-18";2115881;1026004;0.485;201382;0.196;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-17";2115881;1025488;0.485;195244;0.19;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-16";2115881;1025061;0.484;189087;0.184;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-15";2115881;1024577;0.484;184042;0.18;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-14";2115881;1024357;0.484;179979;0.176;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-13";2115881;1024081;0.484;175645;0.172;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-12";2115881;1023528;0.484;169492;0.166;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-11";2115881;1022916;0.483;162308;0.159;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-10";2115881;1022265;0.483;144628;0.141;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-09";2115881;1021630;0.483;120014;0.117;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-08";2115881;1021035;0.483;104867;0.103;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-07";2115881;1020800;0.482;98639;0.097;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-06";2115881;1020457;0.482;91218;0.089;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-05";2115881;1019955;0.482;81730;0.08;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-04";2115881;1019386;0.482;71492;0.07;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-03";2115881;1018799;0.482;60568;0.059;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-02";2115881;1018100;0.481;46952;0.046;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202602;"2026-02-01";2115881;1017353;0.481;28190;0.028;0.5281336608448282;0.6560082059673001;0.19492827065161145;823396217.71518;1139791017.7953122;0.7224098144832446;236;1000;0.236;610;2000;0.305
|
||
202601;"2026-01-31";2119709;1016924;0.48;258199;0.254;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-30";2119709;1016432;0.48;255143;0.251;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-29";2119709;1015746;0.479;251261;0.247;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-28";2119709;1015140;0.479;246361;0.243;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-27";2119709;1014520;0.479;242061;0.239;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-26";2119709;1013886;0.478;237580;0.234;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-25";2119709;1013248;0.478;232829;0.23;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-24";2119709;1012924;0.478;229318;0.226;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-23";2119709;1012500;0.478;222590;0.22;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-22";2119709;1011910;0.477;217002;0.214;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-21";2119709;1011318;0.477;211901;0.21;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-20";2119709;1010784;0.477;207205;0.205;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-19";2119709;1010244;0.477;202734;0.201;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-18";2119709;1009671;0.476;197504;0.196;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-17";2119709;1009441;0.476;194019;0.192;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-16";2119709;1009127;0.476;189558;0.188;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-15";2119709;1008640;0.476;184617;0.183;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-14";2119709;1008138;0.476;177007;0.176;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-13";2119709;1007587;0.475;171155;0.17;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-12";2119709;1007015;0.475;164480;0.163;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-11";2119709;1006355;0.475;156058;0.155;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-10";2119709;1006055;0.475;144870;0.144;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-09";2119709;1005603;0.474;125634;0.125;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-08";2119709;1004940;0.474;117022;0.116;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-07";2119709;1004268;0.474;107934;0.107;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-06";2119709;1003933;0.474;100992;0.101;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-05";2119709;1003255;0.473;89813;0.09;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-04";2119709;1002458;0.473;77277;0.077;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-03";2119709;1002142;0.473;68400;0.068;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-02";2119709;1001753;0.473;57710;0.058;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;374;1000;0.374;374;1000;0.374
|
||
202601;"2026-01-01";2119709;1001226;0.472;40373;0.04;0.5051958171773308;0.6601111271723942;0.2346806524208247;415145489.39247954;492169296.8361092;0.8435013968998591;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 = `https://git.vibe42.kz/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main/${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 шт.'},
|
||
];
|
||
|
||
// ═══════════════════ 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',()=>document.getElementById(`section-kpi-${kpi.id}`).scrollIntoView({behavior:'smooth',block:'start'}));
|
||
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:1800,
|
||
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);
|
||
});
|
||
});
|
||
|
||
// ═══════════════════ BOOT ═══════════════════
|
||
document.addEventListener('DOMContentLoaded', tryAutoLoad);
|
||
</script>
|
||
</body>
|
||
</html>
|