// ========== МОК-ДАННЫЕ ========== const COURSES = [ { id: 1, name: "Безопасная рабочая среда SWE 2", level: 2, periodicity: 12 }, { id: 2, name: "Безопасное вождение (Defensive Driving)", level: 2, periodicity: 24 }, { id: 3, name: "Безопасное выполнение земляных работ", level: 1, periodicity: 12 }, { id: 4, name: "Безопасное выполнение работ на лестницах", level: 1, periodicity: 12 }, { id: 5, name: "Безопасное выполнение работ на опорах", level: 1, periodicity: 12 }, { id: 6, name: "Безопасность рук", level: 2, periodicity: 12 }, { id: 7, name: "Изоляция источников энергии (LOTO)", level: 1, periodicity: 12 }, { id: 8, name: "Контроль усталости", level: 1, periodicity: 6 }, { id: 9, name: "Культура безопасности труда", level: 2, periodicity: 24 }, { id: 10, name: "Оказание первой медицинской помощи", level: 1, periodicity: 24 }, { id: 11, name: "Основы безопасности в замкнутом пространстве", level: 2, periodicity: 12 }, { id: 12, name: "Основы безопасности на высоте", level: 2, periodicity: 12 }, { id: 13, name: "Поведенческие аудиты безопасности", level: 2, periodicity: 12 }, { id: 14, name: "Предотвращение травматизма IPR", level: 1, periodicity: 12 }, { id: 15, name: "Предотвращение травматизма IPR (углублённый)", level: 2, periodicity: 12 }, { id: 16, name: "Расследование инцидентов", level: 1, periodicity: 12 }, { id: 17, name: "Управление опасностями и рисками", level: 1, periodicity: 12 }, { id: 18, name: "Устойчивое развитие и экология", level: 1, periodicity: 24 }, { id: 19, name: "Фасилитатор в HSE", level: 2, periodicity: 24 }, { id: 20, name: "Четырёхступенчатый контроль (1 ступень)", level: 1, periodicity: 12 }, { id: 21, name: "Четырёхступенчатый контроль (2 ступень)", level: 1, periodicity: 12 }, { id: 22, name: "Четырёхступенчатый контроль (3 ступень)", level: 1, periodicity: 12 }, { id: 23, name: "Четырёхступенчатый контроль (4 ступень)", level: 1, periodicity: 12 } ]; const CITIES = ["Алматы", "Астана", "Атырау", "Актау", "Караганда", "Шымкент"]; const DEPARTMENTS = ["HSE", "Производство", "Логистика", "Администрация", "Техническая служба", "Электроснабжение"]; const POSITIONS = ["Инженер HSE", "Мастер", "Электромонтёр", "Слесарь", "Водитель", "Начальник смены", "Техник"]; // Генерация мок-данных сотрудников function generateEmployees() { const employees = []; const lastNames = ["Иванов", "Петров", "Сидоров", "Козлов", "Новиков", "Морозов", "Волков", "Соколов", "Лебедев", "Кузнецов", "Попов", "Васильев", "Зайцев", "Михайлов", "Фёдоров", "Абдуллин", "Нурланов", "Сагиев", "Турсунов", "Касымов", "Жумабаев", "Ермеков", "Баймуханов", "Алиев", "Хасенов"]; const firstNames = ["Александр", "Дмитрий", "Сергей", "Андрей", "Михаил", "Ильяс", "Данияр", "Артём", "Руслан", "Нурсултан", "Айдар", "Ерлан", "Булат", "Тимур", "Арман", "Мадияр", "Самат", "Кайрат", "Ержан", "Аслан"]; const patronymics = ["Александрович", "Дмитриевич", "Сергеевич", "Андреевич", "Михайлович", "Ильясович", "Даниярович", "Артёмович", "Русланович", "Нурсултанович"]; let tab = 1001; for (let city of CITIES) { const count = 8 + Math.floor(Math.random() * 7); for (let i = 0; i < count; i++) { const emp = { tab: String(tab++), lastName: lastNames[Math.floor(Math.random() * lastNames.length)], firstName: firstNames[Math.floor(Math.random() * firstNames.length)], patronymic: patronymics[Math.floor(Math.random() * patronymics.length)], city: city, department: DEPARTMENTS[Math.floor(Math.random() * DEPARTMENTS.length)], position: POSITIONS[Math.floor(Math.random() * POSITIONS.length)], courses: [] }; emp.fullName = `${emp.lastName} ${emp.firstName} ${emp.patronymic}`; // Случайные курсы const courseCount = 5 + Math.floor(Math.random() * 10); const shuffled = [...COURSES].sort(() => 0.5 - Math.random()); for (let j = 0; j < Math.min(courseCount, COURSES.length); j++) { const course = shuffled[j]; const completed = Math.random() > 0.3; if (completed) { const monthsAgo = Math.floor(Math.random() * 36); const completedDate = new Date(); completedDate.setMonth(completedDate.getMonth() - monthsAgo); const nextDate = new Date(completedDate); nextDate.setMonth(nextDate.getMonth() + course.periodicity); const isOverdue = nextDate < new Date(); emp.courses.push({ courseId: course.id, courseName: course.name, level: course.level, completed: true, completedDate: completedDate.toISOString().split('T')[0], nextDate: nextDate.toISOString().split('T')[0], overdue: isOverdue, daysOverdue: isOverdue ? Math.floor((new Date() - nextDate) / 86400000) : 0 }); } else { emp.courses.push({ courseId: course.id, courseName: course.name, level: course.level, completed: false, completedDate: null, nextDate: null, overdue: false, daysOverdue: 0 }); } } employees.push(emp); } } return employees; } const EMPLOYEES = generateEmployees(); // ========== СОСТОЯНИЕ ========== let currentUser = { tab: 'admin', fullName: 'Администратор системы', role: 'admin' }; let isAdmin = true; // ========== АВТОМАТИЧЕСКАЯ ЗАГРУЗКА ========== document.addEventListener('DOMContentLoaded', function() { try { document.getElementById('admin-panel').classList.remove('hidden'); document.getElementById('employee-panel').classList.add('hidden'); showTab('dashboard'); renderDashboard(); renderCityStats(); } catch (e) { console.error('Ошибка инициализации:', e); document.body.innerHTML = '
Ошибка загрузки: ' + e.message + '
'; } }); // ========== ТАБЫ ========== function showTab(name) { document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden')); document.getElementById('tab-' + name).classList.remove('hidden'); document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('btn'); btn.classList.add('btn', 'btn-outline'); }); document.querySelector(`[data-tab="${name}"]`).classList.remove('btn-outline'); } // ========== ПОИСК ========== function searchEmployee() { const query = document.getElementById('search-input').value.trim(); const cityFilter = document.getElementById('filter-city').value; let results = EMPLOYEES; if (query) { results = results.filter(e => e.tab === query || e.fullName.toLowerCase().includes(query.toLowerCase()) ); } if (cityFilter) { results = results.filter(e => e.city === cityFilter); } renderSearchResults(results); } function renderSearchResults(employees) { const container = document.getElementById('search-results'); if (employees.length === 0) { container.innerHTML = '
Сотрудники не найдены
'; return; } let html = '
'; html += ''; html += ''; for (const emp of employees) { const completed = emp.courses.filter(c => c.completed).length; const overdue = emp.courses.filter(c => c.overdue).length; const progress = Math.round((completed / emp.courses.length) * 100); html += ``; } html += '
Таб.№ФИОГородДолжностьПройденоПросроченоПрогресс
${emp.tab} ${emp.fullName} ${emp.city} ${emp.position} ${completed}/${emp.courses.length} ${overdue > 0 ? '❌ ' + overdue : '✅'}
${progress}%
'; container.innerHTML = html; } function showEmployeeDetail(tab) { const emp = EMPLOYEES.find(e => e.tab === tab); if (!emp) return; showTab('search'); renderEmployeeCourses(emp, true); } // ========== ПОКАЗАТЬ КУРСЫ СОТРУДНИКА ========== function renderEmployeeCourses(emp, showBack = false) { const container = isAdmin ? document.getElementById('search-results') : document.getElementById('emp-courses'); const titleEl = document.getElementById('emp-title'); if (titleEl) titleEl.textContent = `Обучение — ${emp.fullName}`; const completed = emp.courses.filter(c => c.completed); const notStarted = emp.courses.filter(c => !c.completed); const overdue = completed.filter(c => c.overdue); const valid = completed.filter(c => !c.overdue); const progress = Math.round((completed.length / emp.courses.length) * 100); let html = `
`; if (showBack) html += ``; html += `
Таб.№: ${emp.tab}
Город: ${emp.city}
Должность: ${emp.position}
Подразделение: ${emp.department}
Общий прогресс${completed.length} из ${emp.courses.length} (${progress}%)
`; if (overdue.length > 0) { html += `

