navaybkod-deshbord-ispolneni/index.html

345 lines
23 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.0">
<title>OKR Дашборд</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>
.page-hero { margin-bottom: var(--kt-ai-stack-group); }
.toolbar-row { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; padding: 0 0 var(--kt-ai-stack-block); }
.toolbar-row .spacer { flex: 1; }
.progress-cell { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
.progress-bar { flex: 1; height: 8px; border-radius: var(--kt-ai-radius-full); background: var(--kt-ai-bg-sunken); overflow: hidden; }
.progress-bar > span { display: block; height: 100%; border-radius: var(--kt-ai-radius-full); background: var(--kt-ai-chart-blue); }
.progress-bar.ok > span { background: var(--kt-ai-chart-green); }
.progress-bar.warn > span { background: var(--kt-ai-chart-orange); }
.progress-bar.risk > span { background: var(--kt-ai-chart-red); }
.progress-pct { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); min-width: 42px; text-align: right; }
.chart-container { height: 220px; margin-top: var(--kt-ai-space-6); }
.chart-bars { display: flex; align-items: flex-end; gap: var(--kt-ai-space-4); height: 180px; padding: var(--kt-ai-space-4) 0; }
.chart-bar { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--kt-ai-space-2); }
.chart-bar .bar { width: 100%; max-width: 48px; border-radius: var(--kt-ai-radius-md) var(--kt-ai-radius-md) 0 0; background: var(--kt-ai-chart-blue); transition: height 0.3s ease; }
.chart-bar .bar.ok { background: var(--kt-ai-chart-green); }
.chart-bar .bar.warn { background: var(--kt-ai-chart-orange); }
.chart-bar .bar.risk { background: var(--kt-ai-chart-red); }
.chart-bar .label { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); text-align: center; }
.chart-bar .value { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg); }
.owner-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); display: inline-flex; align-items: center; justify-content: center; font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-semibold); margin-right: var(--kt-ai-space-2); }
</style>
</head>
<body class="kt-ai-app">
<script id="kt-app-config" type="application/json">
{
"product": { "name": "OKR Дашборд", "purpose": "Исполнение OKR компании", "entity": "ok" },
"user": { "name": "Төре", "role": "Руководитель" },
"nav": [
{ "label": "Дашборд", "icon": "dashboard" },
{ "label": "Цели", "icon": "trophy" },
{ "label": "Команда", "icon": "users" },
{ "label": "Отчёты", "icon": "chart" }
],
"kpis": [
{ "value": "24", "label": "всего OKR", "hint": "Q3 2026" },
{ "value": "9", "label": "выполнено", "hint": "37.5%" },
{ "value": "12", "label": "в работе", "hint": "on-track" },
{ "value": "3", "label": "риски", "hint": "требуют внимания" }
],
"statuses": {
"on-track": { "label": "в графике", "tone": "ok" },
"at-risk": { "label": "риск", "tone": "warn" },
"off-track": { "label": "отставание", "tone": "risk" },
"completed": { "label": "выполнено", "tone": "ok" }
},
"table": {
"columns": [
{ "key": "objective", "label": "Цель", "type": "text" },
{ "key": "owner", "label": "Владелец", "type": "text" },
{ "key": "progress", "label": "Прогресс", "type": "num" },
{ "key": "quarter", "label": "Квартал", "type": "text" },
{ "key": "status", "label": "Статус", "type": "status" }
],
"rows": [
{ "objective": "Увеличить доход на 30%", "owner": "Алия Т.", "progress": 85, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Новые клиенты +25%, KR2: LTV +15%" },
{ "objective": "Запустить мобильное приложение", "owner": "Бауыржан К.", "progress": 100, "quarter": "Q3", "status": "completed", "_kr": "KR1: App Store, KR2: Google Play, KR3: 10K загрузок" },
{ "objective": "Снизить отток клиентов до 5%", "owner": "Светлана И.", "progress": 60, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Churn <5%, KR2: NPS >40" },
{ "objective": "Автоматизировать 80% рутины", "owner": "Данияр М.", "progress": 45, "quarter": "Q3", "status": "at-risk", "_kr": "KR1: 20 процессов, KR2: 500 часов/мес" },
{ "objective": "Выйти на рынок Узбекистана", "owner": "Ержан А.", "progress": 30, "quarter": "Q3", "status": "at-risk", "_kr": "KR1: Юрлицо, KR2: 5 сотрудников" },
{ "objective": "Повысить удовлетворённость сотрудников", "owner": "Гульнара С.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: eNPS >30, KR2: Текучесть <10%" },
{ "objective": "Запустить 3 новых продукта", "owner": "Алия Т.", "progress": 67, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Продукт A, KR2: Продукт B, KR3: Продукт C" },
{ "objective": "Сократить издержки на 15%", "owner": "Бауыржан К.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: Оптимизация закупок, KR2: Энергоэффективность" },
{ "objective": "Обучить 100% команды", "owner": "Гульнара С.", "progress": 78, "quarter": "Q3", "status": "on-track", "_kr": "KR1: 40 часов/сотрудник, KR2: 10 сертификаций" },
{ "objective": "Увеличить долю рынка до 25%", "owner": "Светлана И.", "progress": 52, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Маркетинг +30%, KR2: Партнёрства" },
{ "objective": "Внедрить CRM-систему", "owner": "Данияр М.", "progress": 90, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Миграция данных, KR2: Обучение" },
{ "objective": "Запустить программу лояльности", "owner": "Ержан А.", "progress": 20, "quarter": "Q3", "status": "off-track", "_kr": "KR1: 10K участников, KR2: Retention +10%" },
{ "objective": "Оптимизировать логистику", "owner": "Алия Т.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: Время доставки -20%, KR2: Стоимость -15%" },
{ "objective": "Развить партнёрскую сеть", "owner": "Бауыржан К.", "progress": 65, "quarter": "Q3", "status": "on-track", "_kr": "KR1: 50 партнёров, KR2: 20% продаж" },
{ "objective": "Улучшить качество поддержки", "owner": "Светлана И.", "progress": 88, "quarter": "Q3", "status": "on-track", "_kr": "KR1: CSAT >4.5, KR2: Время ответа <5 мин" },
{ "objective": "Запустить онлайн-академию", "owner": "Гульнара С.", "progress": 40, "quarter": "Q3", "status": "at-risk", "_kr": "KR1: 20 курсов, KR2: 1000 студентов" },
{ "objective": "Модернизировать инфраструктуру", "owner": "Данияр М.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: Миграция в облако, KR2: Uptime 99.9%" },
{ "objective": "Расширить ассортимент на 40%", "owner": "Ержан А.", "progress": 72, "quarter": "Q3", "status": "on-track", "_kr": "KR1: 200 SKU, KR2: 30% от продаж" },
{ "objective": "Снизить время разработки на 25%", "owner": "Алия Т.", "progress": 55, "quarter": "Q3", "status": "on-track", "_kr": "KR1: CI/CD, KR2: Автоматизация тестов" },
{ "objective": "Повысить узнаваемость бренда", "owner": "Бауыржан К.", "progress": 48, "quarter": "Q3", "status": "on-track", "_kr": "KR1: Охват 1M, KR2: Упоминания +50%" },
{ "objective": "Запустить реферальную программу", "owner": "Светлана И.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: 5K рефералов, KR2: 15% от продаж" },
{ "objective": "Оптимизировать налоговую нагрузку", "owner": "Гульнара С.", "progress": 100, "quarter": "Q2", "status": "completed", "_kr": "KR1: Легальная оптимизация, KR2: Экономия 10%" },
{ "objective": "Внедрить эко-инициативы", "owner": "Данияр М.", "progress": 35, "quarter": "Q3", "status": "off-track", "_kr": "KR1: Углеродный след -20%, KR2: Переработка" },
{ "objective": "Развить корпоративную культуру", "owner": "Ержан А.", "progress": 82, "quarter": "Q3", "status": "on-track", "_kr": "KR1: 12 мероприятий, KR2: Участие 80%" }
]
},
"drawer": {
"titleKey": "objective",
"fields": [
{ "key": "owner", "label": "Владелец" },
{ "key": "quarter", "label": "Квартал" },
{ "key": "progress", "label": "Прогресс" }
],
"flagsLabel": "Ключевые результаты"
}
}
</script>
<div id="shell-root"></div>
<script src="./design-system/kt-ai-chart-tip.js"></script>
<script>
(function () {
var CFG = JSON.parse(document.getElementById('kt-app-config').textContent);
var owners = [];
CFG.table.rows.forEach(function (r) { if (owners.indexOf(r.owner) === -1) owners.push(r.owner); });
var saved = null;
try { saved = localStorage.getItem('okr-dashboard-state'); } catch (_) {}
var state = saved ? JSON.parse(saved) : { filterStatus: 'all', filterOwner: 'all', sortBy: null, sortDir: 'asc' };
function saveState() { try { localStorage.setItem('okr-dashboard-state', JSON.stringify(state)); } catch (_) {} }
function getStatusTone(status) {
var s = CFG.statuses[status];
if (!s) return '';
if (s.tone === 'ok') return 'ok';
if (s.tone === 'warn') return 'warn';
if (s.tone === 'risk') return 'risk';
return '';
}
function getProgressTone(pct) {
if (pct >= 80) return 'ok';
if (pct >= 50) return 'warn';
return 'risk';
}
function initials(name) {
var parts = (name || '').split(' ');
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function render() {
var rows = CFG.table.rows.slice();
if (state.filterStatus !== 'all') {
rows = rows.filter(function (r) { return r.status === state.filterStatus; });
}
if (state.filterOwner !== 'all') {
rows = rows.filter(function (r) { return r.owner === state.filterOwner; });
}
if (state.sortBy) {
rows.sort(function (a, b) {
var va = a[state.sortBy], vb = b[state.sortBy];
if (typeof va === 'number' && typeof vb === 'number') {
return state.sortDir === 'asc' ? va - vb : vb - va;
}
va = String(va).toLowerCase(); vb = String(vb).toLowerCase();
if (va < vb) return state.sortDir === 'asc' ? -1 : 1;
if (va > vb) return state.sortDir === 'asc' ? 1 : -1;
return 0;
});
}
var statusCounts = { all: CFG.table.rows.length, on-track: 0, at-risk: 0, off-track: 0, completed: 0 };
CFG.table.rows.forEach(function (r) { if (statusCounts[r.status] !== undefined) statusCounts[r.status]++; });
var quarterData = {};
CFG.table.rows.forEach(function (r) {
var q = r.quarter;
if (!quarterData[q]) quarterData[q] = { total: 0, sum: 0 };
quarterData[q].total++;
quarterData[q].sum += r.progress;
});
var quarters = Object.keys(quarterData).sort();
var html = '';
html += '<div class="kt-ai-shell" data-rail="false">';
html += '<aside class="kt-ai-sidebar">';
html += '<div class="kt-ai-sidebar-header"><div class="kt-ai-sidebar-brand"><svg class="kt-icon" style="width:22px;height:22px"><use href="#trophy"></use></svg><span class="kt-ai-sidebar-brand-name">OKR Дашборд</span></div></div>';
html += '<nav class="kt-ai-sidebar-nav">';
CFG.nav.forEach(function (item, i) {
html += '<a class="kt-ai-nav-item' + (i === 0 ? ' active' : '') + '" href="#"><svg class="kt-icon"><use href="#' + (item.icon || 'dashboard') + '"></use></svg><span>' + item.label + '</span></a>';
});
html += '</nav>';
html += '<div class="kt-ai-sidebar-footer">';
html += '<button class="kt-ai-nav-item" id="themeToggle"><svg class="kt-icon"><use href="#sun"></use></svg><span>Тема</span></button>';
html += '<button class="kt-ai-nav-item"><svg class="kt-icon"><use href="#help"></use></svg><span>Помощь</span></button>';
html += '<div class="kt-ai-nav-item"><div class="kt-ai-avatar">' + initials(CFG.user.name) + '</div><span>' + CFG.user.name + '</span></div>';
html += '</div>';
html += '</aside>';
html += '<div class="kt-ai-canvas">';
html += '<header class="kt-ai-topbar">';
html += '<div class="kt-ai-topbar-start"><button class="kt-ai-icon-button kt-ai-topbar-burger" id="burgerBtn"><svg class="kt-icon"><use href="#panelLeft"></use></svg></button>';
html += '<div class="kt-ai-topbar-title"><span class="t">Исполнение OKR</span><span class="sub">Q3 2026</span></div></div>';
html += '<div class="kt-ai-topbar-end">';
html += '<div class="kt-ai-search-field"><input class="kt-ai-search" placeholder="Поиск OKR…" id="searchInput"><svg class="kt-icon"><use href="#search"></use></svg></div>';
html += '</div></header>';
html += '<main id="view">';
html += '<div class="page-hero"><div class="kt-ai-page-head"><h1>Дашборд исполнения OKR</h1><p class="kt-ai-page-subtitle">Отслеживайте прогресс по целям и ключевым результатам компании</p></div></div>';
html += '<div class="kt-ai-kpi-strip" data-cards="true">';
CFG.kpis.forEach(function (kpi) {
html += '<div class="kt-ai-kpi"><span class="value">' + kpi.value + '</span><span class="label">' + kpi.label + '</span>' + (kpi.hint ? '<span class="hint">' + kpi.hint + '</span>' : '') + '</div>';
});
html += '</div>';
html += '<div class="toolbar-row">';
html += '<div class="kt-ai-chip-group">';
var statusLabels = { all: 'Все', on-track: 'В графике', at-risk: 'Риск', off-track: 'Отставание', completed: 'Выполнено' };
['all', 'on-track', 'at-risk', 'off-track', 'completed'].forEach(function (s) {
html += '<button class="kt-ai-chip' + (state.filterStatus === s ? ' active' : '') + '" data-status="' + s + '">' + statusLabels[s] + ' <span class="kt-ai-chip-count">' + statusCounts[s] + '</span></button>';
});
html += '</div>';
html += '<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="ownerFilter"><option value="all">Все владельцы</option>';
owners.forEach(function (o) { html += '<option value="' + o + '"' + (state.filterOwner === o ? ' selected' : '') + '>' + o + '</option>'; });
html += '</select></div>';
html += '<div class="spacer"></div>';
html += '<button class="kt-ai-icon-button" title="Обновить"><svg class="kt-icon"><use href="#check"></use></svg></button>';
html += '<button class="kt-ai-icon-button" id="columnsBtn" title="Колонки"><svg class="kt-icon"><use href="#more"></use></svg></button>';
html += '</div>';
html += '<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr>';
CFG.table.columns.forEach(function (col) {
html += '<th>' + col.label + '</th>';
});
html += '<th></th></tr></thead><tbody>';
rows.forEach(function (r) {
var statusDef = CFG.statuses[r.status] || {};
var tone = getStatusTone(r.status);
var progressTone = getProgressTone(r.progress);
html += '<tr data-obj="' + r.objective.replace(/"/g, '&quot;') + '">';
html += '<td><div><div class="kt-ai-table-cell-title">' + r.objective + '</div>' + (r._kr ? '<div class="kt-ai-table-cell-sub">' + r._kr + '</div>' : '') + '</div></td>';
html += '<td><span class="owner-avatar">' + initials(r.owner) + '</span>' + r.owner + '</td>';
html += '<td><div class="progress-cell"><div class="progress-bar ' + progressTone + '"><span style="width:' + r.progress + '%"></span></div><span class="progress-pct">' + r.progress + '%</span></div></td>';
html += '<td>' + r.quarter + '</td>';
html += '<td><span class="kt-ai-chip' + (tone ? ' data-tone="' + tone + '"' : '') + '>' + statusDef.label + '</span></td>';
html += '<td class="kt-ai-table-cell-actions"><button class="kt-ai-icon-button"><svg class="kt-icon"><use href="#more"></use></svg></button></td>';
html += '</tr>';
});
html += '</tbody></table></div>';
html += '<div class="chart-container">';
html += '<h3 style="font-size:var(--kt-ai-text-lg);font-weight:var(--kt-ai-weight-semibold);margin-bottom:var(--kt-ai-space-4)">Прогресс по кварталам</h3>';
html += '<div class="chart-bars">';
quarters.forEach(function (q) {
var avg = Math.round(quarterData[q].sum / quarterData[q].total);
var barTone = getProgressTone(avg);
html += '<div class="chart-bar" data-kt-tip=\'{"title":"' + q + ' 2026","rows":[{"label":"Средний прогресс","value":"' + avg + '%"},{"label":"Целей","value":"' + quarterData[q].total + '"}]}\' tabindex="0">';
html += '<div class="bar ' + barTone + '" style="height:' + (avg * 1.8) + 'px"></div>';
html += '<span class="label">' + q + '</span>';
html += '<span class="value">' + avg + '%</span>';
html += '</div>';
});
html += '</div></div>';
html += '</main></div></div>';
document.getElementById('shell-root').innerHTML = html;
document.querySelectorAll('.kt-ai-chip[data-status]').forEach(function (chip) {
chip.addEventListener('click', function () {
state.filterStatus = this.getAttribute('data-status');
saveState();
render();
});
});
document.getElementById('ownerFilter').addEventListener('change', function () {
state.filterOwner = this.value;
saveState();
render();
});
document.getElementById('themeToggle').addEventListener('click', function () {
var htmlEl = document.documentElement;
var cur = htmlEl.getAttribute('data-theme');
var next = cur === 'light' ? 'dark' : 'light';
htmlEl.setAttribute('data-theme', next);
var icon = this.querySelector('use');
if (icon) icon.setAttribute('href', next === 'light' ? '#sun' : '#moon');
});
document.getElementById('burgerBtn').addEventListener('click', function () {
var shell = document.querySelector('.kt-ai-shell');
var cur = shell.getAttribute('data-rail');
shell.setAttribute('data-rail', cur === 'true' ? 'false' : 'true');
var icon = this.querySelector('use');
if (icon) icon.setAttribute('href', cur === 'true' ? '#panelLeft' : '#panelLeftClose');
});
document.querySelectorAll('tbody tr').forEach(function (tr) {
tr.addEventListener('click', function () {
var obj = this.getAttribute('data-obj');
var row = CFG.table.rows.find(function (r) { return r.objective === obj; });
if (!row) return;
openDrawer(row);
});
});
}
function openDrawer(row) {
var statusDef = CFG.statuses[row.status] || {};
var overlay = document.createElement('div');
overlay.className = 'kt-ai-overlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:1000';
var drawer = document.createElement('aside');
drawer.className = 'kt-ai-drawer kt-ai-card';
drawer.style.cssText = 'position:fixed;top:0;right:0;bottom:0;width:480px;max-width:100vw;z-index:1001;padding:var(--kt-ai-space-8);overflow-y:auto';
var krHtml = '';
if (row._kr) {
row._kr.split(',').forEach(function (kr) {
krHtml += '<span class="kt-ai-chip">' + kr.trim() + '</span>';
});
}
drawer.innerHTML = '<div class="kt-ai-drawer-header"><div class="kt-ai-drawer-title"><div style="font-size:var(--kt-ai-text-xl);font-weight:var(--kt-ai-weight-semibold);margin-bottom:var(--kt-ai-space-2)">' + row.objective + '</div><span class="kt-ai-chip' + (getStatusTone(row.status) ? ' data-tone="' + getStatusTone(row.status) + '"' : '') + '">' + statusDef.label + '</span></div><button class="kt-ai-icon-button" id="closeDrawer"><svg class="kt-icon"><use href="#x"></use></svg></button></div>';
drawer.innerHTML += '<div class="kt-ai-drawer-section"><strong class="kt-ai-drawer-section-label">Ключевые результаты</strong>' + krHtml + '</div>';
drawer.innerHTML += '<div class="kt-ai-drawer-section"><div style="display:grid;gap:var(--kt-ai-space-4)">' +
'<div><span class="kt-ai-form-label">Владелец</span><div style="font-size:var(--kt-ai-text-sm);font-weight:var(--kt-ai-weight-medium)">' + row.owner + '</div></div>' +
'<div><span class="kt-ai-form-label">Квартал</span><div style="font-size:var(--kt-ai-text-sm);font-weight:var(--kt-ai-weight-medium)">' + row.quarter + '</div></div>' +
'<div><span class="kt-ai-form-label">Прогресс</span><div class="progress-cell" style="margin-top:var(--kt-ai-space-2)"><div class="progress-bar ' + getProgressTone(row.progress) + '" style="flex:1"><span style="width:' + row.progress + '%"></span></div><span class="progress-pct">' + row.progress + '%</span></div></div>' +
'</div></div>';
drawer.innerHTML += '<div class="kt-ai-drawer-actions"><button class="kt-ai-btn" data-variant="primary">Редактировать</button><button class="kt-ai-btn">Закрыть</button></div>';
document.body.appendChild(overlay);
document.body.appendChild(drawer);
function close() { overlay.remove(); drawer.remove(); }
document.getElementById('closeDrawer').addEventListener('click', close);
overlay.addEventListener('click', close);
drawer.querySelector('.kt-ai-btn:last-child').addEventListener('click', close);
}
render();
})();
</script>
</body>
</html>