corp-university-dashboard/script.js

242 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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', 'тема': 'course', 'дисциплина': 'course', 'модуль': 'course',
'статус': 'status', 'status': 'status', 'состояние': 'status',
'оценка': 'score', 'балл': 'score', 'баллы': 'score', 'рейтинг': 'score',
'тренер': 'trainer', 'преподаватель': 'trainer', 'coach': 'trainer',
'дата': 'date', 'дата прохождения': 'date'
};
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
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;
}
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 });
}