diff --git a/script.js b/script.js
index 067af2e..296f4a5 100644
--- a/script.js
+++ b/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) =>
+ `
+ ${i + 1}. ${s.name}
+ ${s.pct}%
+
`
+ ).join('');
+
+
+
+
+ `;
+
+ // Проблемные филиалы
+ 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}