add KPI modal popup with raw data charts and formulas
This commit is contained in:
parent
028ebe605f
commit
b1b3f4682a
178
index.html
178
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; } }
|
||||
</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()">×</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 →</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% × (Зарегистрировано / Всего абонентов 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% × (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: 'Снижение = (Доля трад. прошлого года − Доля трад. этого года) / Доля трад. прошлого года × 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% × (Цифровая выручка / Общая выручка)',
|
||||
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-услугам из канала продаж <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';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user