analiticheskiy-deshbord-sluz/index.html

291 lines
15 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>Аналитический дашборд — Служба поддержки по регионам</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>
body.kt-ai-app { margin: 0; background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
.app-header { position: sticky; top: 0; z-index: 100; background: var(--kt-ai-bg); border-bottom: 1px solid var(--kt-ai-border); padding: var(--kt-ai-space-4) 0; }
.app-header-in { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
.app-content { padding: var(--kt-ai-space-6) 0; }
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 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); line-height: 1.2; }
.kpi-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); 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); }
.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-area { height: 200px; display: flex; align-items: flex-end; gap: 8px; padding-top: var(--kt-ai-space-4); }
.chart-bar { flex: 1; background: var(--kt-ai-chart-blue); border-radius: var(--kt-ai-radius-md); min-height: 4px; transition: height 0.3s ease; position: relative; }
.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; }
.chart-bar:hover[data-kt-tip]::after { opacity: 1; }
.filters-row { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-4); }
.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; }
.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); border-bottom: 1px solid var(--kt-ai-border); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
.kt-ai-table td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); font-size: var(--kt-ai-text-sm); }
.kt-ai-table tr:last-child td { border-bottom: none; }
.kt-ai-table tr:hover { background: var(--kt-ai-bg-soft); }
.text-right { text-align: right; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.status-chip { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-new { background: rgba(59, 130, 246, 0.1); color: var(--kt-ai-chart-blue); }
.status-work { background: rgba(249, 115, 22, 0.1); color: var(--kt-ai-chart-orange); }
.status-done { background: rgba(34, 197, 94, 0.1); color: var(--kt-ai-chart-green); }
.status-closed { background: rgba(107, 114, 128, 0.1); color: var(--kt-ai-fg-muted); }
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-4); color: var(--kt-ai-fg-muted); }
.section-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
a.kt-ai-btn { text-decoration: none; }
</style>
</head>
<body class="kt-ai-app">
<header class="app-header">
<div class="kt-ai-wrap app-header-in">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<div>
<strong style="font-size: var(--kt-ai-text-lg)">Аналитический дашборд</strong>
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">Служба поддержки по регионам</div>
</div>
<span class="kt-ai-spacer"></span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
</div>
</header>
<main class="app-content">
<div class="kt-ai-wrap">
<!-- KPI полоса -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-value" id="kpi-open">24</div>
<div class="kpi-label">Открытые заявки</div>
<div class="kpi-hint">+3 за сегодня</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-work">12</div>
<div class="kpi-label">В работе</div>
<div class="kpi-hint">Среднее время: 2.4 ч</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-solved">156</div>
<div class="kpi-label">Решено за неделю</div>
<div class="kpi-hint">+12% к прошлой</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-csat">4.7</div>
<div class="kpi-label">Удовлетворённость</div>
<div class="kpi-hint">из 5.0</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-avg-time">1.8</div>
<div class="kpi-label">Среднее время решения</div>
<div class="kpi-hint">часов</div>
</div>
</div>
<!-- График -->
<div class="chart-wrap">
<div class="chart-title">Динамика заявок по дням</div>
<div class="filters-row">
<select class="kt-ai-select" id="period-filter" style="min-width: 180px">
<option value="7d">Последние 7 дней</option>
<option value="14d">Последние 14 дней</option>
<option value="30d">Последние 30 дней</option>
</select>
<span class="kt-ai-chip" data-tone="orange" data-kt-filter-note>в демо не меняет данные</span>
</div>
<div class="chart-area" id="chart-bars"></div>
</div>
<!-- Таблица -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-4)">
<h2 class="section-title" style="margin: 0">Заявки</h2>
<div class="filters-row" style="margin: 0">
<select class="kt-ai-select" id="region-filter" style="min-width: 160px">
<option value="all">Все регионы</option>
<option value="Алматы">Алматы</option>
<option value="Астана">Астана</option>
<option value="Шымкент">Шымкент</option>
<option value="Караганда">Караганда</option>
<option value="Актобе">Актобе</option>
<option value="Тараз">Тараз</option>
<option value="Уральск">Уральск</option>
<option value="Атырау">Атырау</option>
</select>
<select class="kt-ai-select" id="status-filter" style="min-width: 140px">
<option value="all">Все статусы</option>
<option value="Новая">Новая</option>
<option value="В работе">В работе</option>
<option value="Решена">Решена</option>
<option value="Закрыта">Закрыта</option>
</select>
</div>
</div>
<div class="table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>ID</th>
<th>Регион</th>
<th>Клиент</th>
<th>Проблема</th>
<th>Дата создания</th>
<th class="text-right">Время решения</th>
<th>Статус</th>
</tr>
</thead>
<tbody id="tickets-body"></tbody>
</table>
<div class="empty-state" id="empty-state" style="display: none">
<div style="font-size: var(--kt-ai-text-lg); margin-bottom: var(--kt-ai-space-2)">Нет данных</div>
<div>По выбранным фильтрам заявок не найдено</div>
</div>
</div>
</div>
</main>
<script>
// === Демо-данные ===
const DEMO_DATA = {
tickets: [
{ id: 1001, region: 'Алматы', client: 'ИП Иванов А.А.', problem: 'Нет интернета', created: '2026-09-14', solveTime: 1.5, status: 'Новая' },
{ id: 1002, region: 'Астана', client: 'ТОО "TechSoft"', problem: 'Телефония не работает', created: '2026-09-14', solveTime: 3.2, status: 'В работе' },
{ id: 1003, region: 'Шымкент', client: 'Физ. лицо', problem: 'Медленный канал', created: '2026-09-13', solveTime: 0.8, status: 'Решена' },
{ id: 1004, region: 'Караганда', client: 'ИП Петрова С.К.', problem: 'Оборудование неисправно', created: '2026-09-13', solveTime: 4.1, status: 'В работе' },
{ id: 1005, region: 'Алматы', client: 'ТОО "АлмаТрейд"', problem: 'Настройка VPN', created: '2026-09-12', solveTime: 2.0, status: 'Решена' },
{ id: 1006, region: 'Актобе', client: 'Физ. лицо', problem: 'Нет связи', created: '2026-09-12', solveTime: 1.2, status: 'Закрыта' },
{ id: 1007, region: 'Тараз', client: 'ИП Ahmedov B.', problem: 'Перенос линии', created: '2026-09-11', solveTime: 5.5, status: 'В работе' },
{ id: 1008, region: 'Астана', client: 'ТОО "BuildCorp"', problem: 'Сервер не доступен', created: '2026-09-11', solveTime: 0.5, status: 'Решена' },
{ id: 1009, region: 'Уральск', client: 'Физ. лицо', problem: 'Помехи на линии', created: '2026-09-10', solveTime: 1.8, status: 'Закрыта' },
{ id: 1010, region: 'Атырау', client: 'ТОО "OilService"', problem: 'IP-телефония', created: '2026-09-10', solveTime: 2.3, status: 'Новая' },
{ id: 1011, region: 'Алматы', client: 'ИП Kim D.', problem: 'Подключение услуги', created: '2026-09-09', solveTime: 0.3, status: 'Решена' },
{ id: 1012, region: 'Караганда', client: 'Физ. лицо', problem: 'Частые обрывы', created: '2026-09-09', solveTime: 3.7, status: 'В работе' },
{ id: 1013, region: 'Шымкент', client: 'ТОО "SouthTrade"', problem: 'Смена тарифа', created: '2026-09-08', solveTime: 0.2, status: 'Закрыта' },
{ id: 1014, region: 'Астана', client: 'ИП Nazarbayev A.', problem: 'Оптика повреждена', created: '2026-09-08', solveTime: 6.0, status: 'Новая' },
{ id: 1015, region: 'Тараз', client: 'Физ. лицо', problem: 'Нет DSL', created: '2026-09-07', solveTime: 1.1, status: 'Решена' },
],
chart: {
'7d': [18, 24, 15, 28, 22, 31, 26],
'14d': [18, 24, 15, 28, 22, 31, 26, 19, 27, 23, 30, 25, 29, 21],
'30d': Array.from({length: 30}, () => Math.floor(Math.random() * 20) + 15)
}
};
// === Загрузка/сохранение в localStorage ===
function loadData() {
const stored = localStorage.getItem('dashboardData');
return stored ? JSON.parse(stored) : DEMO_DATA;
}
function saveData(data) {
localStorage.setItem('dashboardData', JSON.stringify(data));
}
// === Инициализация ===
let appData = loadData();
saveData(appData); // Сохраняем при первом запуске
// === Рендер KPI ===
function renderKPI() {
const tickets = appData.tickets;
const open = tickets.filter(t => t.status === 'Новая').length;
const work = tickets.filter(t => t.status === 'В работе').length;
const solved = tickets.filter(t => t.status === 'Решена').length;
const solvedTimes = tickets.filter(t => t.status === 'Решена' || t.status === 'Закрыта').map(t => t.solveTime);
const avgTime = solvedTimes.length > 0 ? (solvedTimes.reduce((a, b) => a + b, 0) / solvedTimes.length).toFixed(1) : '0.0';
const csat = (4.5 + Math.random() * 0.4).toFixed(1);
document.getElementById('kpi-open').textContent = open;
document.getElementById('kpi-work').textContent = work;
document.getElementById('kpi-solved').textContent = solved;
document.getElementById('kpi-csat').textContent = csat;
document.getElementById('kpi-avg-time').textContent = avgTime;
}
// === Рендер графика ===
function renderChart(period) {
const container = document.getElementById('chart-bars');
const data = appData.chart[period] || appData.chart['7d'];
const max = Math.max(...data);
container.innerHTML = '';
data.forEach((value, i) => {
const bar = document.createElement('div');
bar.className = 'chart-bar';
bar.style.height = (value / max * 180) + 'px';
bar.setAttribute('data-kt-tip', value + ' заявок');
container.appendChild(bar);
});
}
// === Рендер таблицы ===
function renderTable() {
const regionFilter = document.getElementById('region-filter').value;
const statusFilter = document.getElementById('status-filter').value;
const tbody = document.getElementById('tickets-body');
const emptyState = document.getElementById('empty-state');
let filtered = appData.tickets;
if (regionFilter !== 'all') {
filtered = filtered.filter(t => t.region === regionFilter);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(t => t.status === statusFilter);
}
if (filtered.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = filtered.map(t => {
let statusClass = 'status-new';
if (t.status === 'В работе') statusClass = 'status-work';
if (t.status === 'Решена') statusClass = 'status-done';
if (t.status === 'Закрыта') statusClass = 'status-closed';
return `<tr>
<td class="tabular-nums">#${t.id}</td>
<td>${t.region}</td>
<td>${t.client}</td>
<td>${t.problem}</td>
<td>${t.created}</td>
<td class="text-right tabular-nums">${t.solveTime} ч</td>
<td><span class="status-chip ${statusClass}">${t.status}</span></td>
</tr>`;
}).join('');
}
// === Обработчики фильтров ===
document.getElementById('period-filter').addEventListener('change', (e) => {
renderChart(e.target.value);
});
document.getElementById('region-filter').addEventListener('change', () => {
renderTable();
});
document.getElementById('status-filter').addEventListener('change', () => {
renderTable();
});
// === Первый рендер ===
renderKPI();
renderChart('7d');
renderTable();
</script>
</body>
</html>