v4 rewrite with error handling, fix upload

This commit is contained in:
Dauren777 2026-06-19 06:10:58 +00:00
parent ab8fc2e8ac
commit fc1ef2c650

401
script.js
View File

@ -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,6 +18,7 @@ document.getElementById('excelFile').addEventListener('change', function(e) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(event) { reader.onload = function(event) {
try {
const data = new Uint8Array(event.target.result); const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, { type: 'array' }); const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
@ -28,114 +29,119 @@ document.getElementById('excelFile').addEventListener('change', function(e) {
return; return;
} }
const sampleRow = jsonData[0] || {}; var sampleRow = jsonData[0];
const rawCols = Object.keys(sampleRow); var rawCols = Object.keys(sampleRow);
const normalized = normalizeHeaders([sampleRow])[0]; var normalized = normalizeHeaders([sampleRow])[0];
const mapping = rawCols.map(k => { var debugHtml = '<strong>Оригинальные столбцы:</strong> ' + rawCols.join(', ') + '<br><br>';
const nk = k.toLowerCase().trim(); rawCols.forEach(function(k) {
const mapped = headerMap[nk] || nk; var nk = k.toLowerCase().trim();
const val = normalized[mapped] || '(пусто)'; var mapped = headerMap[nk] || nk;
return `<b>${k}</b> → ${mapped} = "${val}"`; var val = normalized[mapped] || '(пусто)';
}).join('<br>'); debugHtml += '<b>' + k + '</b> → ' + mapped + ' = "' + val + '"<br>';
});
document.getElementById('debug').classList.remove('hidden'); var debugEl = document.getElementById('debug');
document.getElementById('debugColumns').innerHTML = var debugColsEl = document.getElementById('debugColumns');
`<strong>Оригинальные столбцы:</strong> ${rawCols.join(', ')}<br><br>${mapping}`; if (debugEl && debugColsEl) {
debugEl.classList.remove('hidden');
debugColsEl.innerHTML = debugHtml;
}
processAndRender(jsonData); processAndRender(jsonData);
} catch (err) {
alert('Ошибка чтения файла: ' + err.message);
console.error(err);
}
}; };
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
});
} }