Обновление дашборда с загрузкой файлов

This commit is contained in:
Dauren777 2026-06-19 06:16:55 +00:00
parent 29a38a42dc
commit 4fc0930592
2 changed files with 273 additions and 0 deletions

View File

@ -107,6 +107,21 @@ body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
<div id="tab-dashboard" class="tab-content hidden"> <div id="tab-dashboard" class="tab-content hidden">
<div class="card"> <div class="card">
<h2 class="text-xl font-bold mb-4">📊 Дашборд обучения ТБ</h2> <h2 class="text-xl font-bold mb-4">📊 Дашборд обучения ТБ</h2>
<div class="mb-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-blue-800">📤 Загрузка данных</p>
<p class="text-sm text-blue-600">Загрузите Excel (.xlsx) или CSV файл с данными сотрудников</p>
</div>
<label class="cursor-pointer">
<div class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition flex items-center gap-2">
<span>📁 Выбрать файл</span>
<input type="file" id="file-upload" accept=".csv,.xlsx" class="hidden" onchange="handleFileUpload(event)">
</div>
</label>
</div>
<div id="upload-results" class="mt-3"></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6" id="dash-stats"></div> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6" id="dash-stats"></div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="card"> <div class="card">

258
script.js
View File

@ -322,6 +322,230 @@ function showCityEmployees(city) {
showTab('search'); 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() { function renderDashboard() {
const totalEmp = EMPLOYEES.length; const totalEmp = EMPLOYEES.length;
@ -360,6 +584,40 @@ function renderDashboard() {
return { name: c.name, notDone, pct: Math.round((notDone / total) * 100) }; return { name: c.name, notDone, pct: Math.round((notDone / total) * 100) };
}).sort((a, b) => b.pct - a.pct).slice(0, 5); }).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) => document.getElementById('dash-courses').innerHTML = courseStats.map((s, i) =>
`<div class="flex justify-between items-center py-2 ${i < courseStats.length - 1 ? 'border-b' : ''}"> `<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="text-sm">${i + 1}. ${s.name}</span>