v5 complete rewrite from scratch, 'тема курса' column

This commit is contained in:
Dauren777 2026-06-19 06:19:22 +00:00
parent 887b2d0949
commit b26d2698d1
4 changed files with 256 additions and 644 deletions

182
app.js Normal file
View File

@ -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 '<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'}}}}});
}

View File

@ -5,169 +5,76 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Корпоративный университет — Дашборд</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
</head>
<body>
<header>
<div class="header-content">
<h1>Корпоративный университет</h1>
<p>Управленческий дашборд развития персонала</p>
</div>
<h1>Корпоративный университет</h1>
<p>Управленческий дашборд развития персонала</p>
</header>
<main>
<section class="upload-section">
<section class="upload-section" id="uploadSection">
<div class="upload-box">
<div class="upload-icon">📊</div>
<h2>Загрузите данные</h2>
<p>Перетащите Excel-файл или нажмите для выбора</p>
<h2>Загрузите Excel-файл</h2>
<p>Столбцы файла: ФИО, Дивизион, Тема курса, Статус, Оценка, Тренер</p>
<input type="file" id="excelFile" accept=".xlsx,.xls,.csv" hidden>
<button onclick="document.getElementById('excelFile').click()">Выбрать файл</button>
<p class="hint">Формат: столбцы — ФИО, Дивизион, Курс, Статус, Оценка, Тренер</p>
</div>
</section>
<section id="debug" class="debug-section hidden">
<div class="debug-box">
<h3>Распознанные столбцы:</h3>
<p id="debugColumns"></p>
<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="totalEmployees">0</div>
<div class="stat-label">Сотрудников</div>
</div>
<div class="stat-card">
<div class="stat-value" id="completedCount">0</div>
<div class="stat-label">Прошли обучение</div>
</div>
<div class="stat-card">
<div class="stat-value" id="completionRate">0%</div>
<div class="stat-label">Процент прохождения</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgScore">0</div>
<div class="stat-label">Средняя оценка</div>
</div>
<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="statDone">0</div><div class="stat-label">Прошли обучение</div></div>
<div class="stat-card"><div class="stat-value" id="statRate">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="chart-card wide">
<h3>Тренинги — сколько человек прошли</h3>
<canvas id="chartTrainings"></canvas>
</div>
<div class="charts-row">
<div class="chart-card wide">
<h3>Тренинги — количество прошедших</h3>
<canvas id="trainingChart"></canvas>
</div>
<div class="chart-card"><h3>Тренинги по дивизионам</h3><canvas id="chartTrainDiv"></canvas></div>
<div class="chart-card"><h3>Дивизионы — кто прошёл</h3><canvas id="chartDiv"></canvas></div>
</div>
<div class="charts-row">
<div class="chart-card">
<h3>Тренинги по дивизионам</h3>
<canvas id="trainingByDivisionChart"></canvas>
</div>
<div class="chart-card">
<h3>Прохождение по дивизионам</h3>
<canvas id="divisionChart"></canvas>
</div>
<div class="chart-card"><h3>Нагрузка тренеров</h3><canvas id="chartTrainers"></canvas></div>
<div class="chart-card"><h3>Оценки</h3><canvas id="chartScores"></canvas></div>
</div>
<div class="chart-card wide">
<h3>Средняя оценка по тренингам</h3>
<canvas id="chartTrainScores"></canvas>
</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="chart-card">
<h3>Нагрузка тренеров</h3>
<canvas id="trainerChart"></canvas>
</div>
<div class="chart-card">
<h3>Распределение оценок</h3>
<canvas id="scoreChart"></canvas>
</div>
<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="charts-row">
<div class="chart-card">
<h3>Средняя оценка по тренингам</h3>
<canvas id="trainingScoreChart"></canvas>
</div>
<div class="chart-card">
<h3>Статусы обучения</h3>
<canvas id="statusChart"></canvas>
</div>
</div>
<div class="table-card full-width">
<h3>📋 Кто какие тренинги прошёл</h3>
<div class="table-wrapper">
<table id="trainingDetailTable">
<thead>
<tr>
<th>Тренинг</th>
<th>Прошли</th>
<th>Не прошли</th>
<th>% прохождения</th>
<th>Средняя оценка</th>
<th>Тренер</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tables-row">
<div class="table-card">
<h3>🏆 Чемпионы по обучению</h3>
<table id="championsTable">
<thead>
<tr>
<th>ФИО</th>
<th>Дивизион</th>
<th>Курсов пройдено</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-card">
<h3>⚠️ Без обучения</h3>
<table id="noTrainingTable">
<thead>
<tr>
<th>ФИО</th>
<th>Дивизион</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="table-card full-width">
<h3>Детализация по сотрудникам</h3>
<div class="table-wrapper">
<table id="detailTable">
<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="tblAll"><thead><tr>
<th>ФИО</th><th>Дивизион</th><th>Тема курса</th><th>Статус</th><th>Оценка</th><th>Тренер</th>
</tr></thead><tbody></tbody></table>
</div>
</section>
</main>
<script>
if (typeof XLSX === 'undefined') {
document.querySelector('.upload-box').innerHTML = '<div class="upload-icon">⚠️</div><h2>Ошибка загрузки</h2><p>Не удалось загрузить библиотеку для чтения Excel. Проверьте интернет-соединение и обновите страницу.</p>';
}
</script>
<script src="script.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="app.js"></script>
</body>
</html>

246
script.js
View File

@ -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 = '<strong>Оригинальные столбцы:</strong> ' + rawCols.join(', ') + '<br><br>';
rawCols.forEach(function(k) {
var nk = k.toLowerCase().trim();
var mapped = headerMap[nk] || nk;
var val = normalized[mapped] || '(пусто)';
debugHtml += '<b>' + k + '</b> → ' + mapped + ' = "' + val + '"<br>';
});
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 '<tr><td><strong>' + t + '</strong></td><td>' + done + '</td><td>' + (total - done) + '</td><td>' + pct + '%</td><td>' + avg + '</td><td>' + trainers + '</td></tr>';
}).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 '<tr><td>' + c.name + '</td><td>' + c.division + '</td><td>' + c.count + '</td></tr>';
}).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 '<tr><td>' + n + '</td><td>' + (noTrainingDivs[n] || '') + '</td></tr>';
}).join('');
document.querySelector('#detailTable tbody').innerHTML = data.map(function(r) {
var cls = isCompleted(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 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 });
}

295
style.css
View File

@ -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}}