kpi-dashboard/index.html
kyrykbaev 93e055ea6a
Some checks failed
Deploy to Gitea Pages / deploy (push) Has been cancelled
center login form vertically and horizontally
2026-06-01 06:35:03 +00:00

1383 lines
96 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KPI InDigiCo — Казахтелеком 2026</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Токен DeepSeek: положите рядом файл config.js с содержимым: window.DS_KEY = 'sk-...' -->
<script src="config.js" onerror="console.info('config.js не найден')"></script>
<style>
:root {
--color-bg: #F4F6F9;
--color-surface: #FFFFFF;
--color-border: #E5E7EB;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-brand: #0052CC;
--color-brand-light: #EFF6FF;
--color-green: #10B981;
--color-green-bg: #ECFDF5;
--color-yellow: #F59E0B;
--color-yellow-bg: #FFFBEB;
--color-red: #EF4444;
--color-red-bg: #FEF2F2;
--color-orange: #F97316;
--color-orange-bg: #FFF7ED;
--radius-card: 12px;
--radius-btn: 8px;
--shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
--shadow-card-hover: 0 4px 12px rgba(0,0,0,0.1);
--font-base: 'Inter', system-ui, sans-serif;
--font-mono: 'Roboto Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-base); background: var(--color-bg); color: var(--color-text-primary); font-size: 14px; line-height: 1.5; }
/* ── LOADING / UPLOAD SCREENS ── */
#loading-screen {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%);
}
.loading-box { text-align: center; color: #fff; }
.spinner-lg { width: 48px; height: 48px; border: 4px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 20px; }
.loading-box p { font-size: 16px; font-weight: 500; opacity: 0.9; }
#upload-screen { min-height: 100vh; display: none; align-items: center; justify-content: center; background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%); }
#login-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #0052CC 0%, #0070F3 100%); }
.upload-box { background: #fff; border-radius: 20px; padding: 48px 56px; text-align: center; max-width: 480px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.15); }
.upload-logo { display: inline-flex; align-items: center; justify-content: center; width: 64px; height: 64px; background: var(--color-brand); color: #fff; font-size: 24px; font-weight: 700; border-radius: 16px; margin-bottom: 20px; }
.upload-box h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
.upload-box > p { color: var(--color-text-secondary); margin-bottom: 32px; font-size: 14px; }
.upload-note { margin-bottom: 24px; padding: 10px 14px; background: var(--color-yellow-bg); border-radius: 8px; font-size: 12px; color: #92400E; border: 1px solid rgba(245,158,11,0.3); text-align: left; line-height: 1.6; }
.upload-btn-label { display: inline-block; background: var(--color-brand); color: #fff; padding: 14px 32px; border-radius: var(--radius-btn); font-weight: 600; font-size: 15px; cursor: pointer; transition: background 0.2s; }
.upload-btn-label:hover { background: #003fa3; }
#file-input { display: none; }
.upload-hint { margin-top: 14px; font-size: 12px; color: var(--color-text-secondary); }
#upload-error { color: var(--color-red); margin-top: 10px; font-size: 13px; min-height: 18px; }
/* ── HEADER ── */
#dashboard { display: none; }
.header { background: var(--color-brand); color: #fff; padding: 0 24px; height: 64px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 8px rgba(0,82,204,0.3); }
.header-left { display: flex; align-items: center; gap: 14px; }
.header-logo { width: 40px; height: 40px; background: rgba(255,255,255,0.2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; flex-shrink: 0; }
.header-left h1 { font-size: 16px; font-weight: 700; line-height: 1.2; }
.header-left p { font-size: 12px; opacity: 0.75; }
.header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#last-updated { font-size: 12px; opacity: 0.85; }
.btn { padding: 7px 14px; border-radius: var(--radius-btn); font-size: 13px; font-weight: 500; cursor: pointer; border: none; font-family: var(--font-base); transition: all 0.15s; }
.btn-ghost { background: rgba(255,255,255,0.15); color: #fff; }
.btn-ghost:hover { background: rgba(255,255,255,0.25); }
.btn-outline { background: transparent; border: 1px solid rgba(255,255,255,0.4); color: #fff; }
.btn-outline:hover { background: rgba(255,255,255,0.1); }
/* ── SUMMARY BAR ── */
.summary-section { padding: 20px 24px 0; }
.summary-bar { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; }
.kpi-card { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px; box-shadow: var(--shadow-card); cursor: pointer; transition: box-shadow 0.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 -->
<!-- LOGIN -->
<div id="login-screen">
<div class="upload-box">
<div class="upload-logo">KT</div>
<h1>KPI InDigiCo</h1>
<p>Казахтелеком · 2026</p>
<input type="password" id="login-password" class="api-key-input" placeholder="Введите пароль" style="text-align:center;font-size:16px;margin-bottom:12px;">
<button class="btn-primary" id="btn-login" style="width:auto;padding:12px 40px;">Войти</button>
<p id="login-error" style="color:var(--color-red);font-size:13px;margin-top:10px;min-height:18px;"></p>
</div>
</div>
<div id="loading-screen" style="display:none">
<div class="loading-box">
<div class="spinner-lg"></div>
<p>Загрузка данных KPI...</p>
</div>
</div>
<!-- UPLOAD (резерв) -->
<div id="upload-screen">
<div class="upload-box">
<div class="upload-logo">KT</div>
<h1>KPI InDigiCo</h1>
<p id="upload-reason">Выберите CSV файл с данными KPI</p>
<div class="upload-note" id="upload-note" style="display:none"></div>
<label class="upload-btn-label" for="file-input">📂 Выбрать CSV файл</label>
<input type="file" id="file-input" accept=".csv">
<p class="upload-hint">Файл: <code>drb_iliyas_kpi_2026.csv</code> · Разделитель <code>;</code></p>
<p id="upload-error"></p>
</div>
</div>
<!-- DASHBOARD -->
<div id="dashboard">
<header class="header">
<div class="header-left">
<div class="header-logo">KT</div>
<div>
<h1>KPI InDigiCo</h1>
<p>Казахтелеком · 2026</p>
</div>
</div>
<div class="header-right">
<span id="last-updated"></span>
<button class="btn btn-ghost" id="btn-export-csv">↓ Скачать CSV</button>
<button class="btn btn-outline" id="btn-reload">Обновить данные</button>
</div>
</header>
<div class="summary-section"><div class="summary-bar" id="summary-bar"></div></div>
<div class="toolbar" id="toolbar"><span class="toolbar-label">Период:</span></div>
<div class="main-layout">
<div class="charts-grid">
<div class="chart-section" id="section-kpi-registrations">
<div class="chart-section-header">
<div><div class="chart-section-title">👤 KPI 1 — Регистрации</div><div class="chart-section-subtitle">Доля зарег. пользователей · Цель: 60%</div></div>
</div>
<div class="chart-canvas-wrap"><canvas id="chart-registrations"></canvas></div>
<div class="chart-risk-banner hidden" id="risk-registrations"></div>
</div>
<div class="chart-section" id="section-kpi-mau">
<div class="chart-section-header">
<div><div class="chart-section-title">📱 KPI 2 — MAU</div><div class="chart-section-subtitle">Monthly Active Users · Цель: 30%</div></div>
<div class="chart-tabs">
<button class="chart-tab active" id="tab-mau-monthly" onclick="switchMauTab('monthly')">По месяцам</button>
<button class="chart-tab" id="tab-mau-daily" onclick="switchMauTab('daily')">По дням</button>
</div>
</div>
<div class="chart-canvas-wrap"><canvas id="chart-mau"></canvas></div>
<div class="chart-risk-banner hidden" id="risk-mau"></div>
</div>
<div class="chart-section" id="section-kpi-traditional">
<div class="chart-section-header">
<div><div class="chart-section-title">📉 KPI 3 — Традиционные обращения</div><div class="chart-section-subtitle">Снижение доли обращений · Цель: 10%</div></div>
</div>
<div class="chart-canvas-wrap"><canvas id="chart-traditional"></canvas></div>
<div class="chart-risk-banner hidden" id="risk-traditional"></div>
</div>
<div class="chart-section" id="section-kpi-digital-sales">
<div class="chart-section-header">
<div><div class="chart-section-title">💰 KPI 4 — Цифровые продажи</div><div class="chart-section-subtitle">Доля цифровой выручки · Цель: 45%</div></div>
</div>
<div class="chart-canvas-wrap"><canvas id="chart-digital-sales"></canvas></div>
<div class="chart-risk-banner hidden" id="risk-digital-sales"></div>
</div>
<div class="chart-section" id="section-kpi-fd-orders">
<div class="chart-section-header">
<div><div class="chart-section-title">⚡ KPI 5 — Full Digital установки</div><div class="chart-section-subtitle">FD заказы · Цель: 1 000/мес · 12 000/год</div></div>
</div>
<div class="chart-canvas-wrap"><canvas id="chart-fd-orders"></canvas></div>
<div class="progress-bar-wrap">
<div class="progress-bar-labels">
<span>Годовой прогресс: <b id="cum-fd-text"></b></span>
<span id="cum-fd-pct" style="font-weight:600;font-family:var(--font-mono);"></span>
</div>
<div class="progress-track"><div class="progress-fill" id="cum-fd-bar" style="width:0%"></div></div>
</div>
<div class="fd-trend-info hidden" id="fd-trend-info"></div>
<div class="annotation-badge hidden" id="fd-annotation">⚠️ Показатель существенно ниже плана — цель требует пересмотра</div>
</div>
</div>
<!-- AI Panel -->
<div class="ai-panel">
<div class="ai-panel-title">🤖 AI-анализ KPI</div>
<!-- Cached response (shown to all users automatically) -->
<div class="ai-loading hidden" id="ai-loading"><div class="spinner"></div><span>Получаем анализ...</span></div>
<div class="ai-result hidden" id="ai-result">
<div class="ai-cache-meta">
<span id="ai-cache-date" style="font-size:11px;color:var(--color-text-secondary);">Загрузка...</span>
<span style="font-size:11px;color:var(--color-text-secondary);">· общий для всех</span>
</div>
<div id="ai-text"></div>
<button class="btn-copy" id="btn-ai-copy">📋 Скопировать</button>
</div>
<div class="ai-error hidden" id="ai-error"></div>
<!-- Update section: only useful if you have DeepSeek key -->
<div class="ai-divider"></div>
<details class="ai-update-details" id="ai-update-details">
<summary class="ai-update-summary">🔄 Обновить анализ</summary>
<div class="ai-api-row" style="margin-top:10px;">
<div id="ai-key-note" class="ai-key-note hidden">✓ DeepSeek токен загружен из config.js</div>
<input type="password" class="api-key-input" id="ai-api-key" placeholder="DeepSeek API ключ">
<button class="btn-primary" id="btn-ai-analyze">Запустить и сохранить для всех</button>
</div>
<p class="ai-hint" style="margin-top:8px;">Новый ответ сохранится в репо и сразу станет доступен всем пользователям. Обновляйте раз в месяц при новых данных.</p>
</details>
</div>
</div>
</div>
<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 = 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);
});
});
// ═══════════════════ LOGIN ═══════════════════
const LOGIN_PASSWORD = 'KTdash1';
function handleLogin() {
const input = document.getElementById('login-password');
const err = document.getElementById('login-error');
if (input.value === LOGIN_PASSWORD) {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('loading-screen').style.display = 'flex';
if (sessionStorage) sessionStorage.setItem('kpi_auth', '1');
tryAutoLoad();
} else {
err.textContent = 'Неверный пароль';
input.value = '';
}
}
document.getElementById('btn-login').addEventListener('click', handleLogin);
document.getElementById('login-password').addEventListener('keydown', function(e) {
if (e.key === 'Enter') handleLogin();
});
// ═══════════════════ BOOT ═══════════════════
document.addEventListener('DOMContentLoaded', function() {
if (sessionStorage && sessionStorage.getItem('kpi_auth') === '1') {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('loading-screen').style.display = 'flex';
tryAutoLoad();
}
});
</script>
</body>
</html>