corp-university-dashboard/index.html

178 lines
9.9 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">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Корпоративный университет — Дашборд</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Корпоративный университет</h1>
<p>Управленческий дашборд развития персонала</p>
</header>
<main>
<section class="upload-section" id="uploadSection">
<div class="upload-box">
<div class="upload-icon">📊</div>
<h2>Загрузите Excel-файл</h2>
<p>Столбцы файла: ФИО, Дивизион, Тема курса, Статус, Оценка, Тренер</p>
<input type="file" id="excelFile" accept=".xlsx,.xls,.csv" hidden>
<button id="uploadBtn">Выбрать файл</button>
<p class="hint" id="statusMsg"></p>
</div>
</section>
<section id="dashboard" class="dashboard hidden">
<div class="stats-row">
<div class="stat-card"><div class="stat-value" id="statTotal">0</div><div class="stat-label">Сотрудников</div></div>
<div class="stat-card"><div class="stat-value" id="statAvg"></div><div class="stat-label">Средняя оценка</div></div>
</div>
<div class="table-card wide">
<h3>📋 Нагрузка тренеров</h3>
<table id="tblTrainers"><thead><tr>
<th>ФИО тренера</th><th>Кол-во тренингов</th><th>Темы</th><th>Обучено чел.</th><th>Ср. оценка</th><th>Дивизионы</th>
</tr></thead><tbody></tbody></table>
</div>
<div class="table-card wide">
<h3>📋 Детализация по тренингам</h3>
<table id="tblTrainings"><thead><tr>
<th>Тренинг</th><th>Прошли</th><th>Не прошли</th><th>%</th><th>Оценка</th><th>Тренер</th>
</tr></thead><tbody></tbody></table>
</div>
<div class="charts-row">
<div class="table-card"><h3>🏆 Чемпионы</h3><table id="tblChamp"><thead><tr><th>ФИО</th><th>Дивизион</th><th>Курсов</th></tr></thead><tbody></tbody></table></div>
<div class="table-card"><h3>⚠️ Без обучения</h3><table id="tblNoTrain"><thead><tr><th>ФИО</th><th>Дивизион</th></tr></thead><tbody></tbody></table></div>
</div>
<div class="table-card wide">
<h3>Все записи</h3>
<table id="tblAll"><thead><tr>
<th>ФИО</th><th>Дивизион</th><th>Тема курса</th><th>Статус</th><th>Оценка</th><th>Тренер</th>
</tr></thead><tbody></tbody></table>
</div>
</section>
</main>
<script>
document.getElementById('uploadBtn').addEventListener('click', function() {
document.getElementById('excelFile').click();
});
document.getElementById('excelFile').addEventListener('change', function(e) {
var file = e.target.files[0];
if (file) {
document.getElementById('statusMsg').textContent = 'Выбран: ' + file.name;
var reader = new FileReader();
reader.onload = function(ev) {
try {
var arr = new Uint8Array(ev.target.result);
var wb = XLSX.read(arr, {type: 'array'});
var ws = wb.Sheets[wb.SheetNames[0]];
var rows = XLSX.utils.sheet_to_json(ws, {defval: ''});
if (rows.length > 0) {
document.getElementById('statusMsg').textContent = 'Загружено строк: ' + rows.length;
buildDashboard(rows);
}
} catch(err) {
document.getElementById('statusMsg').textContent = 'Ошибка: ' + err.message;
}
};
reader.readAsArrayBuffer(file);
}
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
function buildDashboard(raw) {
document.getElementById('uploadSection').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
var norm = {
'фио':'name','ф.и.о':'name','сотрудник':'name','employee':'name','имя':'name',
'дивизион':'div','подразделение':'div','отдел':'div','division':'div','department':'div',
'курс':'course','обучение':'course','training':'course','программа':'course',
'тренинг':'course','название тренинга':'course','название курса':'course','название обучения':'course',
'тема':'course','тема курса':'course','дисциплина':'course','модуль':'course','module':'course',
'статус':'status','status':'status','состояние':'status',
'оценка':'score','балл':'score','баллы':'score','рейтинг':'score','ball':'score','score':'score',
'тренер':'trainer','преподаватель':'trainer','trainer':'trainer','coach':'trainer',
'дата':'date','date':'date'
};
var d = raw.map(function(r) {
var out = {};
for (var k in r) {
var key = k.toLowerCase().replace(/\s+/g,' ').trim();
out[norm[key] || key] = r[k];
}
return out;
});
var allN = {}, scores = [], train = {}, trainers = {};
d.forEach(function(r) {
allN[r.name] = true;
var sc = parseFloat(r.score);
if (!isNaN(sc) && sc > 0) scores.push(sc);
var c = r.course || 'Без темы';
if (!train[c]) train[c] = {all:{}, ok:{}, sc:[], tr:{}};
train[c].all[r.name] = true;
var s = (r.status||'').toLowerCase();
if (s.indexOf('завершен')>=0 || s.indexOf('пройден')>=0 || s==='completed' || s==='ок') train[c].ok[r.name] = true;
if (!isNaN(sc) && sc > 0) train[c].sc.push(sc);
if (r.trainer) train[c].tr[r.trainer] = true;
var tr = r.trainer || '—';
if (!trainers[tr]) trainers[tr] = {n:0, sc:[], topics:[], trained:{}, divisions:{}};
trainers[tr].n++;
if (!isNaN(sc) && sc > 0) trainers[tr].sc.push(sc);
if (c && c !== 'Без темы') trainers[tr].topics.push(c);
if (r.name) trainers[tr].trained[r.name] = true;
var dv = r.division || '—';
trainers[tr].divisions[dv] = true;
});
document.getElementById('statTotal').textContent = Object.keys(allN).length;
document.getElementById('statAvg').textContent = scores.length ? (scores.reduce(function(a,b){return a+b},0)/scores.length).toFixed(1) : '—';
document.querySelector('#tblTrainings tbody').innerHTML = Object.keys(train).map(function(k){
var t=train[k], tot=Object.keys(t.all).length, ok=Object.keys(t.ok).length;
var avg=t.sc.length?(t.sc.reduce(function(a,b){return a+b},0)/t.sc.length).toFixed(1):'—';
var trn=Object.keys(t.tr).join(', ')||'—';
return '<tr><td><b>'+k+'</b></td><td>'+ok+'</td><td>'+(tot-ok)+'</td><td>'+(tot?Math.round(ok/tot*100):0)+'%</td><td>'+avg+'</td><td>'+trn+'</td></tr>';
}).join('');
document.querySelector('#tblTrainers tbody').innerHTML = Object.keys(trainers).map(function(tr) {
var t = trainers[tr];
var topics = (t.topics && t.topics.length > 0) ? Array.from(new Set(t.topics)).join(', ') : '—';
var avgScore = t.sc && t.sc.length ? (t.sc.reduce(function(a,b){return a+b},0)/t.sc.length).toFixed(1) : '—';
var trainedCount = t.trained ? Object.keys(t.trained).length : 0;
var divisions = t.divisions ? Array.from(new Set(Object.keys(t.divisions))).join(', ') : '—';
return '<tr><td><b>' + tr + '</b></td><td>' + t.n + '</td><td>' + topics + '</td><td>' + trainedCount + '</td><td>' + avgScore + '</td><td>' + divisions + '</td></tr>';
}).join('');
cph = {};
d.forEach(function(r){if(!cph[r.name])cph[r.name]={d:r.division||'',c:{} };if(r.course)cph[r.name].c[r.course]=true});
var champs=Object.keys(cph).map(function(n){return{name:n,div:cph[n].d,cnt:Object.keys(cph[n].c).length}})
.sort(function(a,b){return b.cnt-a.cnt}).slice(0,10);
document.querySelector('#tblChamp tbody').innerHTML = champs.map(function(c){
return '<tr><td>'+c.name+'</td><td>'+c.div+'</td><td>'+c.cnt+'</td></tr>';
}).join('');
document.querySelector('#tblNoTrain tbody').innerHTML = Object.keys(allN).slice(0,20).map(function(n){
return '<tr><td>'+n+'</td><td>'+(cph[n]?cph[n].d:'')+'</td></tr>';
}).join('');
document.querySelector('#tblAll tbody').innerHTML = d.map(function(r){
return '<tr><td>'+(r.name||'')+'</td><td>'+(r.division||'')+'</td><td>'+(r.course||'')+'</td><td>'+(r.status||'')+'</td><td>'+(r.score||'')+'</td><td>'+(r.trainer||'')+'</td></tr>';
}).join('');
}
</script>
</body>
</html>