diff --git a/script.js b/script.js
index f02e797..031d26b 100644
--- a/script.js
+++ b/script.js
@@ -4,12 +4,12 @@ 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', 'module': 'course', 'модуль': 'course',
+ 'тренинг': 'course', 'название тренинга': 'course', 'название курса': 'course',
+ 'название': 'course', 'тема': 'course', 'дисциплина': 'course', 'модуль': 'course',
'статус': 'status', 'status': 'status', 'состояние': 'status',
- 'оценка': 'score', 'ball': 'score', 'score': 'score', 'рейтинг': 'score', 'балл': 'score',
- 'тренер': 'trainer', 'trainer': 'coach', 'преподаватель': 'trainer', 'coach': 'trainer',
- 'дата': 'date', 'date': 'date', 'дата прохождения': 'date'
+ 'оценка': 'score', 'балл': 'score', 'баллы': 'score', 'рейтинг': 'score',
+ 'тренер': 'trainer', 'преподаватель': 'trainer', 'coach': 'trainer',
+ 'дата': 'date', 'дата прохождения': 'date'
};
document.getElementById('excelFile').addEventListener('change', function(e) {
@@ -18,124 +18,130 @@ document.getElementById('excelFile').addEventListener('change', function(e) {
const reader = new FileReader();
reader.onload = function(event) {
- const data = new Uint8Array(event.target.result);
- const workbook = XLSX.read(data, { type: 'array' });
- const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
- const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' });
+ try {
+ const data = new Uint8Array(event.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
+ const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' });
- if (jsonData.length === 0) {
- alert('Файл пуст или имеет неверный формат');
- return;
+ 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);
}
-
- 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 `${k} → ${mapped} = "${val}"`;
- }).join('
');
-
- document.getElementById('debug').classList.remove('hidden');
- document.getElementById('debugColumns').innerHTML =
- `Оригинальные столбцы: ${rawCols.join(', ')}
${mapping}`;
-
- processAndRender(jsonData);
};
reader.readAsArrayBuffer(file);
});
function normalizeHeaders(rows) {
- return rows.map(row => {
- const normalized = {};
- for (const key in row) {
- const cleanKey = key.toLowerCase().trim();
- const mapped = headerMap[cleanKey] || cleanKey;
+ 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) {
- const data = normalizeHeaders(rawData);
+ var data = normalizeHeaders(rawData);
document.querySelector('.upload-section').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
- const totalEmployees = new Set(data.map(r => r.name)).size;
- const completed = data.filter(r => {
- const s = (r.status || '').toLowerCase();
- return s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок');
- });
- const completedEmployees = new Set(completed.map(r => r.name)).size;
- const rate = totalEmployees ? Math.round(completedEmployees / totalEmployees * 100) : 0;
+ var allNames = new Set();
+ var completedNames = new Set();
+ var scores = [];
+ var trainingStats = {};
+ var divisionStats = {};
+ var trainerLoad = {};
- const scores = data.map(r => parseFloat(r.score)).filter(s => !isNaN(s) && s > 0);
- const avgScore = scores.length ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) : '—';
+ 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 = completedEmployees;
+ document.getElementById('completedCount').textContent = completedCount;
document.getElementById('completionRate').textContent = rate + '%';
document.getElementById('avgScore').textContent = avgScore;
- const trainingStats = {};
- data.forEach(r => {
- const 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);
- 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);
+ 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
- }
+ { 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' } }
- }
- });
+ }, { 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 || 'Без дивизиона'))];
- const trainingByDivDatasets = allDivisions.map((div, i) => {
- const colors = ['#2196F3', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', '#ff5722', '#8bc34a', '#e91e63'];
+ 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(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',
borderColor: colors[i % colors.length],
borderWidth: 1
@@ -145,230 +151,91 @@ function processAndRender(rawData) {
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' } }
- }
- });
-
- 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);
- }
- });
+ }, { 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(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
- }
+ { 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 }
]
});
- const 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;
- });
-
+ var trainerLabels = Object.keys(trainerLoad);
renderChart('trainerChart', 'bar', {
labels: trainerLabels,
datasets: [
- {
- label: 'Занятий',
- 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'
- }
+ { 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' } }
- }
- });
-
- 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)]++;
- });
+ }, { 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(k => 'Оценка ' + k),
- datasets: [{
- 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;
+ 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
- }]
- });
-
- 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;
+ 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(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' } }
- }
- });
+ 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' } } } });
- const trainingDetailBody = document.querySelector('#trainingDetailTable tbody');
- trainingDetailBody.innerHTML = trainingLabels.map(t => {
- const st = trainingStats[t];
- const total = st.all.size;
- const done = st.completed.size;
- const 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) : '—';
- const trainers = [...st.trainers].join(', ') || '—';
- return `