From b1b3f4682ac4eb7c8f86c771903002c5bfcdd585 Mon Sep 17 00:00:00 2001 From: kyrykbaev Date: Tue, 2 Jun 2026 10:55:50 +0000 Subject: [PATCH] add KPI modal popup with raw data charts and formulas --- index.html | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) 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';