add KPI modal popup with raw data charts and formulas

This commit is contained in:
kyrykbaev 2026-06-02 10:55:50 +00:00
parent 028ebe605f
commit b1b3f4682a

View File

@ -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; } }
</style>
</head>
<body>
@ -355,6 +376,25 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
</div>
</div>
<!-- KPI DETAIL MODAL -->
<div id="kpi-modal-overlay" onclick="closeKpiModal(event)">
<div id="kpi-modal" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modal-title"></h2>
<button class="modal-close" onclick="closeKpiModal()">&times;</button>
</div>
<div class="modal-body">
<div class="modal-info" id="modal-info"></div>
<div class="modal-formula" id="modal-formula"></div>
<a class="modal-link" id="modal-link" href="#" target="_blank" rel="noopener">📊 Открыть детальный отчёт в Qlik Sense &rarr;</a>
<div class="modal-charts">
<div class="modal-chart-box"><h4 id="modal-chart1-label"></h4><canvas id="modal-chart1"></canvas></div>
<div class="modal-chart-box"><h4 id="modal-chart2-label"></h4><canvas id="modal-chart2"></canvas></div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
@ -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% &times; (Зарегистрировано / Всего абонентов B2C)',
description: 'Используется вся активная абонентская база B2C компании Казахтелеком и полное количество зарегистрированных абонентов в системе <b>telecom.kz</b> за всё время.',
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
charts: [{field:'abons', label:'Абоненты B2C'}, {field:'registered_total', label:'Зарегистрировано'}],
},
mau: {
formula: 'MAU / Зарегистрированные = 100% &times; (MAU за месяц / Всего зарегистрированных)',
description: 'Используется полное количество зарегистрированных абонентов в системе <b>telecom.kz</b> и общее количество уникальных пользователей приложения <b>TelecomKz</b> за месяц.',
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
charts: [{field:'registered_total', label:'Зарегистрировано'}, {field:'mau_max', label:'MAU (макс. за месяц)'}],
},
traditional: {
formula: 'Снижение = (Доля трад. прошлого года &minus; Доля трад. этого года) / Доля трад. прошлого года &times; 100%',
description: 'Берётся доля традиционных обращений текущего года и доля традиционных обращений прошлого года. Количество заказов по традиционным каналам — из отчётов <b>Qlik Sense</b>, предоставленных отделом Customer Service.',
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/a343af17-0cfc-4538-81d6-ea87abb615a2/sheet/a08db3f1-bf5b-44d9-bc62-43386169c221/state/analysis',
charts: [{field:'traditional_comms_pct', label:'Традиционные обращения 2026 (%)', isPct:true}, {field:'prev_yesr_traditional_comms_pct', label:'Традиционные обращения 2025 (%)', isPct:true}],
},
'digital-sales': {
formula: 'Доля цифровых продаж = 100% &times; (Цифровая выручка / Общая выручка)',
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) &times; 100%',
description: 'Учитываются успешно закрытые заказы CRM по core-услугам из канала продаж <b>«Мобильное приложение B2C»</b>. Учитывается накопительный итог с начала года.',
qlik: 'https://app-qlik-ss03.cdn.telecom.kz/sense/app/203416cd-f1e2-44dd-8b73-7afeb0f8037e/sheet/XZANwR/state/analysis',
charts: [{field:'fd_orders', label:'FD заказы (мес.)'}, {field:'cum_fd_orders', label:'FD заказы (накоп.)'}],
},
};
// ═══════════════════ FORMATTING ═══════════════════
const fmtPct = (v, d=1) => (v*100).toFixed(d)+'%';
const fmtInt = n => new Intl.NumberFormat('ru-RU').format(Math.round(n));
@ -799,7 +872,7 @@ function renderSummaryBar() {
<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'}));
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} = <span class="formula-val">${fmtPct(val)}</span>`;
const linkEl = document.getElementById('modal-link');
linkEl.href = detail.qlik;
linkEl.style.display = detail.qlik ? 'inline-flex' : 'none';
document.getElementById('kpi-modal-overlay').classList.add('active');
document.body.style.overflow = 'hidden';
// Render charts after modal is visible
setTimeout(() => renderModalCharts(kpiId, detail), 100);
}
function closeKpiModal(e) {
if (e && e.target !== document.getElementById('kpi-modal-overlay')) return;
document.getElementById('kpi-modal-overlay').classList.remove('active');
document.body.style.overflow = '';
modalCharts.forEach(c => c.destroy());
modalCharts = [];
}
function getMauMaxPerMonth() {
return AppState.snapshots.map((s, i) => {
const pid = s.report_period_id;
const days = AppState.dailySeries[pid] || [];
if (days.length === 0) return s.mau_daily;
return Math.max(...days.map(d => d.mau_daily));
});
}
function renderModalCharts(kpiId, detail) {
modalCharts.forEach(c => c.destroy());
modalCharts = [];
const labels = AppState.snapshots.map(s => periodLabel(s.report_period_id));
const colors = ['#0052CC', '#F59E0B'];
detail.charts.forEach((ch, idx) => {
let data;
if (ch.field === 'mau_max') {
data = getMauMaxPerMonth();
} else {
data = AppState.snapshots.map(s => s[ch.field]);
}
const canvasId = `modal-chart${idx+1}`;
document.getElementById(`modal-chart${idx+1}-label`).textContent = ch.label;
const dataset = {
label: ch.label,
data,
backgroundColor: colors[idx]+'20',
borderColor: colors[idx],
borderWidth: 2,
pointRadius: 4,
pointBackgroundColor: colors[idx],
tension: 0.3,
};
if (ch.isPct) {
dataset.data = data.map(v => v !== null ? +(v*100).toFixed(2) : null);
}
const yOpts = {};
if (ch.isMoney) {
yOpts.ticks = {callback: v => fmtMoney(v)};
} else if (ch.isPct) {
yOpts.ticks = {callback: v => v+'%'};
} else if (data.some(v => v > 10000)) {
yOpts.ticks = {callback: v => fmtInt(v)};
}
const ctx = document.getElementById(canvasId).getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {labels, datasets: [dataset]},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {legend: {display: false}},
scales: {
y: {...yOpts, beginAtZero: false},
x: {ticks: {font: {size: 10}}},
},
},
});
modalCharts.push(chart);
});
}
// ═══════════════════ LOGIN ═══════════════════
const LOGIN_PASSWORD = 'KTdash1';