diff --git a/index.html b/index.html
index 25b1043..f39cde0 100644
--- a/index.html
+++ b/index.html
@@ -75,8 +75,8 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
/* ── 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 { background: var(--color-surface); border-radius: var(--radius-card); padding: 16px; box-shadow: var(--shadow-card); cursor: pointer; transition: box-shadow 0.25s, transform 0.25s; border-top: 4px solid var(--color-border); }
+.kpi-card:hover { box-shadow: var(--shadow-card-hover); transform: scale(1.03); }
.kpi-card.status-green { border-top-color: var(--color-green); }
.kpi-card.status-yellow { border-top-color: var(--color-yellow); }
.kpi-card.status-red { border-top-color: var(--color-red); }
@@ -209,6 +209,27 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
@media (max-width: 1024px) { .main-layout { grid-template-columns: 1fr; } .ai-panel { position: static; } }
@media (max-width: 768px) { .charts-grid { grid-template-columns: 1fr; } .charts-grid .chart-section:nth-child(5) { grid-column: auto; } .summary-bar { grid-template-columns: 1fr 1fr; } .main-layout,.summary-section { padding-left: 12px; padding-right: 12px; } .toolbar { padding: 10px 12px; } }
@media (max-width: 480px) { .summary-bar { grid-template-columns: 1fr; } .header-left h1 { font-size: 14px; } }
+
+/* ── KPI MODAL ── */
+#kpi-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 999; display: none; align-items: center; justify-content: center; }
+#kpi-modal-overlay.active { display: flex; }
+#kpi-modal { background: var(--color-surface); border-radius: 16px; max-width: 840px; width: 94%; max-height: 92vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25); }
+.modal-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 24px 24px 0; }
+.modal-header h2 { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
+.modal-close { background: var(--color-bg); color: var(--color-text-secondary); border: 1px solid var(--color-border); width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 18px; flex-shrink: 0; transition: all 0.15s; }
+.modal-close:hover { background: var(--color-red-bg); color: var(--color-red); border-color: var(--color-red); }
+.modal-body { padding: 20px 24px 24px; }
+.modal-info { font-size: 13px; line-height: 1.7; color: var(--color-text-secondary); margin-bottom: 20px; }
+.modal-info b { color: var(--color-text-primary); }
+.modal-formula { background: var(--color-brand-light); border: 1px solid rgba(0,82,204,0.15); border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; font-family: var(--font-mono); font-size: 13px; }
+.modal-formula .formula-val { color: var(--color-brand); font-weight: 700; }
+.modal-link { display: inline-flex; align-items: center; gap: 6px; color: var(--color-brand); font-size: 12px; font-weight: 500; text-decoration: none; margin-bottom: 20px; }
+.modal-link:hover { text-decoration: underline; }
+.modal-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
+.modal-chart-box { background: var(--color-bg); border-radius: 10px; padding: 14px; }
+.modal-chart-box h4 { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.03em; }
+.modal-chart-box canvas { max-height: 210px; }
+@media (max-width: 640px) { .modal-charts { grid-template-columns: 1fr; } }
@@ -355,6 +376,25 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
+
+
+
@@ -549,6 +589,39 @@ const KPI_CONFIG = [
{ id:'fd-orders', name:'FD установки', icon:'⚡', field:'cum_fd_orders_pct', target:1.00, targetLabel:'12 000 шт.'},
];
+const KPI_DETAIL = {
+ registrations: {
+ formula: 'Доля регистраций = 100% × (Зарегистрировано / Всего абонентов B2C)',
+ description: 'Используется вся активная абонентская база B2C компании Казахтелеком и полное количество зарегистрированных абонентов в системе telecom.kz за всё время.',
+ qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
+ charts: [{field:'abons', label:'Абоненты B2C'}, {field:'registered_total', label:'Зарегистрировано'}],
+ },
+ mau: {
+ formula: 'MAU / Зарегистрированные = 100% × (MAU за месяц / Всего зарегистрированных)',
+ description: 'Используется полное количество зарегистрированных абонентов в системе telecom.kz и общее количество уникальных пользователей приложения TelecomKz за месяц.',
+ qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
+ charts: [{field:'registered_total', label:'Зарегистрировано'}, {field:'mau_max', label:'MAU (макс. за месяц)'}],
+ },
+ traditional: {
+ formula: 'Снижение = (Доля трад. прошлого года − Доля трад. этого года) / Доля трад. прошлого года × 100%',
+ description: 'Берётся доля традиционных обращений текущего года и доля традиционных обращений прошлого года. Количество заказов по традиционным каналам — из отчётов Qlik Sense, предоставленных отделом Customer Service.',
+ qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
+ charts: [{field:'traditional_comms_pct', label:'Традиционные обращения 2026 (%)', isPct:true}, {field:'prev_yesr_traditional_comms_pct', label:'Традиционные обращения 2025 (%)', isPct:true}],
+ },
+ 'digital-sales': {
+ formula: 'Доля цифровых продаж = 100% × (Цифровая выручка / Общая выручка)',
+ description: 'Берутся доходы по всем каналам продаж компании и вычисляется доля цифровых каналов. Детальная разбивка по каждому каналу — в отчёте Qlik Sense.',
+ qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/203416cd-f1e2-44dd-8b73-7afeb0f8037e/sheet/ajCCVt/state/analysis',
+ charts: [{field:'cumulative_digital_rap_total', label:'Цифровая выручка', isMoney:true}, {field:'cumulative_rap_total', label:'Общая выручка', isMoney:true}],
+ },
+ 'fd-orders': {
+ formula: 'Годовой план FD = Накоплено заказов / Годовая цель (12 000) × 100%',
+ description: 'Учитываются успешно закрытые заказы CRM по core-услугам из канала продаж «Мобильное приложение B2C». Учитывается накопительный итог с начала года.',
+ qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/203416cd-f1e2-44dd-8b73-7afeb0f8037e/sheet/XZANwR/state/analysis',
+ charts: [{field:'fd_orders', label:'FD заказы (мес.)'}, {field:'cum_fd_orders', label:'FD заказы (накоп.)'}],
+ },
+};
+
// ═══════════════════ FORMATTING ═══════════════════
const fmtPct = (v, d=1) => (v*100).toFixed(d)+'%';
const fmtInt = n => new Intl.NumberFormat('ru-RU').format(Math.round(n));
@@ -799,7 +872,7 @@ function renderSummaryBar() {
Цель: ${kpi.targetLabel}${delta}
${riskText}
`;
- card.addEventListener('click',()=>document.getElementById(`section-kpi-${kpi.id}`).scrollIntoView({behavior:'smooth',block:'start'}));
+ card.addEventListener('click',()=>openKpiModal(kpi.id));
bar.appendChild(card);
const sparkColor={green:'#10B981',orange:'#F97316',yellow:'#F59E0B',red:'#EF4444'}[status];
@@ -1349,6 +1422,105 @@ document.getElementById('btn-ai-copy').addEventListener('click',()=>{
});
});
+// ═══════════════════ KPI MODAL ═══════════════════
+let modalCharts = [];
+
+function openKpiModal(kpiId) {
+ const kpi = KPI_CONFIG.find(k=>k.id===kpiId);
+ const detail = KPI_DETAIL[kpiId];
+ const last = AppState.snapshots[AppState.snapshots.length-1];
+ const val = last[kpi.field];
+
+ document.getElementById('modal-title').innerHTML = `${kpi.icon} ${kpi.name}`;
+ document.getElementById('modal-info').innerHTML = detail.description;
+ document.getElementById('modal-formula').innerHTML =
+ `${detail.formula} = ${fmtPct(val)}`;
+ const linkEl = document.getElementById('modal-link');
+ linkEl.href = detail.qlik;
+ linkEl.style.display = detail.qlik ? 'inline-flex' : 'none';
+
+ document.getElementById('kpi-modal-overlay').classList.add('active');
+ document.body.style.overflow = 'hidden';
+
+ // Render charts after modal is visible
+ setTimeout(() => renderModalCharts(kpiId, detail), 100);
+}
+
+function closeKpiModal(e) {
+ if (e && e.target !== document.getElementById('kpi-modal-overlay')) return;
+ document.getElementById('kpi-modal-overlay').classList.remove('active');
+ document.body.style.overflow = '';
+ modalCharts.forEach(c => c.destroy());
+ modalCharts = [];
+}
+
+function getMauMaxPerMonth() {
+ return AppState.snapshots.map((s, i) => {
+ const pid = s.report_period_id;
+ const days = AppState.dailySeries[pid] || [];
+ if (days.length === 0) return s.mau_daily;
+ return Math.max(...days.map(d => d.mau_daily));
+ });
+}
+
+function renderModalCharts(kpiId, detail) {
+ modalCharts.forEach(c => c.destroy());
+ modalCharts = [];
+ const labels = AppState.snapshots.map(s => periodLabel(s.report_period_id));
+ const colors = ['#0052CC', '#F59E0B'];
+
+ detail.charts.forEach((ch, idx) => {
+ let data;
+ if (ch.field === 'mau_max') {
+ data = getMauMaxPerMonth();
+ } else {
+ data = AppState.snapshots.map(s => s[ch.field]);
+ }
+ const canvasId = `modal-chart${idx+1}`;
+
+ document.getElementById(`modal-chart${idx+1}-label`).textContent = ch.label;
+
+ const dataset = {
+ label: ch.label,
+ data,
+ backgroundColor: colors[idx]+'20',
+ borderColor: colors[idx],
+ borderWidth: 2,
+ pointRadius: 4,
+ pointBackgroundColor: colors[idx],
+ tension: 0.3,
+ };
+
+ if (ch.isPct) {
+ dataset.data = data.map(v => v !== null ? +(v*100).toFixed(2) : null);
+ }
+
+ const yOpts = {};
+ if (ch.isMoney) {
+ yOpts.ticks = {callback: v => fmtMoney(v)};
+ } else if (ch.isPct) {
+ yOpts.ticks = {callback: v => v+'%'};
+ } else if (data.some(v => v > 10000)) {
+ yOpts.ticks = {callback: v => fmtInt(v)};
+ }
+
+ const ctx = document.getElementById(canvasId).getContext('2d');
+ const chart = new Chart(ctx, {
+ type: 'line',
+ data: {labels, datasets: [dataset]},
+ options: {
+ responsive: true, maintainAspectRatio: false,
+ plugins: {legend: {display: false}},
+ scales: {
+ y: {...yOpts, beginAtZero: false},
+ x: {ticks: {font: {size: 10}}},
+ },
+ },
+ });
+ modalCharts.push(chart);
+ });
+}
+
// ═══════════════════ LOGIN ═══════════════════
const LOGIN_PASSWORD = 'KTdash1';