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

455 lines
24 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); }
.demo-badge { background: var(--kt-ai-chart-orange); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-bold); padding: 4px 10px; border-radius: var(--kt-ai-radius-full); text-transform: uppercase; letter-spacing: 0.5px; }
.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="demo-badge">Демонстрационные данные</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>
</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="dataModeFilter" style="min-width: 160px;">
<option value="data">С данными</option>
<option value="nodata">Нет данных</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>
</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 = {
'7d': {
metrics: [
{ channel: 'call-center', segment: 'B2B', nps: 38, csat: 84, fcr: 72, aht: 5.2, requests: 320 },
{ channel: 'call-center', segment: 'B2C', nps: 45, csat: 89, fcr: 78, aht: 3.8, requests: 890 },
{ channel: 'online', segment: 'B2B', nps: 52, csat: 92, fcr: 85, aht: 2.1, requests: 240 },
{ channel: 'online', segment: 'B2C', nps: 48, csat: 88, fcr: 80, aht: 2.5, requests: 560 },
{ channel: 'mobile-app', segment: 'B2B', nps: 44, csat: 86, fcr: 75, aht: 3.2, requests: 150 },
{ channel: 'mobile-app', segment: 'B2C', nps: 50, csat: 91, fcr: 82, aht: 2.8, requests: 480 },
{ channel: 'office', segment: 'B2B', nps: 35, csat: 80, fcr: 68, aht: 6.5, requests: 85 },
{ channel: 'office', segment: 'B2C', nps: 40, csat: 85, fcr: 74, aht: 4.5, requests: 260 }
],
history: [
{ period: 'Пн', nps: 40 }, { period: 'Вт', nps: 42 }, { period: 'Ср', nps: 39 },
{ period: 'Чт', nps: 44 }, { period: 'Пт', nps: 43 }, { period: 'Сб', nps: 41 }, { period: 'Вс', nps: 42 }
],
insights: {
B2B: {
explanation: 'Возможно, снижение FCR в B2B связано с увеличением сложных технических запросов на этой неделе. AHT выше среднего из-за длительной обработки в офисах.',
recommendations: [
'Усилить техническую подготовку операторов B2B-направления',
'Внедрить шаблоны ответов для типовых сложных запросов'
]
},
B2C: {
explanation: 'Возможно, небольшое снижение NPS в выходные связано с ростом нагрузки в выходные дни.',
recommendations: [
'Поддерживать текущий уровень сервиса в онлайн-каналах',
'Провести работу над ошибками в call-центре для роста FCR'
]
},
all: {
explanation: 'Общие показатели в пределах нормы. Наблюдается стабильная динамика NPS в течение недели.',
recommendations: [
'Продолжить развитие онлайн-каналов обслуживания',
'Провести обучение операторов call-центра'
]
}
}
},
'30d': {
metrics: [
{ channel: 'call-center', segment: 'B2B', nps: 40, csat: 85, fcr: 74, aht: 5.0, requests: 1240 },
{ channel: 'call-center', segment: 'B2C', nps: 47, csat: 90, fcr: 79, aht: 3.6, requests: 3420 },
{ channel: 'online', segment: 'B2B', nps: 54, csat: 93, fcr: 86, aht: 2.0, requests: 890 },
{ channel: 'online', segment: 'B2C', nps: 50, csat: 89, fcr: 81, aht: 2.4, requests: 2150 },
{ channel: 'mobile-app', segment: 'B2B', nps: 46, csat: 87, fcr: 76, aht: 3.0, requests: 560 },
{ channel: 'mobile-app', segment: 'B2C', nps: 52, csat: 92, fcr: 83, aht: 2.6, requests: 1890 },
{ channel: 'office', segment: 'B2B', nps: 37, csat: 81, fcr: 70, aht: 6.2, requests: 320 },
{ channel: 'office', segment: 'B2C', nps: 42, csat: 86, fcr: 75, aht: 4.3, requests: 980 }
],
history: [
{ period: 'Нед 1', nps: 39 }, { period: 'Нед 2', nps: 42 }, { period: 'Нед 3', nps: 44 },
{ period: 'Нед 4', nps: 43 }
],
insights: {
B2B: {
explanation: 'Возможно, снижение FCR в B2B-сегменте связано с увеличением сложных технических запросов в этом месяце.',
recommendations: [
'Усилить техническую подготовку операторов B2B-направления',
'Внедрить шаблоны ответов для типовых сложных запросов',
'Сократить время обработки в офисах через цифровизацию'
]
},
B2C: {
explanation: 'B2C-сегмент показывает стабильные результаты. Возможно, небольшое снижение NPS связано с сезонным ростом нагрузки.',
recommendations: [
'Поддерживать текущий уровень сервиса в онлайн-каналах',
'Провести работу над ошибками в call-центре'
]
},
all: {
explanation: 'Общие показатели в пределах нормы. Наблюдается положительная динамика NPS за месяц.',
recommendations: [
'Продолжить развитие онлайн-каналов обслуживания',
'Провести обучение операторов call-центра',
'Внедрить единую базу знаний для всех каналов'
]
}
}
},
'90d': {
metrics: [
{ channel: 'call-center', segment: 'B2B', nps: 41, csat: 86, fcr: 75, aht: 4.9, requests: 3800 },
{ channel: 'call-center', segment: 'B2C', nps: 48, csat: 91, fcr: 80, aht: 3.5, requests: 10200 },
{ channel: 'online', segment: 'B2B', nps: 55, csat: 94, fcr: 87, aht: 1.9, requests: 2700 },
{ channel: 'online', segment: 'B2C', nps: 51, csat: 90, fcr: 82, aht: 2.3, requests: 6400 },
{ channel: 'mobile-app', segment: 'B2B', nps: 47, csat: 88, fcr: 77, aht: 2.9, requests: 1700 },
{ channel: 'mobile-app', segment: 'B2C', nps: 53, csat: 93, fcr: 84, aht: 2.5, requests: 5600 },
{ channel: 'office', segment: 'B2B', nps: 38, csat: 82, fcr: 71, aht: 6.0, requests: 950 },
{ channel: 'office', segment: 'B2C', nps: 43, csat: 87, fcr: 76, aht: 4.2, requests: 2900 }
],
history: [
{ period: 'Июл', nps: 44 }, { period: 'Авг', nps: 46 }, { period: 'Сен', nps: 42 }
],
insights: {
B2B: {
explanation: 'Возможно, снижение FCR в B2B-сегменте за квартал связано с увеличением объёма сложных технических запросов.',
recommendations: [
'Усилить техническую подготовку операторов B2B-направления',
'Внедрить шаблоны ответов для типовых сложных запросов',
'Сократить время обработки в офисах через цифровизацию процессов'
]
},
B2C: {
explanation: 'B2C-сегмент показывает стабильные результаты за квартал. Возможно, снижение NPS в сентябре связано с сезонным фактором.',
recommendations: [
'Поддерживать текущий уровень сервиса в онлайн-каналах',
'Провести работу над ошибками в call-центре для роста FCR'
]
},
all: {
explanation: 'Общие показатели в пределах нормы. Наблюдается положительная динамика NPS за последние 3 месяца.',
recommendations: [
'Продолжить развитие онлайн-каналов обслуживания',
'Провести обучение операторов call-центра',
'Внедрить единую базу знаний для всех каналов'
]
}
}
}
};
let currentFilters = { segment: 'all', channel: 'all', period: '7d', dataMode: 'data' };
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;
document.getElementById('dataModeFilter').value = currentFilters.dataMode;
}
}
function saveFilters() {
localStorage.setItem('bm_filters', JSON.stringify(currentFilters));
}
function getFilteredData() {
const periodData = DEMO_DATA[currentFilters.period] || DEMO_DATA['7d'];
let data = periodData.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 getHistory() {
const periodData = DEMO_DATA[currentFilters.period] || DEMO_DATA['7d'];
return periodData.history;
}
function getInsights(segment) {
const periodData = DEMO_DATA[currentFilters.period] || DEMO_DATA['7d'];
return periodData.insights[segment] || periodData.insights.all;
}
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 history = getHistory();
if (!history || history.length === 0) {
container.innerHTML = '<div class="no-data" style="padding: var(--kt-ai-space-10);">Недостаточно данных</div>';
return;
}
const maxNPS = Math.max(...history.map(h => h.nps));
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)'];
container.innerHTML = history.map((h, i) => {
const height = (h.nps / maxNPS) * 160;
return `<div class="chart-bar" style="height: ${height}px; background: ${colors[i % colors.length]}" 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) {
if (currentFilters.dataMode === 'nodata') {
document.getElementById('insightsContent').style.display = 'none';
return;
}
document.getElementById('insightsContent').style.display = 'block';
const insight = getInsights(segment);
document.getElementById('explanationText').textContent = insight.explanation;
document.getElementById('recommendationsList').innerHTML = insight.recommendations.map(r => `<li>${r}</li>`).join('');
}
function render() {
const noData = currentFilters.dataMode === 'nodata';
document.getElementById('kpiStrip').style.display = noData ? 'none' : 'grid';
document.getElementById('noDataBlock').style.display = noData ? 'block' : 'none';
document.getElementById('metricsTable').closest('.table-wrap').style.display = noData ? 'none' : 'block';
if (noData) {
renderChart();
renderInsights(null);
return;
}
const data = getFilteredData();
const avgs = calculateAverages(data);
renderKPIs(avgs);
renderChart();
renderTable(data);
renderInsights(currentFilters.segment);
}
function onFilterChange() {
currentFilters = {
segment: document.getElementById('segmentFilter').value,
channel: document.getElementById('channelFilter').value,
period: document.getElementById('periodFilter').value,
dataMode: document.getElementById('dataModeFilter').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', dataMode: 'data' };
document.getElementById('segmentFilter').value = 'all';
document.getElementById('channelFilter').value = 'all';
document.getElementById('periodFilter').value = '7d';
document.getElementById('dataModeFilter').value = 'data';
saveFilters();
render();
});
loadFilters();
render();
</script>
</body>
</html>