// Global state let employeesData = []; let filteredData = []; let currentPeriod = ''; // Process uploaded Excel file function processFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('Пожалуйста, выберите файл'); return; } const reader = new FileReader(); reader.onload = function(e) { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); // Read first sheet const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; const jsonData = XLSX.utils.sheet_to_json(firstSheet); parseEmployeeData(jsonData); } catch (error) { console.error('Error parsing file:', error); alert('Ошибка при чтении файла. Убедитесь, что это корректный Excel-файл.'); } }; reader.readAsArrayBuffer(file); } // Parse employee data from Excel function parseEmployeeData(data) { employeesData = data.map((row, index) => { // Try to map common column names const fio = row['ФИО'] || row['FIO'] || row['Сотрудник'] || row['Employee'] || `Сотрудник ${index + 1}`; const dept = row['Подразделение'] || row['Department'] || row['Отдел'] || 'Не указано'; const rating = parseFloat(row['Рейтинг'] || row['Rating'] || row['Баллы'] || 0); const prevPlace = parseInt(row['Предыдущее место'] || row['Место пред.'] || row['PrevPlace'] || index + 1); const plannedHours = parseFloat(row['План часы'] || row['Плановые часы'] || row['PlannedHours'] || 160); const absenceHours = parseFloat(row['Часы отсутствия'] || row['Отсутствие часы'] || row['AbsenceHours'] || 0); const schedule = row['График'] || row['Schedule'] || 'Стандартный'; const absences = row['Отсутствия'] || row['Absences'] || ''; const sickness = parseFloat(row['Больничный'] || row['SickLeave'] || 0); const vacation = parseFloat(row['Отпуск'] || row['Vacation'] || 0); const socialLeave = parseFloat(row['Соц. отпуск'] || row['SocialLeave'] || 0); const timeOff = parseFloat(row['Отгул'] || row['TimeOff'] || 0); // Calculate availability const availableHours = Math.max(0, plannedHours - absenceHours); const availabilityCoeff = plannedHours > 0 ? (availableHours / plannedHours) : 1; // Calculate position change const currentPosition = index + 1; const positionChange = prevPlace - currentPosition; // Positive = moved up // Determine if needs review const needsReview = availabilityCoeff < 0.7 || positionChange < -5 || positionChange > 5; const isIncompletePeriod = availabilityCoeff < 0.5; return { id: index, fio, dept, rating, prevPlace, currentPosition, positionChange, plannedHours, absenceHours, availableHours, availabilityCoeff, schedule, absences, sickness, vacation, socialLeave, timeOff, needsReview, isIncompletePeriod }; }); // Sort by rating (descending) employeesData.sort((a, b) => b.rating - a.rating); // Update current positions after sorting employeesData.forEach((emp, idx) => { emp.currentPosition = idx + 1; }); filteredData = [...employeesData]; // Show controls and table document.getElementById('controlsSection').classList.remove('hidden'); document.getElementById('tableView').classList.remove('hidden'); // Populate filters populateDepartmentFilter(); applyFilters(); } // Populate department filter function populateDepartmentFilter() { const depts = [...new Set(employeesData.map(emp => emp.dept))].sort(); const select = document.getElementById('deptFilter'); select.innerHTML = ''; depts.forEach(dept => { const option = document.createElement('option'); option.value = dept; option.textContent = dept; select.appendChild(option); }); } // Apply filters function applyFilters() { const deptFilter = document.getElementById('deptFilter').value; const nameFilter = document.getElementById('nameFilter').value.toLowerCase(); filteredData = employeesData.filter(emp => { const matchesDept = !deptFilter || emp.dept === deptFilter; const matchesName = !nameFilter || emp.fio.toLowerCase().includes(nameFilter); return matchesDept && matchesName; }); renderTable(); } // Render table function renderTable() { const tbody = document.getElementById('ratingTableBody'); tbody.innerHTML = ''; filteredData.forEach(emp => { const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50 cursor-pointer transition'; tr.onclick = () => showEmployeeModal(emp.id); const trendClass = emp.positionChange > 0 ? 'trend-up' : emp.positionChange < 0 ? 'trend-down' : 'trend-neutral'; const trendIcon = emp.positionChange > 0 ? '↑' : emp.positionChange < 0 ? '↓' : '−'; let statusBadge = 'Норма'; if (emp.isIncompletePeriod) { statusBadge = '⚠️ Неполный период'; } else if (emp.availabilityCoeff < 0.7) { statusBadge = 'Низкая доступность'; } tr.innerHTML = `
Нет данных
'; // Biggest decline const decline = [...employeesData].filter(e => e.positionChange < 0).sort((a, b) => a.positionChange - b.positionChange).slice(0, 5); document.getElementById('biggestDecline').innerHTML = decline.map(emp => `Нет данных
'; // Low availability const lowAvail = employeesData.filter(e => e.availabilityCoeff < 0.7).slice(0, 5); document.getElementById('lowAvailability').innerHTML = lowAvail.map(emp => `Все сотрудники с нормальной доступностью
'; // Needs review const review = employeesData.filter(e => e.needsReview).slice(0, 5); document.getElementById('needsReview').innerHTML = review.map(emp => `Все сотрудники в норме
'; // Summary stats const avgRating = employeesData.reduce((sum, e) => sum + e.rating, 0) / employeesData.length; const avgAvailability = employeesData.reduce((sum, e) => sum + e.availabilityCoeff, 0) / employeesData.length; const growthCount = employeesData.filter(e => e.positionChange > 0).length; const declineCount = employeesData.filter(e => e.positionChange < 0).length; document.getElementById('summaryStats').innerHTML = `Отсутствий не зафиксировано
'; } // Warnings const warningsDiv = document.getElementById('modalWarnings'); if (emp.isIncompletePeriod || emp.availabilityCoeff < 0.7) { warningsDiv.innerHTML = `🔴 Требуется учитывать неполный период при сравнении
' : ''} ${emp.availabilityCoeff < 0.7 ? '🔴 Низкая доступность рабочего времени
' : ''}