362 lines
17 KiB
HTML
362 lines
17 KiB
HTML
<!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>
|