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 = {}, 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 ''+k+''+ok+''+(tot-ok)+''+(tot?Math.round(ok/tot*100):0)+'%'+avg+''+trn+''; }).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 ''+c.name+''+c.div+''+c.cnt+''; }).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 ''+n+''+(cph[n]?cph[n].d:'')+''; }).join(''); document.querySelector('#tblAll tbody').innerHTML = d.map(function(r){ var cls=done(r.status)?'status-completed':'status-not-started'; return ''+(r.name||'')+''+(r.division||'')+''+(r.course||'')+''+(r.status||'')+''+(r.score||'')+''+(r.trainer||'')+''; }).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'}}}}}); }