tb-assistant/script.js

702 lines
32 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.

// ========== МОК-ДАННЫЕ ==========
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 = null;
let isAdmin = false;
// ========== АВТОРИЗАЦИЯ ==========
function doLogin() {
const login = document.getElementById('login-id').value.trim();
const pass = document.getElementById('login-pass').value.trim();
if (!login) { alert('Введите табельный номер'); return; }
// Админ
if (login === 'admin' || login === '12345') {
isAdmin = true;
currentUser = { tab: 'admin', fullName: 'Администратор системы', role: 'admin' };
} else {
// Поиск сотрудника
const emp = EMPLOYEES.find(e => e.tab === login || e.fullName.toLowerCase().includes(login.toLowerCase()));
if (!emp) { alert('Сотрудник не найден. Попробуйте: 1001-1050'); return; }
isAdmin = false;
currentUser = emp;
}
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('main-app').classList.remove('hidden');
document.getElementById('user-info').textContent = `${currentUser.fullName} (${isAdmin ? 'Администратор' : 'Сотрудник'})`;
if (isAdmin) {
document.getElementById('admin-panel').classList.remove('hidden');
document.getElementById('employee-panel').classList.add('hidden');
showTab('dashboard');
renderDashboard();
renderCityStats();
} else {
document.getElementById('admin-panel').classList.add('hidden');
document.getElementById('employee-panel').classList.remove('hidden');
renderEmployeeCourses(currentUser);
}
}
function doLogout() {
currentUser = null;
isAdmin = false;
document.getElementById('login-screen').classList.remove('hidden');
document.getElementById('main-app').classList.add('hidden');
document.getElementById('login-id').value = '';
document.getElementById('login-pass').value = '';
}
// ========== ТАБЫ ==========
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 = '<div class="card">Сотрудники не найдены</div>';
return;
}
let html = '<div class="card overflow-x-auto"><table class="table"><thead><tr>';
html += '<th>Таб.№</th><th>ФИО</th><th>Город</th><th>Должность</th><th>Пройдено</th><th>Просрочено</th><th>Прогресс</th><th></th>';
html += '</tr></thead><tbody>';
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 += `<tr>
<td class="font-mono">${emp.tab}</td>
<td class="font-semibold">${emp.fullName}</td>
<td>${emp.city}</td>
<td>${emp.position}</td>
<td class="status-ok">${completed}/${emp.courses.length}</td>
<td class="${overdue > 0 ? 'status-danger' : ''}">${overdue > 0 ? '❌ ' + overdue : '✅'}</td>
<td>
<div class="flex items-center gap-2">
<div class="progress-bar flex-1"><div class="progress-fill" style="width:${progress}%"></div></div>
<span class="text-sm">${progress}%</span>
</div>
</td>
<td><button onclick="showEmployeeDetail('${emp.tab}')" class="btn text-xs py-1 px-3">Подробнее</button></td>
</tr>`;
}
html += '</tbody></table></div>';
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 = `<div class="card">`;
if (showBack) html += `<button onclick="searchEmployee()" class="btn btn-outline text-sm mb-4">← Назад к списку</button>`;
html += `
<div class="flex flex-wrap gap-4 mb-4">
<div><span class="text-gray-500">Таб.№:</span> <strong>${emp.tab}</strong></div>
<div><span class="text-gray-500">Город:</span> <strong>${emp.city}</strong></div>
<div><span class="text-gray-500">Должность:</span> <strong>${emp.position}</strong></div>
<div><span class="text-gray-500">Подразделение:</span> <strong>${emp.department}</strong></div>
</div>
<div class="mb-4">
<div class="flex justify-between mb-1"><span class="font-semibold">Общий прогресс</span><span>${completed.length} из ${emp.courses.length} (${progress}%)</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:${progress}%"></div></div>
</div>
`;
if (overdue.length > 0) {
html += `<div class="mb-4"><h3 class="font-bold text-red-600 mb-2">⏰ Просрочено (${overdue.length})</h3>
<div class="overflow-x-auto"><table class="table"><thead><tr><th>Курс</th><th>Уровень</th><th>Завершён</th><th>Должен пройти</th><th>Просрочка</th></tr></thead><tbody>`;
for (const c of overdue) {
html += `<tr><td>${c.courseName}</td><td>Ур.${c.level}</td><td>${formatDate(c.completedDate)}</td><td>${formatDate(c.nextDate)}</td><td class="status-danger">⚠️ ${c.daysOverdue} дн.</td></tr>`;
}
html += `</tbody></table></div></div>`;
}
if (valid.length > 0) {
html += `<div class="mb-4"><h3 class="font-bold text-green-600 mb-2">✅ Действует (${valid.length})</h3>
<div class="overflow-x-auto"><table class="table"><thead><tr><th>Курс</th><th>Уровень</th><th>Завершён</th><th>Следующее</th><th></th></tr></thead><tbody>`;
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 += `<tr><td>${c.courseName}</td><td>Ур.${c.level}</td><td>${formatDate(c.completedDate)}</td><td class="${urgency}">${formatDate(c.nextDate)} ${daysLeft <= 30 ? '⚠️ ' + daysLeft + ' дн.' : ''}</td><td></td></tr>`;
}
html += `</tbody></table></div></div>`;
}
if (notStarted.length > 0) {
html += `<div><h3 class="font-bold text-gray-500 mb-2">❗ Не пройдено (${notStarted.length})</h3><ul class="list-disc list-inside text-gray-600">`;
for (const c of notStarted) {
html += `<li>${c.courseName} (Ур.${c.level})</li>`;
}
html += `</ul></div>`;
}
html += '</div>';
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 => `
<div class="card cursor-pointer hover:border-[#00E5FF] transition" onclick="showCityEmployees('${s.city}')">
<div class="text-2xl font-bold">${s.count}</div>
<div class="text-sm text-gray-500">${s.city}</div>
<div class="progress-bar mt-2"><div class="progress-fill" style="width:${s.progress}%"></div></div>
<div class="text-xs mt-1">${s.progress}% выполнено ${s.overdue > 0 ? `<span class="text-red-500">| ${s.overdue} просрочено</span>` : ''}</div>
</div>
`).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 = `
<div class="card text-center"><div class="text-3xl font-bold">${totalEmp}</div><div class="text-sm text-gray-500">Сотрудников</div></div>
<div class="card text-center"><div class="text-3xl font-bold">${progress}%</div><div class="text-sm text-gray-500">Выполнено</div></div>
<div class="card text-center"><div class="text-3xl font-bold text-red-500">${overdue}</div><div class="text-sm text-gray-500">Просрочено</div></div>
<div class="card text-center"><div class="text-3xl font-bold text-yellow-500">${notStarted}</div><div class="text-sm text-gray-500">Не начато</div></div>
`;
// Проблемные филиалы
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) =>
`<div class="flex justify-between items-center py-2 ${i < cityStats.length - 1 ? 'border-b' : ''}">
<span>${i + 1}. ${s.city}</span>
<span class="status-danger">${s.pct}% просрочено (${s.overdue})</span>
</div>`
).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) =>
`<div class="flex justify-between items-center py-2 ${i < courseStats.length - 1 ? 'border-b' : ''}">
<span class="text-sm">${i + 1}. ${s.name}</span>
<span class="status-danger text-sm">${s.pct}%</span>
</div>`
).join('');
<div class="card text-center"><div class="text-3xl font-bold">${totalEmp}</div><div class="text-sm text-gray-500">Сотрудников</div></div>
<div class="card text-center"><div class="text-3xl font-bold">${progress}%</div><div class="text-sm text-gray-500">Выполнено</div></div>
<div class="card text-center"><div class="text-3xl font-bold text-red-500">${overdue}</div><div class="text-sm text-gray-500">Просрочено</div></div>
<div class="card text-center"><div class="text-3xl font-bold text-yellow-500">${notStarted}</div><div class="text-sm text-gray-500">Не начато</div></div>
`;
// Проблемные филиалы
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) =>
`<div class="flex justify-between items-center py-2 ${i < cityStats.length - 1 ? 'border-b' : ''}">
<span>${i + 1}. ${s.city}</span>
<span class="status-danger">${s.pct}% просрочено (${s.overdue})</span>
</div>`
).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) =>
`<div class="flex justify-between items-center py-2 ${i < courseStats.length - 1 ? 'border-b' : ''}">
<span class="text-sm">${i + 1}. ${s.name}</span>
<span class="status-danger text-sm">${s.pct}%</span>
</div>`
).join('');
}
// ========== ВЫГРУЗКА ==========
function exportData(type) {
const container = document.getElementById('export-results');
let html = '<div class="card overflow-x-auto">';
let data = [];
if (type === 'overdue') {
html += '<h3 class="font-bold mb-3">⏰ Просроченные обучения</h3>';
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 += '<h3 class="font-bold mb-3">🏙️ По филиалам</h3>';
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 += '<h3 class="font-bold mb-3">👤 По сотрудникам</h3>';
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 += '<h3 class="font-bold mb-3">📋 Сводный отчёт по филиалам</h3>';
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 += '<p class="text-gray-500">Нет данных для выгрузки</p>';
} else {
const headers = Object.keys(data[0]);
html += '<table class="table"><thead><tr>' + headers.map(h => `<th>${h}</th>`).join('') + '</tr></thead><tbody>';
for (const row of data.slice(0, 100)) {
html += '<tr>' + headers.map(h => `<td>${row[h]}</td>`).join('') + '</tr>';
}
html += '</tbody></table>';
if (data.length > 100) html += `<p class="text-sm text-gray-500 mt-2">Показано 100 из ${data.length} записей</p>`;
}
html += '</div>';
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);
}