qa-20260912-p2-ya-daniyar-vy/index.html

345 lines
18 KiB
HTML
Raw 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" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Business Metrics — Контроль качества</title>
<link rel="stylesheet" href="./design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
<style>
.kt-ai-app { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); min-height: 100vh; }
.metrics-header { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) 0; border-bottom: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg); position: sticky; top: 0; z-index: 10; }
.metrics-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.filters-row { display: flex; gap: var(--kt-ai-space-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
.kpi-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); font-family: var(--kt-ai-font-mono); color: var(--kt-ai-fg); }
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
.kpi-hint { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
.hint-positive { color: var(--kt-ai-chart-green); }
.hint-negative { color: var(--kt-ai-chart-red); }
.hint-neutral { color: var(--kt-ai-fg-faint); }
.chart-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-6); }
.chart-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.chart-container { height: 200px; display: flex; align-items: flex-end; gap: 8px; padding: var(--kt-ai-space-4) 0; }
.chart-bar { flex: 1; background: var(--kt-ai-chart-blue); border-radius: var(--kt-ai-radius-md); min-width: 24px; position: relative; cursor: pointer; transition: opacity 0.2s; }
.chart-bar:hover { opacity: 0.8; }
.chart-bar[data-kt-tip]::after { content: attr(data-kt-tip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--kt-ai-fg); color: var(--kt-ai-bg); padding: 4px 8px; border-radius: var(--kt-ai-radius-md); font-size: var(--kt-ai-text-xs); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; margin-bottom: 4px; }
.chart-bar:hover::after { opacity: 1; }
.table-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); overflow: hidden; margin-bottom: var(--kt-ai-space-6); }
.kt-ai-table { width: 100%; border-collapse: collapse; }
.kt-ai-table th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); border-bottom: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg-soft); }
.kt-ai-table td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); border-bottom: 1px solid var(--kt-ai-border); }
.kt-ai-table tr:last-child td { border-bottom: none; }
.kt-ai-table td.num { text-align: right; font-family: var(--kt-ai-font-mono); tabular-nums: 1; }
.insights-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-6); }
.insights-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.insight-card { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-lg); padding: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-3); }
.insight-card:last-child { margin-bottom: 0; }
.insight-label { font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: var(--kt-ai-space-2); }
.insight-text { font-size: var(--kt-ai-text-sm); line-height: 1.5; }
.recommendation-list { list-style: none; padding: 0; margin: 0; }
.recommendation-list li { position: relative; padding-left: var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-2); font-size: var(--kt-ai-text-sm); line-height: 1.5; }
.recommendation-list li::before { content: "→"; position: absolute; left: 0; color: var(--kt-ai-primary); font-weight: var(--kt-ai-weight-bold); }
.no-data { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-lg); }
</style>
</head>
<body class="kt-ai-app">
<div class="kt-ai-wrap">
<header class="metrics-header">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<span class="metrics-title">Business Metrics</span>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="resetBtn">Сбросить фильтры</button>
</header>
<main style="padding: var(--kt-ai-space-6) 0;">
<div class="filters-row">
<div class="kt-ai-field">
<label style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); display: block; margin-bottom: var(--kt-ai-space-1);">Сегмент</label>
<select class="kt-ai-select" id="segmentFilter" style="min-width: 140px;">
<option value="all">Все</option>
<option value="B2B">B2B</option>
<option value="B2C">B2C</option>
</select>
</div>
<div class="kt-ai-field">
<label style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); display: block; margin-bottom: var(--kt-ai-space-1);">Канал</label>
<select class="kt-ai-select" id="channelFilter" style="min-width: 160px;">
<option value="all">Все каналы</option>
<option value="call-center">Call-центр</option>
<option value="online">Онлайн-кабинет</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="office">Офисы</option>
</select>
</div>
<div class="kt-ai-field">
<label style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); display: block; margin-bottom: var(--kt-ai-space-1);">Период</label>
<select class="kt-ai-select" id="periodFilter" style="min-width: 140px;">
<option value="7d">7 дней</option>
<option value="30d">30 дней</option>
<option value="90d">90 дней</option>
<option value="year">Год</option>
</select>
</div>
</div>
<div class="kpi-strip" id="kpiStrip">
<div class="kpi-card">
<div class="kpi-value" id="kpiNPS">42</div>
<div class="kpi-label">NPS</div>
<div class="kpi-hint hint-positive">+3 за период</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiCSAT">87%</div>
<div class="kpi-label">CSAT</div>
<div class="kpi-hint hint-neutral">Без изменений</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiFCR">76%</div>
<div class="kpi-label">FCR</div>
<div class="kpi-hint hint-negative">−2 за период</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiAHT">4.2 мин</div>
<div class="kpi-label">AHT</div>
<div class="kpi-hint hint-positive">−0.3 мин</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiResolution">94%</div>
<div class="kpi-label">Решено с первого раза</div>
<div class="kpi-hint hint-neutral">Без изменений</div>
</div>
</div>
<div class="chart-wrap">
<div class="chart-title">Динамика NPS по периодам</div>
<div class="chart-container" id="chartContainer"></div>
</div>
<div class="table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>Канал</th>
<th>Сегмент</th>
<th class="num">NPS</th>
<th class="num">CSAT</th>
<th class="num">FCR</th>
<th class="num">AHT</th>
<th class="num">Заявок</th>
</tr>
</thead>
<tbody id="metricsTable"></tbody>
</table>
</div>
<div class="insights-wrap">
<div class="insights-title">Анализ отклонений</div>
<div id="insightsContent">
<div class="insight-card">
<div class="insight-label">Объяснение отклонений</div>
<div class="insight-text" id="explanationText">Загрузка...</div>
</div>
<div class="insight-card">
<div class="insight-label">Рекомендации</div>
<ul class="recommendation-list" id="recommendationsList"></ul>
</div>
<div class="insight-card">
<div class="insight-label">Исходные показатели</div>
<div class="insight-text" id="sourceLink">Загрузка...</div>
</div>
</div>
</div>
<div id="noDataBlock" class="no-data" style="display: none;">
Недостаточно данных для выбранных фильтров
</div>
</main>
</div>
<script src="./design-system/kt-ai-chart-tip.js"></script>
<script>
const DEMO_DATA = {
metrics: [
{ channel: 'call-center', segment: 'B2B', nps: 38, csat: 84, fcr: 72, aht: 5.2, requests: 1240 },
{ channel: 'call-center', segment: 'B2C', nps: 45, csat: 89, fcr: 78, aht: 3.8, requests: 3420 },
{ channel: 'online', segment: 'B2B', nps: 52, csat: 92, fcr: 85, aht: 2.1, requests: 890 },
{ channel: 'online', segment: 'B2C', nps: 48, csat: 88, fcr: 80, aht: 2.5, requests: 2150 },
{ channel: 'mobile-app', segment: 'B2B', nps: 44, csat: 86, fcr: 75, aht: 3.2, requests: 560 },
{ channel: 'mobile-app', segment: 'B2C', nps: 50, csat: 91, fcr: 82, aht: 2.8, requests: 1890 },
{ channel: 'office', segment: 'B2B', nps: 35, csat: 80, fcr: 68, aht: 6.5, requests: 320 },
{ channel: 'office', segment: 'B2C', nps: 40, csat: 85, fcr: 74, aht: 4.5, requests: 980 }
],
history: [
{ period: 'Янв', nps: 39 }, { period: 'Фев', nps: 41 }, { period: 'Мар', nps: 38 },
{ period: 'Апр', nps: 43 }, { period: 'Май', nps: 45 }, { period: 'Июн', nps: 42 },
{ period: 'Июл', nps: 44 }, { period: 'Авг', nps: 46 }, { period: 'Сен', nps: 42 },
{ period: 'Окт', nps: 40 }, { period: 'Ноя', nps: 43 }, { period: 'Дек', nps: 42 }
],
insights: {
B2B: {
explanation: 'Снижение FCR в B2B-сегменте связано с увеличением сложных технических запросов. AHT выше среднего из-за длительной обработки заявок в офисах.',
recommendations: [
'Усилить техническую подготовку операторов B2B-направления',
'Внедрить шаблоны ответов для типовых сложных запросов',
'Сократить время обработки в офисах через цифровизацию процессов'
],
source: 'Показатели взяты из системы мониторинга качества за 4 квартал 2025'
},
B2C: {
explanation: 'B2C-сегмент показывает стабильные результаты. Небольшое снижение NPS в сентябре связано с сезонным ростом нагрузки.',
recommendations: [
'Поддерживать текущий уровень сервиса в онлайн-каналах',
'Провести работу над ошибками в call-центре для роста FCR'
],
source: 'Показатели взяты из системы мониторинга качества за 4 квартал 2025'
},
all: {
explanation: 'Общие показатели в пределах нормы. Наблюдается положительная динамика NPS за последние 3 месяца.',
recommendations: [
'Продолжить развитие онлайн-каналов обслуживания',
'Провести обучение операторов call-центра',
'Внедрить единую базу знаний для всех каналов'
],
source: 'Показатели взяты из системы мониторинга качества за 4 квартал 2025'
}
}
};
let currentFilters = { segment: 'all', channel: 'all', period: '7d' };
function loadFilters() {
const saved = localStorage.getItem('bm_filters');
if (saved) {
currentFilters = JSON.parse(saved);
document.getElementById('segmentFilter').value = currentFilters.segment;
document.getElementById('channelFilter').value = currentFilters.channel;
document.getElementById('periodFilter').value = currentFilters.period;
}
}
function saveFilters() {
localStorage.setItem('bm_filters', JSON.stringify(currentFilters));
}
function getFilteredData() {
let data = DEMO_DATA.metrics;
if (currentFilters.segment !== 'all') {
data = data.filter(m => m.segment === currentFilters.segment);
}
if (currentFilters.channel !== 'all') {
data = data.filter(m => m.channel === currentFilters.channel);
}
return data;
}
function calculateAverages(data) {
if (data.length === 0) return null;
const sum = data.reduce((acc, m) => ({
nps: acc.nps + m.nps,
csat: acc.csat + m.csat,
fcr: acc.fcr + m.fcr,
aht: acc.aht + m.aht,
requests: acc.requests + m.requests
}), { nps: 0, csat: 0, fcr: 0, aht: 0, requests: 0 });
return {
nps: Math.round(sum.nps / data.length),
csat: Math.round(sum.csat / data.length),
fcr: Math.round(sum.fcr / data.length),
aht: (sum.aht / data.length).toFixed(1),
requests: sum.requests
};
}
function renderKPIs(avgs) {
if (!avgs) {
document.getElementById('kpiStrip').style.display = 'none';
return;
}
document.getElementById('kpiStrip').style.display = 'grid';
document.getElementById('kpiNPS').textContent = avgs.nps;
document.getElementById('kpiCSAT').textContent = avgs.csat + '%';
document.getElementById('kpiFCR').textContent = avgs.fcr + '%';
document.getElementById('kpiAHT').textContent = avgs.aht + ' мин';
document.getElementById('kpiResolution').textContent = avgs.fcr + '%';
}
function renderChart() {
const container = document.getElementById('chartContainer');
const maxNPS = Math.max(...DEMO_DATA.history.map(h => h.nps));
container.innerHTML = DEMO_DATA.history.map((h, i) => {
const height = (h.nps / maxNPS) * 160;
const colorIndex = i % 6;
const colors = ['var(--kt-ai-chart-blue)', 'var(--kt-ai-chart-green)', 'var(--kt-ai-chart-orange)', 'var(--kt-ai-chart-pink)', 'var(--kt-ai-chart-red)', 'var(--kt-ai-chart-salmon)'];
return `<div class="chart-bar" style="height: ${height}px; background: ${colors[colorIndex]}" data-kt-tip="${h.period}: ${h.nps}"></div>`;
}).join('');
}
function renderTable(data) {
const tbody = document.getElementById('metricsTable');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="num" style="text-align: center; padding: var(--kt-ai-space-6);">Нет данных</td></tr>';
return;
}
tbody.innerHTML = data.map(m => `
<tr>
<td>${m.channel === 'call-center' ? 'Call-центр' : m.channel === 'online' ? 'Онлайн-кабинет' : m.channel === 'mobile-app' ? 'Мобильное приложение' : 'Офисы'}</td>
<td>${m.segment}</td>
<td class="num">${m.nps}</td>
<td class="num">${m.csat}%</td>
<td class="num">${m.fcr}%</td>
<td class="num">${m.aht}</td>
<td class="num">${m.requests}</td>
</tr>
`).join('');
}
function renderInsights(segment) {
const insight = DEMO_DATA.insights[segment] || DEMO_DATA.insights.all;
document.getElementById('explanationText').textContent = insight.explanation;
document.getElementById('recommendationsList').innerHTML = insight.recommendations.map(r => `<li>${r}</li>`).join('');
document.getElementById('sourceLink').textContent = insight.source;
}
function render() {
const data = getFilteredData();
const avgs = calculateAverages(data);
renderKPIs(avgs);
renderChart();
renderTable(data);
renderInsights(currentFilters.segment);
document.getElementById('noDataBlock').style.display = data.length === 0 ? 'block' : 'none';
}
function onFilterChange() {
currentFilters = {
segment: document.getElementById('segmentFilter').value,
channel: document.getElementById('channelFilter').value,
period: document.getElementById('periodFilter').value
};
saveFilters();
render();
}
document.getElementById('segmentFilter').addEventListener('change', onFilterChange);
document.getElementById('channelFilter').addEventListener('change', onFilterChange);
document.getElementById('periodFilter').addEventListener('change', onFilterChange);
document.getElementById('resetBtn').addEventListener('click', () => {
currentFilters = { segment: 'all', channel: 'all', period: '7d' };
document.getElementById('segmentFilter').value = 'all';
document.getElementById('channelFilter').value = 'all';
document.getElementById('periodFilter').value = '7d';
saveFilters();
render();
});
loadFilters();
render();
</script>
</body>
</html>