v4 rewrite with error handling, fix upload
This commit is contained in:
parent
ab8fc2e8ac
commit
fc1ef2c650
423
script.js
423
script.js
@ -4,12 +4,12 @@ const headerMap = {
|
|||||||
'фио': 'name', 'ф.и.о': 'name', 'сотрудник': 'name', 'employee': 'name', 'имя': 'name',
|
'фио': 'name', 'ф.и.о': 'name', 'сотрудник': 'name', 'employee': 'name', 'имя': 'name',
|
||||||
'дивизион': 'division', 'подразделение': 'division', 'отдел': 'division', 'division': 'division', 'department': 'division',
|
'дивизион': 'division', 'подразделение': 'division', 'отдел': 'division', 'division': 'division', 'department': 'division',
|
||||||
'курс': 'course', 'обучение': 'course', 'training': 'course', 'программа': 'course',
|
'курс': 'course', 'обучение': 'course', 'training': 'course', 'программа': 'course',
|
||||||
'тренинг': 'course', 'тренинг/курс': 'course', 'название тренинга': 'course', 'название курса': 'course',
|
'тренинг': 'course', 'название тренинга': 'course', 'название курса': 'course',
|
||||||
'what': 'course', 'название': 'course', 'тема': 'course', 'дисциплина': 'course', 'module': 'course', 'модуль': 'course',
|
'название': 'course', 'тема': 'course', 'дисциплина': 'course', 'модуль': 'course',
|
||||||
'статус': 'status', 'status': 'status', 'состояние': 'status',
|
'статус': 'status', 'status': 'status', 'состояние': 'status',
|
||||||
'оценка': 'score', 'ball': 'score', 'score': 'score', 'рейтинг': 'score', 'балл': 'score',
|
'оценка': 'score', 'балл': 'score', 'баллы': 'score', 'рейтинг': 'score',
|
||||||
'тренер': 'trainer', 'trainer': 'coach', 'преподаватель': 'trainer', 'coach': 'trainer',
|
'тренер': 'trainer', 'преподаватель': 'trainer', 'coach': 'trainer',
|
||||||
'дата': 'date', 'date': 'date', 'дата прохождения': 'date'
|
'дата': 'date', 'дата прохождения': 'date'
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('excelFile').addEventListener('change', function(e) {
|
document.getElementById('excelFile').addEventListener('change', function(e) {
|
||||||
@ -18,124 +18,130 @@ document.getElementById('excelFile').addEventListener('change', function(e) {
|
|||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function(event) {
|
||||||
const data = new Uint8Array(event.target.result);
|
try {
|
||||||
const workbook = XLSX.read(data, { type: 'array' });
|
const data = new Uint8Array(event.target.result);
|
||||||
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
|
const workbook = XLSX.read(data, { type: 'array' });
|
||||||
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' });
|
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' });
|
||||||
|
|
||||||
if (jsonData.length === 0) {
|
if (jsonData.length === 0) {
|
||||||
alert('Файл пуст или имеет неверный формат');
|
alert('Файл пуст или имеет неверный формат');
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleRow = jsonData[0] || {};
|
|
||||||
const rawCols = Object.keys(sampleRow);
|
|
||||||
const normalized = normalizeHeaders([sampleRow])[0];
|
|
||||||
const mapping = rawCols.map(k => {
|
|
||||||
const nk = k.toLowerCase().trim();
|
|
||||||
const mapped = headerMap[nk] || nk;
|
|
||||||
const val = normalized[mapped] || '(пусто)';
|
|
||||||
return `<b>${k}</b> → ${mapped} = "${val}"`;
|
|
||||||
}).join('<br>');
|
|
||||||
|
|
||||||
document.getElementById('debug').classList.remove('hidden');
|
|
||||||
document.getElementById('debugColumns').innerHTML =
|
|
||||||
`<strong>Оригинальные столбцы:</strong> ${rawCols.join(', ')}<br><br>${mapping}`;
|
|
||||||
|
|
||||||
processAndRender(jsonData);
|
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
function normalizeHeaders(rows) {
|
function normalizeHeaders(rows) {
|
||||||
return rows.map(row => {
|
return rows.map(function(row) {
|
||||||
const normalized = {};
|
var normalized = {};
|
||||||
for (const key in row) {
|
for (var key in row) {
|
||||||
const cleanKey = key.toLowerCase().trim();
|
var cleanKey = key.toLowerCase().trim();
|
||||||
const mapped = headerMap[cleanKey] || cleanKey;
|
var mapped = headerMap[cleanKey] || cleanKey;
|
||||||
normalized[mapped] = row[key];
|
normalized[mapped] = row[key];
|
||||||
}
|
}
|
||||||
return normalized;
|
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) {
|
function processAndRender(rawData) {
|
||||||
const data = normalizeHeaders(rawData);
|
var data = normalizeHeaders(rawData);
|
||||||
|
|
||||||
document.querySelector('.upload-section').classList.add('hidden');
|
document.querySelector('.upload-section').classList.add('hidden');
|
||||||
document.getElementById('dashboard').classList.remove('hidden');
|
document.getElementById('dashboard').classList.remove('hidden');
|
||||||
|
|
||||||
const totalEmployees = new Set(data.map(r => r.name)).size;
|
var allNames = new Set();
|
||||||
const completed = data.filter(r => {
|
var completedNames = new Set();
|
||||||
const s = (r.status || '').toLowerCase();
|
var scores = [];
|
||||||
return s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок');
|
var trainingStats = {};
|
||||||
});
|
var divisionStats = {};
|
||||||
const completedEmployees = new Set(completed.map(r => r.name)).size;
|
var trainerLoad = {};
|
||||||
const rate = totalEmployees ? Math.round(completedEmployees / totalEmployees * 100) : 0;
|
|
||||||
|
|
||||||
const scores = data.map(r => parseFloat(r.score)).filter(s => !isNaN(s) && s > 0);
|
data.forEach(function(r) {
|
||||||
const avgScore = scores.length ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) : '—';
|
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('totalEmployees').textContent = totalEmployees;
|
||||||
document.getElementById('completedCount').textContent = completedEmployees;
|
document.getElementById('completedCount').textContent = completedCount;
|
||||||
document.getElementById('completionRate').textContent = rate + '%';
|
document.getElementById('completionRate').textContent = rate + '%';
|
||||||
document.getElementById('avgScore').textContent = avgScore;
|
document.getElementById('avgScore').textContent = avgScore;
|
||||||
|
|
||||||
const trainingStats = {};
|
var trainingLabels = Object.keys(trainingStats);
|
||||||
data.forEach(r => {
|
var trainingCompletedCounts = trainingLabels.map(function(t) { return trainingStats[t].completed.size; });
|
||||||
const course = r.course || 'Без названия';
|
var trainingTotalCounts = trainingLabels.map(function(t) { return trainingStats[t].all.size; });
|
||||||
if (!trainingStats[course]) trainingStats[course] = { completed: new Set(), all: new Set(), scores: [], trainers: new Set(), divisions: {} };
|
|
||||||
trainingStats[course].all.add(r.name);
|
|
||||||
const s = (r.status || '').toLowerCase();
|
|
||||||
const isCompleted = s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок');
|
|
||||||
if (isCompleted) {
|
|
||||||
trainingStats[course].completed.add(r.name);
|
|
||||||
}
|
|
||||||
const score = parseFloat(r.score);
|
|
||||||
if (!isNaN(score) && score > 0) trainingStats[course].scores.push(score);
|
|
||||||
if (r.trainer) trainingStats[course].trainers.add(r.trainer);
|
|
||||||
const 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) trainingStats[course].divisions[div].completed.add(r.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const trainingLabels = Object.keys(trainingStats);
|
|
||||||
const trainingCompletedCounts = trainingLabels.map(t => trainingStats[t].completed.size);
|
|
||||||
const trainingTotalCounts = trainingLabels.map(t => trainingStats[t].all.size);
|
|
||||||
|
|
||||||
renderChart('trainingChart', 'bar', {
|
renderChart('trainingChart', 'bar', {
|
||||||
labels: trainingLabels,
|
labels: trainingLabels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{ label: 'Всего записей', data: trainingTotalCounts, backgroundColor: 'rgba(33,150,243,0.4)', borderColor: '#2196F3', borderWidth: 1 },
|
||||||
label: 'Всего записей',
|
{ label: 'Прошли', data: trainingCompletedCounts, backgroundColor: 'rgba(76,175,80,0.7)', borderColor: '#4caf50', borderWidth: 1 }
|
||||||
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' } } } });
|
||||||
indexAxis: 'y',
|
|
||||||
scales: {
|
|
||||||
x: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } },
|
|
||||||
y: { ticks: { color: '#e0e0e0', font: { size: 11 } }, grid: { color: '#1e2d3d' } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const allDivisions = [...new Set(data.map(r => r.division || 'Без дивизиона'))];
|
var allDivisions = Object.keys(divisionStats);
|
||||||
const trainingByDivDatasets = allDivisions.map((div, i) => {
|
var colors = ['#2196F3', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', '#ff5722', '#8bc34a', '#e91e63'];
|
||||||
const colors = ['#2196F3', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', '#ff5722', '#8bc34a', '#e91e63'];
|
var trainingByDivDatasets = allDivisions.map(function(div, i) {
|
||||||
return {
|
return {
|
||||||
label: div,
|
label: div,
|
||||||
data: trainingLabels.map(t => trainingStats[t].divisions[div] ? trainingStats[t].divisions[div].completed.size : 0),
|
data: trainingLabels.map(function(t) { return trainingStats[t].divisions[div] ? trainingStats[t].divisions[div].completed.size : 0; }),
|
||||||
backgroundColor: colors[i % colors.length] + 'aa',
|
backgroundColor: colors[i % colors.length] + 'aa',
|
||||||
borderColor: colors[i % colors.length],
|
borderColor: colors[i % colors.length],
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
@ -145,230 +151,91 @@ function processAndRender(rawData) {
|
|||||||
renderChart('trainingByDivisionChart', 'bar', {
|
renderChart('trainingByDivisionChart', 'bar', {
|
||||||
labels: trainingLabels,
|
labels: trainingLabels,
|
||||||
datasets: trainingByDivDatasets
|
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' } } } });
|
||||||
scales: {
|
|
||||||
x: { stacked: true, ticks: { color: '#90a4ae', maxRotation: 45 }, grid: { color: '#1e2d3d' } },
|
|
||||||
y: { stacked: true, beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const divisionStats = {};
|
|
||||||
data.forEach(r => {
|
|
||||||
const div = r.division || 'Без дивизиона';
|
|
||||||
if (!divisionStats[div]) divisionStats[div] = { total: new Set(), completed: new Set() };
|
|
||||||
divisionStats[div].total.add(r.name);
|
|
||||||
const s = (r.status || '').toLowerCase();
|
|
||||||
if (s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок')) {
|
|
||||||
divisionStats[div].completed.add(r.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
renderChart('divisionChart', 'bar', {
|
renderChart('divisionChart', 'bar', {
|
||||||
labels: Object.keys(divisionStats),
|
labels: Object.keys(divisionStats),
|
||||||
datasets: [
|
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: 'Всего',
|
{ label: 'Прошли', data: Object.values(divisionStats).map(function(d) { return d.completed.size; }), backgroundColor: 'rgba(76,175,80,0.6)', borderColor: '#4caf50', borderWidth: 1 }
|
||||||
data: Object.values(divisionStats).map(d => d.total.size),
|
|
||||||
backgroundColor: 'rgba(33, 150, 243, 0.6)',
|
|
||||||
borderColor: '#2196F3',
|
|
||||||
borderWidth: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Прошли',
|
|
||||||
data: Object.values(divisionStats).map(d => d.completed.size),
|
|
||||||
backgroundColor: 'rgba(76, 175, 80, 0.6)',
|
|
||||||
borderColor: '#4caf50',
|
|
||||||
borderWidth: 1
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const trainerLoad = {};
|
var trainerLabels = Object.keys(trainerLoad);
|
||||||
data.forEach(r => {
|
|
||||||
const t = r.trainer || 'Не указан';
|
|
||||||
if (!trainerLoad[t]) trainerLoad[t] = { count: 0, scores: [] };
|
|
||||||
trainerLoad[t].count++;
|
|
||||||
const s = parseFloat(r.score);
|
|
||||||
if (!isNaN(s) && s > 0) trainerLoad[t].scores.push(s);
|
|
||||||
});
|
|
||||||
|
|
||||||
const trainerLabels = Object.keys(trainerLoad);
|
|
||||||
const trainerCounts = trainerLabels.map(t => trainerLoad[t].count);
|
|
||||||
const trainerAvg = trainerLabels.map(t => {
|
|
||||||
const sc = trainerLoad[t].scores;
|
|
||||||
return sc.length ? (sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
renderChart('trainerChart', 'bar', {
|
renderChart('trainerChart', 'bar', {
|
||||||
labels: trainerLabels,
|
labels: trainerLabels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{ label: 'Занятий', data: trainerLabels.map(function(t) { return trainerLoad[t].count; }), backgroundColor: 'rgba(255,152,0,0.6)', borderColor: '#ff9800', borderWidth: 1 },
|
||||||
label: 'Занятий',
|
{ 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' }
|
||||||
data: trainerCounts,
|
|
||||||
backgroundColor: 'rgba(255, 152, 0, 0.6)',
|
|
||||||
borderColor: '#ff9800',
|
|
||||||
borderWidth: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Средняя оценка',
|
|
||||||
data: trainerAvg,
|
|
||||||
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' } } } });
|
||||||
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' } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const scoreDist = { '5': 0, '4': 0, '3': 0, '2': 0, '1': 0 };
|
|
||||||
scores.forEach(s => {
|
|
||||||
const rounded = Math.round(s);
|
|
||||||
if (scoreDist[String(rounded)] !== undefined) scoreDist[String(rounded)]++;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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', {
|
renderChart('scoreChart', 'doughnut', {
|
||||||
labels: Object.keys(scoreDist).map(k => 'Оценка ' + k),
|
labels: Object.keys(scoreDist).map(function(k) { return 'Оценка ' + k; }),
|
||||||
datasets: [{
|
datasets: [{ data: Object.values(scoreDist), backgroundColor: ['#4caf50', '#8bc34a', '#ff9800', '#ff5722', '#f44336'], borderWidth: 0 }]
|
||||||
data: Object.values(scoreDist),
|
|
||||||
backgroundColor: ['#4caf50', '#8bc34a', '#ff9800', '#ff5722', '#f44336'],
|
|
||||||
borderWidth: 0
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusCounts = {};
|
|
||||||
data.forEach(r => {
|
|
||||||
const s = (r.status || 'Неизвестно').trim();
|
|
||||||
statusCounts[s] = (statusCounts[s] || 0) + 1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var statusCounts = {};
|
||||||
|
data.forEach(function(r) { var s = (r.status || 'Неизвестно').trim(); statusCounts[s] = (statusCounts[s] || 0) + 1; });
|
||||||
renderChart('statusChart', 'pie', {
|
renderChart('statusChart', 'pie', {
|
||||||
labels: Object.keys(statusCounts),
|
labels: Object.keys(statusCounts),
|
||||||
datasets: [{
|
datasets: [{ data: Object.values(statusCounts), backgroundColor: ['#4caf50', '#ff9800', '#f44336', '#2196F3', '#9c27b0', '#00bcd4'], borderWidth: 0 }]
|
||||||
data: Object.values(statusCounts),
|
|
||||||
backgroundColor: ['#4caf50', '#ff9800', '#f44336', '#2196F3', '#9c27b0', '#00bcd4'],
|
|
||||||
borderWidth: 0
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const trainingAvgScores = trainingLabels.map(t => {
|
|
||||||
const sc = trainingStats[t].scores;
|
|
||||||
return sc.length ? (sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 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', {
|
renderChart('trainingScoreChart', 'bar', {
|
||||||
labels: trainingLabels,
|
labels: trainingLabels,
|
||||||
datasets: [{
|
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 }]
|
||||||
label: 'Средняя оценка',
|
}, { scales: { y: { beginAtZero: true, max: 5, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, x: { ticks: { color: '#e0e0e0', maxRotation: 45 }, grid: { color: '#1e2d3d' } } } });
|
||||||
data: trainingAvgScores,
|
|
||||||
backgroundColor: trainingAvgScores.map(v => 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(v => 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' } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const trainingDetailBody = document.querySelector('#trainingDetailTable tbody');
|
var trainingDetailBody = document.querySelector('#trainingDetailTable tbody');
|
||||||
trainingDetailBody.innerHTML = trainingLabels.map(t => {
|
trainingDetailBody.innerHTML = trainingLabels.map(function(t) {
|
||||||
const st = trainingStats[t];
|
var st = trainingStats[t];
|
||||||
const total = st.all.size;
|
var total = st.all.size;
|
||||||
const done = st.completed.size;
|
var done = st.completed.size;
|
||||||
const pct = total ? Math.round(done / total * 100) : 0;
|
var pct = total ? Math.round(done / total * 100) : 0;
|
||||||
const avg = st.scores.length ? (st.scores.reduce((a, b) => a + b, 0) / st.scores.length).toFixed(1) : '—';
|
var avg = st.scores.length ? (st.scores.reduce(function(a, b) { return a + b; }, 0) / st.scores.length).toFixed(1) : '—';
|
||||||
const trainers = [...st.trainers].join(', ') || '—';
|
var trainers = Array.from(st.trainers).join(', ') || '—';
|
||||||
return `<tr>
|
return '<tr><td><strong>' + t + '</strong></td><td>' + done + '</td><td>' + (total - done) + '</td><td>' + pct + '%</td><td>' + avg + '</td><td>' + trainers + '</td></tr>';
|
||||||
<td><strong>${t}</strong></td>
|
|
||||||
<td>${done}</td>
|
|
||||||
<td>${total - done}</td>
|
|
||||||
<td>${pct}%</td>
|
|
||||||
<td>${avg}</td>
|
|
||||||
<td>${trainers}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const coursesPerPerson = {};
|
var coursesPerPerson = {};
|
||||||
data.forEach(r => {
|
data.forEach(function(r) {
|
||||||
if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() };
|
if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() };
|
||||||
if (r.course) coursesPerPerson[r.name].courses.add(r.course);
|
if (r.course) coursesPerPerson[r.name].courses.add(r.course);
|
||||||
});
|
});
|
||||||
|
var champions = Object.entries(coursesPerPerson)
|
||||||
const champions = Object.entries(coursesPerPerson)
|
.map(function(e) { return { name: e[0], division: e[1].division, count: e[1].courses.size }; })
|
||||||
.map(([name, d]) => ({ name, division: d.division, count: d.courses.size }))
|
.sort(function(a, b) { return b.count - a.count; })
|
||||||
.sort((a, b) => b.count - a.count)
|
|
||||||
.slice(0, 10);
|
.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('');
|
||||||
|
|
||||||
const championsBody = document.querySelector('#championsTable tbody');
|
var noTraining = Array.from(allNames).filter(function(n) { return !completedNames.has(n); });
|
||||||
championsBody.innerHTML = champions.map(c =>
|
var noTrainingDivs = {};
|
||||||
`<tr><td>${c.name}</td><td>${c.division}</td><td>${c.count}</td></tr>`
|
data.forEach(function(r) { if (noTraining.indexOf(r.name) !== -1) noTrainingDivs[r.name] = r.division || ''; });
|
||||||
).join('');
|
document.querySelector('#noTrainingTable tbody').innerHTML = noTraining.slice(0, 20).map(function(n) {
|
||||||
|
return '<tr><td>' + n + '</td><td>' + (noTrainingDivs[n] || '') + '</td></tr>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
const completedNames = new Set(completed.map(r => r.name));
|
document.querySelector('#detailTable tbody').innerHTML = data.map(function(r) {
|
||||||
const allNames = new Set(data.map(r => r.name));
|
var cls = isCompleted(r.status) ? 'status-completed' : 'status-not-started';
|
||||||
const noTraining = [...allNames].filter(n => !completedNames.has(n));
|
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>';
|
||||||
const noTrainingDivs = {};
|
|
||||||
data.forEach(r => {
|
|
||||||
if (noTraining.includes(r.name)) noTrainingDivs[r.name] = r.division || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const noTrainingBody = document.querySelector('#noTrainingTable tbody');
|
|
||||||
noTrainingBody.innerHTML = noTraining.slice(0, 20).map(n =>
|
|
||||||
`<tr><td>${n}</td><td>${noTrainingDivs[n]}</td></tr>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const detailBody = document.querySelector('#detailTable tbody');
|
|
||||||
detailBody.innerHTML = data.map(r => {
|
|
||||||
const s = (r.status || '').toLowerCase();
|
|
||||||
let cls = '';
|
|
||||||
if (s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок')) cls = 'status-completed';
|
|
||||||
else if (s.includes('ходе') || s.includes('progress')) cls = 'status-in-progress';
|
|
||||||
else cls = '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('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChart(canvasId, type, chartData, extraOptions) {
|
function renderChart(canvasId, type, chartData, extraOptions) {
|
||||||
if (chartInstances[canvasId]) chartInstances[canvasId].destroy();
|
if (chartInstances[canvasId]) chartInstances[canvasId].destroy();
|
||||||
|
var defaults = { responsive: true, maintainAspectRatio: true, plugins: { legend: { labels: { color: '#e0e0e0', font: { size: 12 } } } } };
|
||||||
const defaults = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: true,
|
|
||||||
plugins: {
|
|
||||||
legend: { labels: { color: '#e0e0e0', font: { size: 12 } } }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === 'bar' || type === 'line') {
|
if (type === 'bar' || type === 'line') {
|
||||||
defaults.scales = {
|
defaults.scales = { y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } };
|
||||||
y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } },
|
|
||||||
x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraOptions) {
|
if (extraOptions) {
|
||||||
Object.assign(defaults, extraOptions);
|
Object.keys(extraOptions).forEach(function(k) { defaults[k] = extraOptions[k]; });
|
||||||
if (extraOptions.scales) defaults.scales = extraOptions.scales;
|
|
||||||
}
|
}
|
||||||
|
var ctx = document.getElementById(canvasId).getContext('2d');
|
||||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
chartInstances[canvasId] = new Chart(ctx, { type: type, data: chartData, options: defaults });
|
||||||
chartInstances[canvasId] = new Chart(ctx, {
|
|
||||||
type: type,
|
|
||||||
data: chartData,
|
|
||||||
options: defaults
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user