From b26d2698d106978e3c208e90477247760e72b317 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 19 Jun 2026 06:19:22 +0000 Subject: [PATCH] =?UTF-8?q?v5=20complete=20rewrite=20from=20scratch,=20'?= =?UTF-8?q?=D1=82=D0=B5=D0=BC=D0=B0=20=D0=BA=D1=83=D1=80=D1=81=D0=B0'=20co?= =?UTF-8?q?lumn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 182 +++++++++++++++++++++++++++++++++ index.html | 177 ++++++++------------------------ script.js | 246 -------------------------------------------- style.css | 295 ++++++----------------------------------------------- 4 files changed, 256 insertions(+), 644 deletions(-) create mode 100644 app.js delete mode 100644 script.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..adf9c20 --- /dev/null +++ b/app.js @@ -0,0 +1,182 @@ +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'}}}}}); +} diff --git a/index.html b/index.html index d73cdda..0f70040 100644 --- a/index.html +++ b/index.html @@ -5,169 +5,76 @@ Корпоративный университет — Дашборд - -
-
-

Корпоративный университет

-

Управленческий дашборд развития персонала

-
+

Корпоративный университет

+

Управленческий дашборд развития персонала

-
+
📊
-

Загрузите данные

-

Перетащите Excel-файл или нажмите для выбора

+

Загрузите Excel-файл

+

Столбцы файла: ФИО, Дивизион, Тема курса, Статус, Оценка, Тренер

- -

Формат: столбцы — ФИО, Дивизион, Курс, Статус, Оценка, Тренер

