corp-university-dashboard/app.js
2026-06-19 06:25:57 +00:00

190 lines
10 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; }
console.log('Данные файла:', rows);
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);
console.log('Обработанные данные:', d);
console.log('Количество строк:', d.length);
d.forEach(function(r, i) {
console.log('Строка ' + i + ':\n ФИО:', r.name, '\n Статус:', r.status, '\n Обработанный статус:', r.status ? 'done' : 'not done');
});
document.getElementById('uploadSection').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
var allN = {}, doneN = {}, scores = [], train = {}, divs = {}, trainers = {};
d.forEach(function(r) {
allN[r.name] = true;
if (done(r.status)) doneN[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:{}, div:{}};
train[c].all[r.name] = true;
if (done(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 dv = r.division || '—';
if (!train[c].div[dv]) train[c].div[dv] = {all:{}, ok:{}};
train[c].div[dv].all[r.name] = true;
if (done(r.status)) train[c].div[dv].ok[r.name] = true;
if (!divs[dv]) divs[dv] = {all:{}, ok:{}};
divs[dv].all[r.name] = true;
if (done(r.status)) divs[dv].ok[r.name] = true;
var tr = r.trainer || '—';
if (!trainers[tr]) trainers[tr] = {n:0, sc:[]};
trainers[tr].n++;
if (!isNaN(sc) && sc > 0) trainers[tr].sc.push(sc);
});
var totalN = Object.keys(allN).length;
var doneNn = Object.keys(doneN).length;
document.getElementById('statTotal').textContent = totalN;
document.getElementById('statDone').textContent = doneNn;
document.getElementById('statRate').textContent = totalN ? Math.round(doneNn/totalN*100)+'%' : '0%';
document.getElementById('statAvg').textContent = scores.length ? (scores.reduce(function(a,b){return a+b},0)/scores.length).toFixed(1) : '—';
var tKeys = Object.keys(train);
var tDone = tKeys.map(function(k){return Object.keys(train[k].ok).length});
var tAll = tKeys.map(function(k){return Object.keys(train[k].all).length});
barH('chartTrainings', tKeys, [
{label:'Всего', data:tAll, bg:'rgba(33,150,243,0.4)', bc:'#2196F3'},
{label:'Прошли', data:tDone, bg:'rgba(76,175,80,0.7)', bc:'#4caf50'}
]);
var dvNames = Object.keys(divs);
var dvColors = ['#2196F3','#4caf50','#ff9800','#9c27b0','#00bcd4','#ff5722','#8bc34a','#e91e63'];
var divDatasets = dvNames.map(function(dv,i){
return {label:dv, data:tKeys.map(function(tk){return train[tk].div[dv]?Object.keys(train[tk].div[dv].ok).length:0}),
bg:dvColors[i%dvColors.length]+'cc', bc:dvColors[i%dvColors.length]};
});
stacked('chartTrainDiv', tKeys, divDatasets);
bar('chartDiv', dvNames, [
{label:'Всего', data:dvNames.map(function(d){return Object.keys(divs[d].all).length}), bg:'rgba(33,150,243,0.6)', bc:'#2196F3'},
{label:'Прошли', data:dvNames.map(function(d){return Object.keys(divs[d].ok).length}), bg:'rgba(76,175,80,0.6)', bc:'#4caf50'}
]);
var trNames = Object.keys(trainers);
bar('chartTrainers', trNames, [
{label:'Занятий', data:trNames.map(function(t){return trainers[t].n}), bg:'rgba(255,152,0,0.6)', bc:'#ff9800'}
]);
var sd = {5:0,4:0,3:0,2:0,1:0};
scores.forEach(function(s){var r=Math.round(s);if(sd[r]!==undefined)sd[r]++});
doughnut('chartScores', Object.keys(sd).map(function(k){return 'Оценка '+k}), Object.values(sd),
['#4caf50','#8bc34a','#ff9800','#ff5722','#f44336']);
var tAvg = tKeys.map(function(k){var sc=train[k].sc;return sc.length?(sc.reduce(function(a,b){return a+b},0)/sc.length).toFixed(1):0});
bar('chartTrainScores', tKeys, [{label:'Средняя оценка', data:tAvg,
bg:tAvg.map(function(v){return v>=4?'rgba(76,175,80,0.7)':v>=3?'rgba(255,152,0,0.7)':'rgba(244,67,54,0.7)'}),
bc:tAvg.map(function(v){return v>=4?'#4caf50':v>=3?'#ff9800':'#f44336'})}]);
document.querySelector('#tblTrainings tbody').innerHTML = tKeys.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('');
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'}}}}});
}