Обновление дашборда с загрузкой файлов
This commit is contained in:
parent
29a38a42dc
commit
4fc0930592
15
index.html
15
index.html
@ -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 class="card">
|
||||
<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 md:grid-cols-2 gap-4">
|
||||
<div class="card">
|
||||
|
||||
258
script.js
258
script.js
@ -322,6 +322,230 @@ function showCityEmployees(city) {
|
||||
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;
|
||||
@ -360,6 +584,40 @@ function renderDashboard() {
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user