-
-
- -
- - + + + diff --git a/script.js b/script.js deleted file mode 100644 index 84be5f6..0000000 --- a/script.js +++ /dev/null @@ -1,246 +0,0 @@ -let chartInstances = {}; - -const headerMap = { - 'фио': 'name', 'ф.и.о': 'name', 'сотрудник': 'name', 'employee': 'name', 'имя': 'name', - 'дивизион': 'division', 'подразделение': 'division', 'отдел': 'division', 'division': 'division', 'department': 'division', - 'курс': 'course', 'обучение': 'course', 'training': 'course', 'программа': 'course', - 'тренинг': 'course', 'название тренинга': 'course', 'название курса': 'course', 'название обучения': 'course', 'what': 'course', - 'название': 'course', 'тема': 'course', 'тема курса': 'course', 'дисциплина': 'course', 'модуль': 'course', 'training': 'course', 'program': 'course', 'программа обучения': 'course', 'обучающая программа': 'course', 'learning': 'course', - 'статус': 'status', 'status': 'status', 'состояние': 'status', - 'оценка': 'score', 'балл': 'score', 'баллы': 'score', 'рейтинг': 'score', - 'тренер': 'trainer', 'преподаватель': 'trainer', 'coach': 'trainer', - 'дата': 'date', 'дата прохождения': 'date' -}; - -document.getElementById('excelFile').addEventListener('change', function(e) { - var file = e.target.files[0]; - if (!file) return; - - console.log('Файл выбран:', file.name, file.size); - - var reader = new FileReader(); - reader.onload = function(event) { - try { - var data = new Uint8Array(event.target.result); - var workbook = XLSX.read(data, { type: 'array' }); - var firstSheet = workbook.Sheets[workbook.SheetNames[0]]; - var jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' }); - - console.log('Строк в файле:', jsonData.length); - console.log('Первая строка:', jsonData[0]); - - if (jsonData.length === 0) { - alert('Файл пуст или имеет неверный формат'); - return; - } - - var sampleRow = jsonData[0]; - var rawCols = Object.keys(sampleRow); - var normalized = normalizeHeaders([sampleRow])[0]; - var debugHtml = 'Оригинальные столбцы: ' + rawCols.join(', ') + '

'; - rawCols.forEach(function(k) { - var nk = k.toLowerCase().trim(); - var mapped = headerMap[nk] || nk; - var val = normalized[mapped] || '(пусто)'; - debugHtml += '' + k + ' → ' + mapped + ' = "' + val + '"
'; - }); - - var debugEl = document.getElementById('debug'); - var debugColsEl = document.getElementById('debugColumns'); - if (debugEl && debugColsEl) { - debugEl.classList.remove('hidden'); - debugColsEl.innerHTML = debugHtml; - } - - processAndRender(jsonData); - } catch (err) { - alert('Ошибка чтения файла: ' + err.message); - console.error(err); - } - }; - reader.readAsArrayBuffer(file); -}); - -function normalizeHeaders(rows) { - return rows.map(function(row) { - var normalized = {}; - for (var key in row) { - var cleanKey = key.toLowerCase().trim(); - var mapped = headerMap[cleanKey] || cleanKey; - normalized[mapped] = row[key]; - } - return normalized; - }); -} - -function isCompleted(status) { - var s = (status || '').toLowerCase(); - return s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок') || s.includes('пройден'); -} - -function processAndRender(rawData) { - var data = normalizeHeaders(rawData); - - document.querySelector('.upload-section').classList.add('hidden'); - document.getElementById('dashboard').classList.remove('hidden'); - - var allNames = new Set(); - var completedNames = new Set(); - var scores = []; - var trainingStats = {}; - var divisionStats = {}; - var trainerLoad = {}; - - data.forEach(function(r) { - allNames.add(r.name); - if (isCompleted(r.status)) completedNames.add(r.name); - - var sc = parseFloat(r.score); - if (!isNaN(sc) && sc > 0) scores.push(sc); - - var course = r.course || 'Без названия'; - if (!trainingStats[course]) trainingStats[course] = { completed: new Set(), all: new Set(), scores: [], trainers: new Set(), divisions: {} }; - trainingStats[course].all.add(r.name); - if (isCompleted(r.status)) trainingStats[course].completed.add(r.name); - if (!isNaN(sc) && sc > 0) trainingStats[course].scores.push(sc); - if (r.trainer) trainingStats[course].trainers.add(r.trainer); - var div = r.division || 'Без дивизиона'; - if (!trainingStats[course].divisions[div]) trainingStats[course].divisions[div] = { total: new Set(), completed: new Set() }; - trainingStats[course].divisions[div].total.add(r.name); - if (isCompleted(r.status)) trainingStats[course].divisions[div].completed.add(r.name); - - if (!divisionStats[div]) divisionStats[div] = { total: new Set(), completed: new Set() }; - divisionStats[div].total.add(r.name); - if (isCompleted(r.status)) divisionStats[div].completed.add(r.name); - - var t = r.trainer || 'Не указан'; - if (!trainerLoad[t]) trainerLoad[t] = { count: 0, scores: [] }; - trainerLoad[t].count++; - if (!isNaN(sc) && sc > 0) trainerLoad[t].scores.push(sc); - }); - - var totalEmployees = allNames.size; - var completedCount = completedNames.size; - var rate = totalEmployees ? Math.round(completedCount / totalEmployees * 100) : 0; - var avgScore = scores.length ? (scores.reduce(function(a, b) { return a + b; }, 0) / scores.length).toFixed(1) : '—'; - - document.getElementById('totalEmployees').textContent = totalEmployees; - document.getElementById('completedCount').textContent = completedCount; - document.getElementById('completionRate').textContent = rate + '%'; - document.getElementById('avgScore').textContent = avgScore; - - var trainingLabels = Object.keys(trainingStats); - var trainingCompletedCounts = trainingLabels.map(function(t) { return trainingStats[t].completed.size; }); - var trainingTotalCounts = trainingLabels.map(function(t) { return trainingStats[t].all.size; }); - - renderChart('trainingChart', 'bar', { - labels: trainingLabels, - datasets: [ - { label: 'Всего записей', data: trainingTotalCounts, backgroundColor: 'rgba(33,150,243,0.4)', borderColor: '#2196F3', borderWidth: 1 }, - { label: 'Прошли', data: trainingCompletedCounts, backgroundColor: 'rgba(76,175,80,0.7)', borderColor: '#4caf50', borderWidth: 1 } - ] - }, { indexAxis: 'y', scales: { x: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, y: { ticks: { color: '#e0e0e0', font: { size: 11 } }, grid: { color: '#1e2d3d' } } } }); - - var allDivisions = Object.keys(divisionStats); - var colors = ['#2196F3', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', '#ff5722', '#8bc34a', '#e91e63']; - var trainingByDivDatasets = allDivisions.map(function(div, i) { - return { - label: div, - data: trainingLabels.map(function(t) { return trainingStats[t].divisions[div] ? trainingStats[t].divisions[div].completed.size : 0; }), - backgroundColor: colors[i % colors.length] + 'aa', - borderColor: colors[i % colors.length], - borderWidth: 1 - }; - }); - - renderChart('trainingByDivisionChart', 'bar', { - labels: trainingLabels, - datasets: trainingByDivDatasets - }, { scales: { x: { stacked: true, ticks: { color: '#90a4ae', maxRotation: 45 }, grid: { color: '#1e2d3d' } }, y: { stacked: true, beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } } }); - - renderChart('divisionChart', 'bar', { - labels: Object.keys(divisionStats), - datasets: [ - { label: 'Всего', data: Object.values(divisionStats).map(function(d) { return d.total.size; }), backgroundColor: 'rgba(33,150,243,0.6)', borderColor: '#2196F3', borderWidth: 1 }, - { label: 'Прошли', data: Object.values(divisionStats).map(function(d) { return d.completed.size; }), backgroundColor: 'rgba(76,175,80,0.6)', borderColor: '#4caf50', borderWidth: 1 } - ] - }); - - var trainerLabels = Object.keys(trainerLoad); - renderChart('trainerChart', 'bar', { - labels: trainerLabels, - datasets: [ - { label: 'Занятий', data: trainerLabels.map(function(t) { return trainerLoad[t].count; }), backgroundColor: 'rgba(255,152,0,0.6)', borderColor: '#ff9800', borderWidth: 1 }, - { label: 'Средняя оценка', data: trainerLabels.map(function(t) { var sc = trainerLoad[t].scores; return sc.length ? (sc.reduce(function(a, b) { return a + b; }, 0) / sc.length).toFixed(1) : 0; }), backgroundColor: 'rgba(156,39,176,0.6)', borderColor: '#9c27b0', borderWidth: 1, yAxisID: 'y1' } - ] - }, { scales: { y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, y1: { position: 'right', beginAtZero: true, max: 5, ticks: { color: '#90a4ae' }, grid: { display: false } }, x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } } }); - - var scoreDist = { '5': 0, '4': 0, '3': 0, '2': 0, '1': 0 }; - scores.forEach(function(s) { var r = Math.round(s); if (scoreDist[String(r)] !== undefined) scoreDist[String(r)]++; }); - renderChart('scoreChart', 'doughnut', { - labels: Object.keys(scoreDist).map(function(k) { return 'Оценка ' + k; }), - datasets: [{ data: Object.values(scoreDist), backgroundColor: ['#4caf50', '#8bc34a', '#ff9800', '#ff5722', '#f44336'], borderWidth: 0 }] - }); - - var statusCounts = {}; - data.forEach(function(r) { var s = (r.status || 'Неизвестно').trim(); statusCounts[s] = (statusCounts[s] || 0) + 1; }); - renderChart('statusChart', 'pie', { - labels: Object.keys(statusCounts), - datasets: [{ data: Object.values(statusCounts), backgroundColor: ['#4caf50', '#ff9800', '#f44336', '#2196F3', '#9c27b0', '#00bcd4'], borderWidth: 0 }] - }); - - var trainingAvgScores = trainingLabels.map(function(t) { var sc = trainingStats[t].scores; return sc.length ? (sc.reduce(function(a, b) { return a + b; }, 0) / sc.length).toFixed(1) : 0; }); - renderChart('trainingScoreChart', 'bar', { - labels: trainingLabels, - datasets: [{ label: 'Средняя оценка', data: trainingAvgScores, backgroundColor: trainingAvgScores.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)'; }), borderColor: trainingAvgScores.map(function(v) { return v >= 4 ? '#4caf50' : v >= 3 ? '#ff9800' : '#f44336'; }), borderWidth: 1 }] - }, { scales: { y: { beginAtZero: true, max: 5, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, x: { ticks: { color: '#e0e0e0', maxRotation: 45 }, grid: { color: '#1e2d3d' } } } }); - - var trainingDetailBody = document.querySelector('#trainingDetailTable tbody'); - trainingDetailBody.innerHTML = trainingLabels.map(function(t) { - var st = trainingStats[t]; - var total = st.all.size; - var done = st.completed.size; - var pct = total ? Math.round(done / total * 100) : 0; - var avg = st.scores.length ? (st.scores.reduce(function(a, b) { return a + b; }, 0) / st.scores.length).toFixed(1) : '—'; - var trainers = Array.from(st.trainers).join(', ') || '—'; - return '' + t + '' + done + '' + (total - done) + '' + pct + '%' + avg + '' + trainers + ''; - }).join(''); - - var coursesPerPerson = {}; - data.forEach(function(r) { - if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() }; - if (r.course) coursesPerPerson[r.name].courses.add(r.course); - }); - var champions = Object.entries(coursesPerPerson) - .map(function(e) { return { name: e[0], division: e[1].division, count: e[1].courses.size }; }) - .sort(function(a, b) { return b.count - a.count; }) - .slice(0, 10); - document.querySelector('#championsTable tbody').innerHTML = champions.map(function(c) { - return '' + c.name + '' + c.division + '' + c.count + ''; - }).join(''); - - var noTraining = Array.from(allNames).filter(function(n) { return !completedNames.has(n); }); - var noTrainingDivs = {}; - data.forEach(function(r) { if (noTraining.indexOf(r.name) !== -1) noTrainingDivs[r.name] = r.division || ''; }); - document.querySelector('#noTrainingTable tbody').innerHTML = noTraining.slice(0, 20).map(function(n) { - return '' + n + '' + (noTrainingDivs[n] || '') + ''; - }).join(''); - - document.querySelector('#detailTable tbody').innerHTML = data.map(function(r) { - var cls = isCompleted(r.status) ? 'status-completed' : 'status-not-started'; - return '' + (r.name || '') + '' + (r.division || '') + '' + (r.course || '') + '' + (r.status || '') + '' + (r.score || '') + '' + (r.trainer || '') + ''; - }).join(''); -} - -function renderChart(canvasId, type, chartData, extraOptions) { - if (chartInstances[canvasId]) chartInstances[canvasId].destroy(); - var defaults = { responsive: true, maintainAspectRatio: true, plugins: { legend: { labels: { color: '#e0e0e0', font: { size: 12 } } } } }; - if (type === 'bar' || type === 'line') { - defaults.scales = { y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } }; - } - if (extraOptions) { - Object.keys(extraOptions).forEach(function(k) { defaults[k] = extraOptions[k]; }); - } - var ctx = document.getElementById(canvasId).getContext('2d'); - chartInstances[canvasId] = new Chart(ctx, { type: type, data: chartData, options: defaults }); -} diff --git a/style.css b/style.css index ae73474..2400486 100644 --- a/style.css +++ b/style.css @@ -1,263 +1,32 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #0f1923; - color: #e0e0e0; - min-height: 100vh; -} - -header { - background: linear-gradient(135deg, #1a2a3a 0%, #0d2137 100%); - border-bottom: 2px solid #2196F3; - padding: 24px 40px; -} - -header h1 { - font-size: 28px; - font-weight: 700; - color: #fff; -} - -header p { - color: #90a4ae; - margin-top: 4px; - font-size: 14px; -} - -main { - max-width: 1400px; - margin: 0 auto; - padding: 32px 40px; -} - -.upload-section { - display: flex; - justify-content: center; - align-items: center; - min-height: 60vh; -} - -.upload-box { - background: #1a2a3a; - border: 2px dashed #2196F3; - border-radius: 16px; - padding: 60px 80px; - text-align: center; - cursor: pointer; - transition: all 0.3s; -} - -.upload-box:hover { - border-color: #64b5f6; - background: #1e3348; -} - -.upload-icon { - font-size: 64px; - margin-bottom: 16px; -} - -.upload-box h2 { - font-size: 24px; - color: #fff; - margin-bottom: 8px; -} - -.upload-box p { - color: #90a4ae; - margin-bottom: 20px; -} - -.upload-box button { - background: #2196F3; - color: #fff; - border: none; - padding: 14px 32px; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: background 0.2s; -} - -.upload-box button:hover { - background: #1976D2; -} - -.hint { - font-size: 12px !important; - color: #607d8b !important; - margin-top: 16px !important; -} - -.debug-section { - margin-bottom: 24px; -} - -.debug-section.hidden { - display: none; -} - -.debug-box { - background: #1a2a3a; - border: 1px solid #ff9800; - border-radius: 12px; - padding: 16px 24px; -} - -.debug-box h3 { - color: #ff9800; - font-size: 14px; - margin-bottom: 8px; -} - -.debug-box p { - color: #e0e0e0; - font-size: 13px; - font-family: monospace; - word-break: break-all; -} - -.dashboard.hidden { - display: none; -} - -.stats-row { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 20px; - margin-bottom: 32px; -} - -.stat-card { - background: #1a2a3a; - border-radius: 12px; - padding: 24px; - text-align: center; - border: 1px solid #263545; -} - -.stat-value { - font-size: 36px; - font-weight: 700; - color: #2196F3; -} - -.stat-label { - font-size: 13px; - color: #90a4ae; - margin-top: 6px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.charts-row { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; - margin-bottom: 20px; -} - -.chart-card { - background: #1a2a3a; - border-radius: 12px; - padding: 24px; - border: 1px solid #263545; -} - -.chart-card.wide { - grid-column: 1 / -1; -} - -.chart-card h3 { - font-size: 16px; - color: #fff; - margin-bottom: 16px; -} - -.tables-row { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; - margin-bottom: 20px; -} - -.table-card { - background: #1a2a3a; - border-radius: 12px; - padding: 24px; - border: 1px solid #263545; -} - -.table-card.full-width { - margin-bottom: 20px; -} - -.table-card h3 { - font-size: 16px; - color: #fff; - margin-bottom: 16px; -} - -.table-wrapper { - overflow-x: auto; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th { - text-align: left; - padding: 12px 16px; - background: #0f1923; - color: #90a4ae; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 1px solid #263545; -} - -td { - padding: 12px 16px; - border-bottom: 1px solid #1e2d3d; - font-size: 14px; -} - -tr:hover td { - background: #1e3348; -} - -.status-completed { - color: #4caf50; - font-weight: 600; -} - -.status-in-progress { - color: #ff9800; - font-weight: 600; -} - -.status-not-started { - color: #f44336; - font-weight: 600; -} - -@media (max-width: 900px) { - .stats-row { - grid-template-columns: repeat(2, 1fr); - } - .charts-row, - .tables-row { - grid-template-columns: 1fr; - } - main { - padding: 20px; - } -} +*{margin:0;padding:0;box-sizing:border-box} +body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f1923;color:#e0e0e0;min-height:100vh} +header{background:linear-gradient(135deg,#1a2a3a,#0d2137);border-bottom:2px solid #2196F3;padding:24px 40px} +header h1{font-size:28px;color:#fff} +header p{color:#90a4ae;margin-top:4px;font-size:14px} +main{max-width:1400px;margin:0 auto;padding:32px 40px} +.upload-section{display:flex;justify-content:center;align-items:center;min-height:60vh} +.upload-box{background:#1a2a3a;border:2px dashed #2196F3;border-radius:16px;padding:60px 80px;text-align:center} +.upload-icon{font-size:64px;margin-bottom:16px} +.upload-box h2{font-size:24px;color:#fff;margin-bottom:8px} +.upload-box p{color:#90a4ae;margin-bottom:20px} +.upload-box button{background:#2196F3;color:#fff;border:none;padding:14px 32px;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer} +.upload-box button:hover{background:#1976D2} +.hint{font-size:12px!important;color:#607d8b!important;margin-top:16px!important} +.dashboard.hidden{display:none} +.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;margin-bottom:24px} +.stat-card{background:#1a2a3a;border-radius:12px;padding:24px;text-align:center;border:1px solid #263545} +.stat-value{font-size:36px;font-weight:700;color:#2196F3} +.stat-label{font-size:13px;color:#90a4ae;margin-top:6px;text-transform:uppercase;letter-spacing:.5px} +.charts-row{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;margin-bottom:20px} +.chart-card{background:#1a2a3a;border-radius:12px;padding:24px;border:1px solid #263545;margin-bottom:20px} +.chart-card.wide{margin-bottom:20px} +.chart-card h3,.table-card h3{font-size:16px;color:#fff;margin-bottom:16px} +.table-card{background:#1a2a3a;border-radius:12px;padding:24px;border:1px solid #263545;margin-bottom:20px} +.table-card.wide{margin-bottom:20px} +table{width:100%;border-collapse:collapse} +th{text-align:left;padding:12px 16px;background:#0f1923;color:#90a4ae;font-size:12px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #263545} +td{padding:12px 16px;border-bottom:1px solid #1e2d3d;font-size:14px} +tr:hover td{background:#1e3348} +.status-completed{color:#4caf50;font-weight:600} +.status-not-started{color:#f44336;font-weight:600} +@media(max-width:900px){.stats-row{grid-template-columns:repeat(2,1fr)}.charts-row{grid-template-columns:1fr}main{padding:20px}}