corp-university-dashboard/app.js

157 lines
8.6 KiB
JavaScript
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.

var charts = {};
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) return;
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) { document.getElementById('statusMsg').textContent = 'Файл пуст'; return; }
build(rows);
} catch(err) {
document.getElementById('statusMsg').textContent = 'Ошибка: ' + err.message;
}
};
reader.readAsArrayBuffer(file);
});
function norm(rows) {
var map = {
'фио':'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'
};
return rows.map(function(r) {
var out = {};
for (var k in r) {
var key = k.toLowerCase().replace(/\s+/g,' ').trim();
out[map[key] || key] = r[k];
}
return out;
});
}
function done(s) {
s = (s||'').toLowerCase();
return s.indexOf('завершен')>=0 || s.indexOf('прошёл')>=0 || s.indexOf('прошел')>=0 ||
s.indexOf('пройден')>=0 || s==='completed' || s==='ок' || s==='да';
}
function build(raw) {
var d = norm(raw);
document.getElementById('uploadSection').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
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;
if (r.status === 'завершен' || r.status === 'пройден') 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:[]};
trainers[tr].n++;
if (!isNaN(sc) && sc > 0) trainers[tr].sc.push(sc);
if (c && c !== 'Без темы') trainers[tr].topics.push(c);
});
var totalN = Object.keys(allN).length;
document.getElementById('statTotal').textContent = totalN;
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) : '—';
return '<tr><td><b>' + tr + '</b></td><td>' + t.n + '</td><td>' + topics + '</td><td>' + avgScore + '</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('');
var noT = Object.keys(allN);
document.querySelector('#tblNoTrain tbody').innerHTML = noT.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('');
var 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('');
var noT=Object.keys(allN).filter(function(n){return !doneN[n]});
document.querySelector('#tblNoTrain tbody').innerHTML = noT.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){
var cls=done(r.status)?'status-completed':'status-not-started';
return '<tr><td>'+(r.name||'')+'</td><td>'+(r.division||'')+'</td><td>'+(r.course||'')+'</td><td class="'+cls+'">'+(r.status||'')+'</td><td>'+(r.score||'')+'</td><td>'+(r.trainer||'')+'</td></tr>';
}).join('');
}
function barH(id, labels, ds) {
if(charts[id])charts[id].destroy();
charts[id]=new Chart(document.getElementById(id),{type:'bar',data:{labels:labels,datasets:ds.map(function(s){return{label:s.label,data:s.data,backgroundColor:s.bg,borderColor:s.bc,borderWidth:1}})},
options:{indexAxis:'y',responsive:true,plugins:{legend:{labels:{color:'#e0e0e0'}}},scales:{x:{beginAtZero:true,ticks:{color:'#90a4ae'},grid:{color:'#1e2d3d'}},y:{ticks:{color:'#e0e0e0',font:{size:11}},grid:{color:'#1e2d3d'}}}}});
}
function bar(id, labels, ds) {
if(charts[id])charts[id].destroy();
charts[id]=new Chart(document.getElementById(id),{type:'bar',data:{labels:labels,datasets:ds.map(function(s){return{label:s.label,data:s.data,backgroundColor:s.bg,borderColor:s.bc,borderWidth:1}})},
options:{responsive:true,plugins:{legend:{labels:{color:'#e0e0e0'}}},scales:{y:{beginAtZero:true,ticks:{color:'#90a4ae'},grid:{color:'#1e2d3d'}},x:{ticks:{color:'#90a4ae'},grid:{color:'#1e2d3d'}}}}});
}
function stacked(id, labels, ds) {
if(charts[id])charts[id].destroy();
charts[id]=new Chart(document.getElementById(id),{type:'bar',data:{labels:labels,datasets:ds.map(function(s){return{label:s.label,data:s.data,backgroundColor:s.bg,borderColor:s.bc,borderWidth:1}})},
options:{responsive:true,plugins:{legend:{labels:{color:'#e0e0e0'}}},scales:{x:{stacked:true,ticks:{color:'#90a4ae',maxRotation:45},grid:{color:'#1e2d3d'}},y:{stacked:true,beginAtZero:true,ticks:{color:'#90a4ae'},grid:{color:'#1e2d3d'}}}}});
}
function doughnut(id, labels, data, colors) {
if(charts[id])charts[id].destroy();
charts[id]=new Chart(document.getElementById(id),{type:'doughnut',data:{labels:labels,datasets:[{data:data,backgroundColor:colors,borderWidth:0}]},
options:{responsive:true,plugins:{legend:{labels:{color:'#e0e0e0'}}}}});
}