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

362 lines
17 KiB
HTML
Raw Permalink 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>Бизнес-метрики — Контроль качества</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-filters-bar { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-5); }
.kt-ai-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); }
.kt-ai-chart { height: 200px; display: flex; align-items: flex-end; gap: 8px; }
.kt-ai-chart-bar { flex: 1; background: var(--kt-ai-chart-blue); border-radius: 2px 2px 0 0; position: relative; min-width: 20px; max-width: 28px; }
.kt-ai-chart-bar[data-kt-tip] { cursor: pointer; }
.kt-ai-chart-bar:hover { opacity: 0.85; }
.kt-ai-insights { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.kt-ai-insights h3 { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-3); }
.kt-ai-insights p { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3); line-height: 1.5; }
.kt-ai-recommendations { margin-top: var(--kt-ai-space-4); }
.kt-ai-recommendations h4 { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-2); }
.kt-ai-recommendations ul { margin: 0; padding-left: var(--kt-ai-space-4); }
.kt-ai-recommendations li { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-2); }
.kt-ai-empty { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-faint); }
[data-kt-data-state="empty"] [data-kt-region] { display: none; }
[data-kt-filter-note] { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-left: var(--kt-ai-space-2); }
</style>
</head>
<body data-kt-data-mode="demo">
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<strong>Бизнес-метрики</strong>
</div>
<span class="kt-ai-spacer"></span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-5)">
<div class="kt-ai-filters-bar">
<div class="kt-ai-field">
<label>Сегмент</label>
<select id="filter-segment" class="kt-ai-select">
<option value="all">Все</option>
<option value="B2B">B2B</option>
<option value="B2C">B2C</option>
</select>
</div>
<div class="kt-ai-field">
<label>Канал</label>
<select id="filter-channel" class="kt-ai-select">
<option value="all">Все</option>
<option value="web">Веб</option>
<option value="mobile">Мобильный</option>
<option value="call">Кол-центр</option>
</select>
</div>
<div class="kt-ai-field">
<label>Период</label>
<select id="filter-period" class="kt-ai-select">
<option value="7d">7 дней</option>
<option value="30d">30 дней</option>
<option value="90d">90 дней</option>
</select>
</div>
<div class="kt-ai-field">
<label>
<input type="checkbox" id="toggle-data" style="margin-right:var(--kt-ai-space-2)">
Нет данных
</label>
</div>
</div>
<div data-kt-region="kpi" class="kt-ai-kpi-strip">
<div class="kt-ai-kpi">
<div class="value" id="kpi-conversion">—</div>
<div class="label">Конверсия</div>
<div class="hint" id="kpi-conversion-hint">гипотеза, не подтверждено</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-satisfaction">—</div>
<div class="label">Удовлетворённость</div>
<div class="hint" id="kpi-satisfaction-hint">измерено</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-resolution">—</div>
<div class="label">Решено с первого раза</div>
<div class="hint" id="kpi-resolution-hint">предложение</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-complaints">—</div>
<div class="label">Жалобы</div>
<div class="hint" id="kpi-complaints-hint">Демо-данные</div>
</div>
</div>
<div data-kt-region="chart" class="kt-ai-card" style="padding:var(--kt-ai-space-4);margin-bottom:var(--kt-ai-space-6)">
<div id="chart" class="kt-ai-chart"></div>
</div>
<div class="kt-ai-table-wrap" data-kt-region="table" style="margin-bottom:var(--kt-ai-space-6)">
<table class="kt-ai-table">
<thead>
<tr>
<th>Дата</th>
<th>Сегмент</th>
<th>Канал</th>
<th class="num">Конверсия</th>
<th class="num">Удовлетворённость</th>
<th class="num">Жалобы</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<div data-kt-region="insights" class="kt-ai-insights">
<h3>Объяснение отклонений</h3>
<p id="insights-text">Загрузка...</p>
<div data-kt-region="recommendations" class="kt-ai-recommendations">
<h4>Рекомендации</h4>
<ul id="recommendations-list"></ul>
</div>
</div>
<div data-kt-region="empty" class="kt-ai-empty" style="display:none;">
<p>Недостаточно данных</p>
</div>
</main>
<script>
const DATA = [
{ date: "2025-11-01", segment: "B2B", channel: "web", period: "7d", conversion: 12.4, satisfaction: 78, resolution: 65, complaints: 3 },
{ date: "2025-11-02", segment: "B2B", channel: "mobile", period: "7d", conversion: 10.8, satisfaction: 82, resolution: 70, complaints: 2 },
{ date: "2025-11-03", segment: "B2B", channel: "call", period: "7d", conversion: 8.5, satisfaction: 71, resolution: 58, complaints: 5 },
{ date: "2025-11-04", segment: "B2C", channel: "web", period: "7d", conversion: 18.2, satisfaction: 85, resolution: 72, complaints: 1 },
{ date: "2025-11-05", segment: "B2C", channel: "mobile", period: "7d", conversion: 22.1, satisfaction: 88, resolution: 75, complaints: 1 },
{ date: "2025-11-06", segment: "B2C", channel: "call", period: "7d", conversion: 15.3, satisfaction: 79, resolution: 62, complaints: 4 },
{ date: "2025-11-07", segment: "B2B", channel: "web", period: "7d", conversion: 13.1, satisfaction: 80, resolution: 68, complaints: 2 },
{ date: "2025-10-01", segment: "B2B", channel: "web", period: "30d", conversion: 11.2, satisfaction: 76, resolution: 63, complaints: 8 },
{ date: "2025-10-05", segment: "B2B", channel: "mobile", period: "30d", conversion: 9.5, satisfaction: 81, resolution: 67, complaints: 6 },
{ date: "2025-10-10", segment: "B2C", channel: "web", period: "30d", conversion: 17.8, satisfaction: 84, resolution: 71, complaints: 4 },
{ date: "2025-10-15", segment: "B2C", channel: "mobile", period: "30d", conversion: 21.5, satisfaction: 87, resolution: 74, complaints: 3 },
{ date: "2025-10-20", segment: "B2B", channel: "call", period: "30d", conversion: 7.9, satisfaction: 69, resolution: 55, complaints: 12 },
{ date: "2025-10-25", segment: "B2C", channel: "call", period: "30d", conversion: 14.6, satisfaction: 77, resolution: 60, complaints: 9 },
{ date: "2025-08-01", segment: "B2B", channel: "web", period: "90d", conversion: 10.5, satisfaction: 75, resolution: 61, complaints: 18 },
{ date: "2025-08-15", segment: "B2C", channel: "web", period: "90d", conversion: 16.9, satisfaction: 83, resolution: 69, complaints: 11 },
{ date: "2025-09-01", segment: "B2B", channel: "mobile", period: "90d", conversion: 8.8, satisfaction: 79, resolution: 64, complaints: 15 },
{ date: "2025-09-15", segment: "B2C", channel: "mobile", period: "90d", conversion: 20.3, satisfaction: 86, resolution: 72, complaints: 8 },
{ date: "2025-10-01", segment: "B2B", channel: "call", period: "90d", conversion: 7.2, satisfaction: 68, resolution: 53, complaints: 28 },
{ date: "2025-10-15", segment: "B2C", channel: "call", period: "90d", conversion: 13.8, satisfaction: 76, resolution: 58, complaints: 22 }
];
const FILTERS = [
{ id: "filter-segment", label: "Сегмент", affects: ["table", "kpi", "chart", "insights"] },
{ id: "filter-channel", label: "Канал", affects: ["table", "kpi", "chart", "insights"] },
{ id: "filter-period", label: "Период", affects: ["table", "kpi", "chart", "insights"] }
];
function getState() {
return {
segment: document.getElementById("filter-segment").value,
channel: document.getElementById("filter-channel").value,
period: document.getElementById("filter-period").value,
noData: document.getElementById("toggle-data").checked
};
}
function saveState(state) {
localStorage.setItem("metrics-state", JSON.stringify(state));
}
function loadState() {
const saved = localStorage.getItem("metrics-state");
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed.segment) document.getElementById("filter-segment").value = parsed.segment;
if (parsed.channel) document.getElementById("filter-channel").value = parsed.channel;
if (parsed.period) document.getElementById("filter-period").value = parsed.period;
if (parsed.noData !== undefined) document.getElementById("toggle-data").checked = parsed.noData;
} catch (e) {}
}
}
function viewFor(state) {
if (state.noData) {
return { status: "empty", rows: [], series: [], kpis: {}, explanation: "", recommendations: [] };
}
let filtered = DATA.filter(d => {
if (state.segment !== "all" && d.segment !== state.segment) return false;
if (state.channel !== "all" && d.channel !== state.channel) return false;
if (state.period && d.period !== state.period) return false;
return true;
});
if (filtered.length === 0) {
return { status: "empty", rows: [], series: [], kpis: {}, explanation: "За выбранный период данных нет", recommendations: [] };
}
const avgConv = filtered.reduce((s, d) => s + d.conversion, 0) / filtered.length;
const avgSat = filtered.reduce((s, d) => s + d.satisfaction, 0) / filtered.length;
const avgRes = filtered.reduce((s, d) => s + d.resolution, 0) / filtered.length;
const totalComplaints = filtered.reduce((s, d) => s + d.complaints, 0);
const series = filtered.map(d => ({ label: d.date.slice(5), value: d.conversion, segment: d.segment, channel: d.channel }));
let explanation = "";
const recommendations = [];
if (avgConv < 10) {
explanation = "Вероятная причина низкой конверсии — сложность воронки в выбранном канале. Гипотеза, не подтверждено.";
recommendations.push("Упростить форму заявки в мобильном канале");
recommendations.push("Провести A/B тест упрощённого чекаута");
} else if (avgConv > 18) {
explanation = "Конверсия в норме или выше. Отклонений не выявлено.";
recommendations.push("Продолжить мониторинг показателей");
} else {
explanation = "Конверсия в пределах ожидаемых значений. Незначительные отклонения могут быть связаны с сезонностью.";
recommendations.push("Сравнить с аналогичным периодом прошлого месяца");
}
if (avgSat < 75) {
explanation += " Удовлетворённость ниже целевой — вероятная причина в длительном времени ответа.";
recommendations.push("Увеличить количество операторов в часы пик");
}
return {
status: "ok",
rows: filtered,
series: series,
kpis: {
conversion: avgConv.toFixed(1) + "%",
satisfaction: avgSat.toFixed(0) + "%",
resolution: avgRes.toFixed(0) + "%",
complaints: totalComplaints.toString()
},
explanation: explanation,
recommendations: recommendations.slice(0, 3)
};
}
function render(view) {
document.documentElement.dataset.ktDataState = view.status;
const emptyRegion = document.querySelector('[data-kt-region="empty"]');
const kpiRegion = document.querySelector('[data-kt-region="kpi"]');
const chartRegion = document.querySelector('[data-kt-region="chart"]');
const tableRegion = document.querySelector('[data-kt-region="table"]');
const insightsRegion = document.querySelector('[data-kt-region="insights"]');
if (view.status === "empty") {
emptyRegion.style.display = "block";
kpiRegion.style.display = "none";
chartRegion.parentElement.style.display = "none";
tableRegion.parentElement.style.display = "none";
insightsRegion.style.display = "none";
} else {
emptyRegion.style.display = "none";
kpiRegion.style.display = "grid";
chartRegion.parentElement.style.display = "block";
tableRegion.parentElement.style.display = "block";
insightsRegion.style.display = "block";
document.getElementById("kpi-conversion").textContent = view.kpis.conversion || "—";
document.getElementById("kpi-satisfaction").textContent = view.kpis.satisfaction || "—";
document.getElementById("kpi-resolution").textContent = view.kpis.resolution || "—";
document.getElementById("kpi-complaints").textContent = view.kpis.complaints || "—";
const chart = document.getElementById("chart");
chart.innerHTML = "";
if (view.series.length > 0) {
const maxValue = Math.max(...view.series.map(s => s.value), 1);
view.series.forEach(s => {
const bar = document.createElement("div");
bar.className = "kt-ai-chart-bar";
bar.style.height = (s.value / maxValue * 100) + "%";
bar.setAttribute("data-kt-tip", s.label + ": " + s.value + "%");
chart.appendChild(bar);
});
}
const tbody = document.getElementById("table-body");
tbody.innerHTML = "";
view.rows.forEach(row => {
const tr = document.createElement("tr");
tr.innerHTML =
'<td>' + row.date + '</td>' +
'<td>' + row.segment + '</td>' +
'<td>' + row.channel + '</td>' +
'<td class="num">' + row.conversion.toFixed(1) + '%</td>' +
'<td class="num">' + row.satisfaction + '%</td>' +
'<td class="num">' + row.complaints + '</td>';
tbody.appendChild(tr);
});
document.getElementById("insights-text").textContent = view.explanation || "Недостаточно данных для анализа";
const recList = document.getElementById("recommendations-list");
recList.innerHTML = "";
view.recommendations.forEach(rec => {
const li = document.createElement("li");
li.textContent = rec;
recList.appendChild(li);
});
}
}
function update() {
const state = getState();
saveState(state);
const view = viewFor(state);
render(view);
}
document.addEventListener("DOMContentLoaded", function() {
loadState();
document.getElementById("filter-segment").addEventListener("change", update);
document.getElementById("filter-channel").addEventListener("change", update);
document.getElementById("filter-period").addEventListener("change", update);
document.getElementById("toggle-data").addEventListener("change", update);
document.querySelectorAll('[data-kt-tip]').forEach(el => {
el.addEventListener("mouseenter", function() {
const tip = document.createElement("div");
tip.textContent = this.getAttribute("data-kt-tip");
tip.style.position = "absolute";
tip.style.background = "var(--kt-ai-bg)";
tip.style.border = "1px solid var(--kt-ai-border)";
tip.style.padding = "var(--kt-ai-space-1) var(--kt-ai-space-2)";
tip.style.borderRadius = "var(--kt-ai-radius-md)";
tip.style.fontSize = "var(--kt-ai-text-xs)";
tip.style.pointerEvents = "none";
tip.style.zIndex = "var(--kt-ai-z-tooltip)";
document.body.appendChild(tip);
const rect = this.getBoundingClientRect();
tip.style.left = (rect.left + rect.width/2 - tip.offsetWidth/2) + "px";
tip.style.top = (rect.top - tip.offsetHeight - 8) + "px";
this._tip = tip;
});
el.addEventListener("mouseleave", function() {
if (this._tip) {
this._tip.remove();
this._tip = null;
}
});
});
update();
});
</script>
</body>
</html>