let chartInstances = {}; document.getElementById('excelFile').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; 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: '' }); if (jsonData.length === 0) { alert('Файл пуст или имеет неверный формат'); return; } processAndRender(jsonData); }; reader.readAsArrayBuffer(file); }); function normalizeHeaders(rows) { const headerMap = { 'фио': 'name', 'ф.и.о': 'name', 'сотрудник': 'name', 'employee': 'name', 'имя': 'name', 'дивизион': 'division', 'подразделение': 'division', 'отдел': 'division', 'division': 'division', 'department': 'division', 'курс': 'course', 'обучение': 'course', 'training': 'course', 'программа': 'course', 'статус': 'status', 'status': 'status', 'состояние': 'status', 'оценка': 'score', 'ball': 'score', 'score': 'score', 'рейтинг': 'score', 'балл': 'score', 'тренер': 'trainer', 'trainer': 'coach', 'преподаватель': 'trainer', 'coach': 'trainer', 'дата': 'date', 'date': 'date', 'дата прохождения': 'date' }; return rows.map(row => { const normalized = {}; for (const key in row) { const cleanKey = key.toLowerCase().trim(); const mapped = headerMap[cleanKey] || cleanKey; normalized[mapped] = row[key]; } return normalized; }); } function processAndRender(rawData) { const 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; 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) : '—'; document.getElementById('totalEmployees').textContent = totalEmployees; document.getElementById('completedCount').textContent = completedEmployees; 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); 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' } } } }); 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']; return { label: div, data: trainingLabels.map(t => 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' } } } }); 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', { 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 } ] }); 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; }); 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' } ] }, { 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)]++; }); 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; }); 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; }); 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' } } } }); 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 `