⏰ Просрочено (${overdue.length})

`; for (const c of overdue) { html += ``; } html += `
КурсУровеньЗавершёнДолжен пройтиПросрочка
${c.courseName}Ур.${c.level}${formatDate(c.completedDate)}${formatDate(c.nextDate)}⚠️ ${c.daysOverdue} дн.
`; } if (valid.length > 0) { html += `

✅ Действует (${valid.length})

`; for (const c of valid) { const daysLeft = Math.ceil((new Date(c.nextDate) - new Date()) / 86400000); const urgency = daysLeft <= 30 ? 'text-yellow-600 font-bold' : ''; html += ``; } html += `
КурсУровеньЗавершёнСледующее
${c.courseName}Ур.${c.level}${formatDate(c.completedDate)}${formatDate(c.nextDate)} ${daysLeft <= 30 ? '⚠️ ' + daysLeft + ' дн.' : ''}
`; } if (notStarted.length > 0) { html += `

❗ Не пройдено (${notStarted.length})

`; } html += '
'; container.innerHTML = html; if (isAdmin) { document.getElementById('tab-search').classList.remove('hidden'); document.querySelectorAll('.tab-content').forEach(el => { if (el.id !== 'tab-search') el.classList.add('hidden'); }); } } function formatDate(dateStr) { if (!dateStr) return '—'; const [y, m, d] = dateStr.split('-'); return `${d}.${m}.${y}`; } // ========== РЕГИОНЫ ========== function renderCityStats() { const container = document.getElementById('city-stats'); const stats = CITIES.map(city => { const emps = EMPLOYEES.filter(e => e.city === city); const totalCourses = emps.reduce((sum, e) => sum + e.courses.length, 0); const completed = emps.reduce((sum, e) => sum + e.courses.filter(c => c.completed).length, 0); const overdue = emps.reduce((sum, e) => sum + e.courses.filter(c => c.overdue).length, 0); const progress = totalCourses > 0 ? Math.round((completed / totalCourses) * 100) : 0; return { city, count: emps.length, progress, overdue }; }); container.innerHTML = stats.map(s => `
${s.count}
${s.city}
${s.progress}% выполнено ${s.overdue > 0 ? `| ${s.overdue} просрочено` : ''}
`).join(''); } function showCityEmployees(city) { const emps = EMPLOYEES.filter(e => e.city === city); renderSearchResults(emps); showTab('search'); } // ========== ЗАГРУЗКА ДАННЫХ ========== function handleFileUpload(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); const extension = file.name.split('.').pop().toLowerCase(); reader.onload = function(e) { let csvData = e.target.result; let parsedData = null; let fileType = ''; if (extension === 'csv') { parsedData = parseCSV(csvData); fileType = 'CSV'; } else if (extension === 'xlsx') { try { parsedData = parseExcel(e.target.result); fileType = 'Excel'; } catch (err) { showUploadMessage(`Ошибка при чтении Excel файла: ${err.message}`, 'error'); return; } } else { showUploadMessage('Поддерживаются только файлы .csv и .xlsx', 'error'); return; } if (parsedData && parsedData.length > 0) { showUploadMessage(`Файл успешно загружен! Найдено ${parsedData.length} строк данных.`, 'success'); processUploadedData(parsedData, fileType); } else { showUploadMessage('Не удалось распознать данные в файле. Проверьте формат CSV/Excel.', 'error'); } }; reader.onerror = function() { showUploadMessage('Ошибка при чтении файла', 'error'); }; if (extension === 'csv') { reader.readAsText(file); } else if (extension === 'xlsx') { reader.readAsArrayBuffer(file); } } function parseCSV(csvData) { const lines = csvData.split(/\r?\n/).filter(line => line.trim()); if (lines.length < 2) return null; const headers = lines[0].split(';').map(h => h.trim()); const data = []; for (let i = 1; i < lines.length; i++) { const values = lines[i].split(';'); if (values.length < 3) continue; const row = {}; for (let j = 0; j < headers.length; j++) { if (j < values.length) { row[headers[j]] = values[j]; } } data.push(row); } return data; } function parseExcel(arrayBuffer) { try { const data = new Uint8Array(arrayBuffer); const workbook = XLSX.read(data, { type: 'array' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); const headers = json[0] ? json[0].map(h => { if (typeof h === 'string') return h.trim(); return String(h).trim(); }) : []; const data = []; for (let i = 1; i < json.length; i++) { if (!json[i] || json[i].length === 0) continue; const row = {}; for (let j = 0; j < headers.length; j++) { if (j < json[i].length) { row[headers[j]] = json[i][j]; } } data.push(row); } return data; } catch (err) { console.error('Ошибка парсинга Excel:', err); return null; } } function processUploadedData(uploadedData, fileType) { try { const processedEmployees = []; for (const row of uploadedData) { const emp = { tab: row['Таб.№'] || row['tab'] || row['Табельный'] || '', lastName: row['ФИО'] ? row['ФИО'].split(' ')[0] : '', firstName: row['ФИО'] && row['ФИО'].includes(' ') ? row['ФИО'].split(' ')[1].split(' ')[0] : '', patronymic: row['ФИО'] && row['ФИО'].split(' ').length > 2 ? row['ФИО'].split(' ')[2] : '', fullName: row['ФИО'] || row['ФИО'] || '', city: row['Город'] || '', department: row['Подразделение'] || '', position: row['Должность'] || row['Штатная должность'] || '', courses: [] }; if (!emp.tab || !emp.fullName) continue; const courseHeaders = ['Курс1', 'Курс2', 'Курс3', 'Курс4', 'Курс5']; const statusHeaders = ['Статус1', 'Статус2', 'Статус3', 'Статус4', 'Статус5']; const dateHeaders = ['Дата1', 'Дата2', 'Дата3', 'Дата4', 'Дата5']; let courseIndex = 1; for (const header of courseHeaders) { const courseName = row[header]; if (!courseName) break; const status = row[statusHeaders[courseIndex - 1]] || row[`Статус ${courseIndex}`] || ''; const dateStr = row[dateHeaders[courseIndex - 1]] || row[`Дата ${courseIndex}`] || ''; const course = { courseId: courseIndex, courseName: courseName, level: Math.floor(Math.random() * 2) + 1, completed: status.toLowerCase() === 'пройдено' || status.toLowerCase() === 'yes' || status.toLowerCase() === 'true', completedDate: dateStr && dateStr.match(/\d{2}\.\d{2}\.\d{4}/) ? dateStr.match(/\d{2}\.\d{2}\.\d{4}/)[0] : '', nextDate: '', overdue: false, daysOverdue: 0 }; if (course.completed && course.completedDate) { const [day, month, year] = course.completedDate.split('.'); const completedDate = new Date(`${year}-${month}-${day}`); course.nextDate = new Date(completedDate); course.nextDate.setMonth(course.nextDate.getMonth() + 12); course.nextDate = course.nextDate.toISOString().split('T')[0]; const today = new Date(); course.overdue = new Date(course.nextDate) < today; course.daysOverdue = course.overdue ? Math.floor((today - new Date(course.nextDate)) / 86400000) : 0; } emp.courses.push(course); courseIndex++; } processedEmployees.push(emp); } EMPLOYEES.length = 0; EMPLOYEES.push(...processedEmployees); saveUploadedDataToLocalStorage(processedEmployees, fileType); updateDashboardAfterUpload(); } catch (error) { showUploadMessage('Ошибка обработки данных: ' + error.message, 'error'); } } function saveUploadedDataToLocalStorage(data, fileType) { try { const savedData = { employees: data, fileType: fileType, timestamp: new Date().toISOString(), version: '1.0' }; localStorage.setItem('tb_assistant_uploaded_data', JSON.stringify(savedData)); localStorage.setItem('tb_assistant_last_upload', new Date().toISOString()); showUploadMessage('Данные успешно загружены и сохранены!', 'success'); } catch (error) { showUploadMessage('Данные загружены, но не сохранены в локальное хранилище: ' + error.message, 'warning'); } } function updateDashboardAfterUpload() { if (currentUser && isAdmin) { if (document.getElementById('tab-dashboard').classList.contains('hidden')) { showTab('dashboard'); } renderDashboard(); renderCityStats(); if (document.getElementById('admin-panel')) { document.getElementById('admin-panel').classList.remove('hidden'); } } } function showUploadMessage(message, type) { const container = document.getElementById('upload-results'); if (!container) return; const msgDiv = document.createElement('div'); msgDiv.className = `p-3 mb-2 rounded text-sm ${type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' : type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-yellow-50 text-yellow-700 border border-yellow-200'}`; msgDiv.textContent = message; container.insertBefore(msgDiv, container.firstChild); if (type === 'success') { setTimeout(() => { if (msgDiv.parentNode) msgDiv.parentNode.removeChild(msgDiv); }, 5000); } } // ========== ДАШБОРД ========== function renderDashboard() { const totalEmp = EMPLOYEES.length; const totalCourses = EMPLOYEES.reduce((sum, e) => sum + e.courses.length, 0); const completed = EMPLOYEES.reduce((sum, e) => sum + e.courses.filter(c => c.completed).length, 0); const overdue = EMPLOYEES.reduce((sum, e) => sum + e.courses.filter(c => c.overdue).length, 0); const notStarted = EMPLOYEES.reduce((sum, e) => sum + e.courses.filter(c => !c.completed).length, 0); const progress = totalCourses > 0 ? Math.round((completed / totalCourses) * 100) : 0; document.getElementById('dash-stats').innerHTML = `
${totalEmp}
Сотрудников
${progress}%
Выполнено
${overdue}
Просрочено
${notStarted}
Не начато
`; const cityStats = CITIES.map(city => { const emps = EMPLOYEES.filter(e => e.city === city); const ov = emps.reduce((sum, e) => sum + e.courses.filter(c => c.overdue).length, 0); const total = emps.reduce((sum, e) => sum + e.courses.length, 0); return { city, overdue: ov, pct: total > 0 ? Math.round((ov / total) * 100) : 0 }; }).sort((a, b) => b.pct - a.pct); document.getElementById('dash-problems').innerHTML = cityStats.map((s, i) => `
${i + 1}. ${s.city} ${s.pct}% просрочено (${s.overdue})
` ).join(''); const courseStats = COURSES.map(c => { const total = EMPLOYEES.length; const notDone = EMPLOYEES.filter(e => !e.courses.find(cc => cc.courseId === c.id && cc.completed)).length; return { name: c.name, notDone, pct: Math.round((notDone / total) * 100) }; }).sort((a, b) => b.pct - a.pct).slice(0, 5); document.getElementById('dash-courses').innerHTML = courseStats.map((s, i) => `
${i + 1}. ${s.name} ${s.pct}%
` ).join(''); } // ========== ВЫГРУЗКА ========== function exportData(type) { const container = document.getElementById('export-results'); let html = '
'; let data = []; if (type === 'overdue') { html += '

⏰ Просроченные обучения

'; for (const emp of EMPLOYEES) { for (const c of emp.courses.filter(c => c.overdue)) { data.push({ tab: emp.tab, fio: emp.fullName, city: emp.city, course: c.courseName, completed: c.completedDate, overdue: c.nextDate, days: c.daysOverdue }); } } data.sort((a, b) => b.days - a.days); } else if (type === 'city') { html += '

🏙️ По филиалам

'; for (const emp of EMPLOYEES) { for (const c of emp.courses) { data.push({ tab: emp.tab, fio: emp.fullName, city: emp.city, course: c.courseName, status: c.completed ? (c.overdue ? 'Просрочено' : 'Пройдено') : 'Не пройдено' }); } } } else if (type === 'employee') { html += '

👤 По сотрудникам

'; for (const emp of EMPLOYEES) { const completed = emp.courses.filter(c => c.completed).length; const overdue = emp.courses.filter(c => c.overdue).length; data.push({ tab: emp.tab, fio: emp.fullName, city: emp.city, department: emp.department, total: emp.courses.length, completed, overdue, progress: Math.round((completed / emp.courses.length) * 100) + '%' }); } } else if (type === 'summary') { html += '

📋 Сводный отчёт по филиалам

'; for (const city of CITIES) { const emps = EMPLOYEES.filter(e => e.city === city); const total = emps.reduce((sum, e) => sum + e.courses.length, 0); const completed = emps.reduce((sum, e) => sum + e.courses.filter(c => c.completed).length, 0); const overdue = emps.reduce((sum, e) => sum + e.courses.filter(c => c.overdue).length, 0); data.push({ city, employees: emps.length, totalCourses: total, completed, overdue, progress: total > 0 ? Math.round((completed / total) * 100) + '%' : '0%' }); } } if (data.length === 0) { html += '

Нет данных для выгрузки

'; } else { const headers = Object.keys(data[0]); html += '' + headers.map(h => ``).join('') + ''; for (const row of data.slice(0, 100)) { html += '' + headers.map(h => ``).join('') + ''; } html += '
${h}
${row[h]}
'; if (data.length > 100) html += `

Показано 100 из ${data.length} записей

`; } html += '
'; container.innerHTML = html; } function exportCSV() { let csv = 'Таб.№;ФИО;Филиал;Должность;Подразделение;Курс;Уровень;Статус;Дата обучения;Дата завершения;Следующее обучение;Просрочено дней\n'; for (const emp of EMPLOYEES) { for (const c of emp.courses) { const status = c.completed ? (c.overdue ? 'Просрочено' : 'Пройдено') : 'Не пройдено'; csv += `${emp.tab};${emp.fullName};${emp.city};${emp.position};${emp.department};${c.courseName};${c.level};${status};${c.completedDate || ''};${c.completedDate || ''};${c.nextDate || ''};${c.daysOverdue || 0}\n`; } } const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'training_report_' + new Date().toISOString().split('T')[0] + '.csv'; a.click(); URL.revokeObjectURL(url); }