291 lines
15 KiB
HTML
291 lines
15 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>
|
||
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>
|