let mainData = null; let templateData = null; let mainFileName = ''; let templateFileName = ''; let mappedResult = null; // Проверка загрузки XLSX function getXLSX() { if (window.XLSX) return window.XLSX; alert('Библиотека для работы с Excel ещё не загрузилась. Подождите секунду...'); return null; } // Обработка выбора файла function handleFileSelect(event, type) { const XLSX = getXLSX(); if (!XLSX) return; const file = event.target.files[0]; if (!file) return; console.log('Файл выбран:', type, file.name, file.size); const zone = document.getElementById(type + '-upload'); const info = document.getElementById(type + '-file-info'); zone.classList.add('has-file'); info.textContent = file.name + ' (' + formatFileSize(file.size) + ')'; if (type === 'main') { mainFileName = file.name; readExcel(file, (data) => { console.log('Основной файл прочитан, строк:', data.length); mainData = data; checkFilesReady(); }, (error) => { console.error('Ошибка чтения основного файла:', error); alert('Ошибка чтения файла: ' + error.message); }); } else { templateFileName = file.name; readExcel(file, (data) => { console.log('Шаблон прочитан, строк:', data.length); templateData = data; checkFilesReady(); }, (error) => { console.error('Ошибка чтения шаблона:', error); alert('Ошибка чтения файла: ' + error.message); }); } } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' Б'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' КБ'; return (bytes / (1024 * 1024)).toFixed(1) + ' МБ'; } function readExcel(file, callback, errorCallback) { const XLSX = window.XLSX; if (!XLSX) { if (errorCallback) errorCallback(new Error('Библиотека XLSX не загружена')); return; } console.log('Чтение файла:', file.name); const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); console.log('Файл загружен в память, байт:', data.length); const workbook = XLSX.read(data, { type: 'array' }); console.log('Workbook прочитан, листов:', workbook.SheetNames.length); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const json = XLSX.utils.sheet_to_json(worksheet); console.log('Данные конвертированы в JSON, строк:', json.length); callback(json); } catch (error) { console.error('Ошибка при чтении Excel:', error); if (errorCallback) errorCallback(error); else alert('Ошибка при чтении файла: ' + error.message); } }; reader.onerror = () => { const error = new Error('Не удалось прочитать файл'); if (errorCallback) errorCallback(error); else alert(error.message); }; reader.readAsArrayBuffer(file); } function checkFilesReady() { const btn = document.getElementById('btn-mapping'); if (mainData && mainData.length > 0 && templateData && templateData.length > 0) { btn.disabled = false; } } function goToMapping() { if (!mainData || !templateData) return; document.getElementById('upload-section').classList.add('hidden'); document.getElementById('mapping-section').classList.remove('hidden'); document.getElementById('step1').classList.remove('active'); document.getElementById('step1').classList.add('completed'); document.getElementById('step2').classList.add('active'); populateFieldSelectors(); } function goBackToUpload() { document.getElementById('upload-section').classList.remove('hidden'); document.getElementById('mapping-section').classList.add('hidden'); document.getElementById('step1').classList.add('active'); document.getElementById('step2').classList.remove('active', 'completed'); } function populateFieldSelectors() { const mainFields = Object.keys(mainData[0] || {}); const templateFields = Object.keys(templateData[0] || {}); const mainSelect = document.getElementById('main-key-field'); const templateSelect = document.getElementById('template-key-field'); mainSelect.innerHTML = ''; templateSelect.innerHTML = ''; // Поиск полей с номером телефона и лицевым счетом const phoneFields = ['телефон', 'phone', 'tel', 'номер', 'number', 'лицевой', 'account', 'лс', 'флс']; phoneFields.forEach(field => { const mainMatch = mainFields.find(f => f.toLowerCase().includes(field)); const templateMatch = templateFields.find(f => f.toLowerCase().includes(field)); if (mainMatch && !mainSelect.querySelector(`[value="${mainMatch}"]`)) { mainSelect.add(new Option(mainMatch, mainMatch)); } if (templateMatch && !templateSelect.querySelector(`[value="${templateMatch}"]`)) { templateSelect.add(new Option(templateMatch, templateMatch)); } }); // Добавляем все остальные поля mainFields.forEach(field => { if (!mainSelect.querySelector(`[value="${field}"]`)) { mainSelect.add(new Option(field, field)); } }); templateFields.forEach(field => { if (!templateSelect.querySelector(`[value="${field}"]`)) { templateSelect.add(new Option(field, field)); } }); // Создаём первую строку сопоставления addMappingRow(); } function addMappingRow() { const container = document.getElementById('column-mappings'); const mainFields = Object.keys(mainData[0] || {}); const templateFields = Object.keys(templateData[0] || {}); const rowId = 'mapping-' + Date.now(); const row = document.createElement('div'); row.id = rowId; row.className = 'mapping-row'; row.style.cssText = 'display: grid; grid-template-columns: 1fr auto 1fr auto; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-2);'; // Заполняем столбец шаблона let templateOptions = ''; templateFields.forEach(field => { templateOptions += ''; }); // Заполняем столбец основного файла let mainOptions = ''; mainFields.forEach(field => { mainOptions += ''; }); row.innerHTML = `
`; container.appendChild(row); processMapping(); } function removeMappingRow(rowId) { const row = document.getElementById(rowId); if (row) { row.remove(); processMapping(); } } function getColumnMappings() { const mappings = []; document.querySelectorAll('.template-column').forEach((select, index) => { const mainSelect = document.querySelectorAll('.main-column')[index]; if (select && mainSelect) { mappings.push({ templateColumn: select.value, mainColumn: mainSelect.value }); } }); return mappings; } function processMapping() { const mainKeyField = document.getElementById('main-key-field').value; const templateKeyField = document.getElementById('template-key-field').value; if (!mainKeyField || !templateKeyField) return; // Получаем ручные сопоставления столбцов const columnMappings = getColumnMappings(); // Создаём карту соответствий const mainMap = new Map(); mainData.forEach(row => { const keyValue = normalizeValue(row[mainKeyField]); if (keyValue) { mainMap.set(keyValue, row); } }); // Сопоставляем данные (без мутации!) let matched = 0; let notMatched = 0; let filled = 0; const previewRows = templateData.slice(0, 10).map((templateRow, index) => { // Создаём КОПИЮ строки, чтобы не мутировать исходные данные const rowCopy = { ...templateRow }; const keyValue = normalizeValue(templateRow[templateKeyField]); const mainRow = mainMap.get(keyValue); if (mainRow) { matched++; // Заполняем пустые поля согласно сопоставлениям columnMappings.forEach(mapping => { const templateCol = mapping.templateColumn; const mainCol = mapping.mainColumn; const isEmpty = rowCopy[templateCol] === null || rowCopy[templateCol] === undefined || String(rowCopy[templateCol]).trim() === ''; const hasValueInMain = mainRow[mainCol] !== null && mainRow[mainCol] !== undefined; if (isEmpty && hasValueInMain) { filled++; rowCopy[templateCol] = mainRow[mainCol]; } }); } else { notMatched++; } return { templateRow: rowCopy, originalRow: templateRow, mainRow, matched: !!mainRow }; }); // Статистика const total = templateData.length; const matchPercent = total > 0 ? Math.round((matched / total) * 100) : 0; document.getElementById('mapping-stats').innerHTML = `
${total}
Всего записей в шаблоне
${matched}
Сопоставлено (${matchPercent}%)
${notMatched}
Не найдено совпадений
${filled}
Полей заполнено (в предпросмотре)
`; // Предпросмотр const table = document.getElementById('mapping-preview'); const allFields = new Set([...Object.keys(templateData[0] || {}), ...fieldsToFill]); let headerHtml = ''; allFields.forEach(field => { headerHtml += '' + escapeHtml(field) + ''; }); headerHtml += 'Статус'; let bodyHtml = ''; previewRows.forEach(row => { bodyHtml += ''; allFields.forEach(field => { const originalValue = row.originalRow[field]; const newValue = row.templateRow[field]; // Проверяем, было ли это поле заполнено из сопоставления let wasFilled = false; columnMappings.forEach(mapping => { if (mapping.templateColumn === field) { const wasEmpty = originalValue === null || originalValue === undefined || String(originalValue).trim() === ''; const hasValueInMain = row.mainRow && row.mainRow[mapping.mainColumn] !== null && row.mainRow[mapping.mainColumn] !== undefined; wasFilled = wasEmpty && hasValueInMain; } }); const cellStyle = wasFilled ? 'background: var(--kt-ai-success-bg); font-weight: 600;' : ''; bodyHtml += '' + escapeHtml(newValue !== null && newValue !== undefined ? newValue : '') + ''; }); bodyHtml += '' + (row.matched ? '✓' : '✗') + ''; }); bodyHtml += ''; table.innerHTML = headerHtml + bodyHtml; // Сохраняем результат для дальнейшей обработки mappedResult = { templateData, columnMappings, mainMap, templateKeyField }; document.getElementById('btn-process').disabled = matched === 0; } function processFiles() { if (!mappedResult) return; const { templateData, columnMappings, mainMap, templateKeyField } = mappedResult; // Полная обработка всех данных (без мутации!) let totalFilled = 0; const resultData = templateData.map(row => { // Создаём КОПИЮ строки const newRow = { ...row }; const keyValue = normalizeValue(row[templateKeyField]); const mainRow = mainMap.get(keyValue); if (mainRow) { columnMappings.forEach(mapping => { const templateCol = mapping.templateColumn; const mainCol = mapping.mainColumn; // Проверяем на пустое значение (null, undefined, пустая строка) const isEmpty = newRow[templateCol] === null || newRow[templateCol] === undefined || String(newRow[templateCol]).trim() === ''; const hasValueInMain = mainRow[mainCol] !== null && mainRow[mainCol] !== undefined; if (isEmpty && hasValueInMain) { newRow[templateCol] = mainRow[mainCol]; totalFilled++; } }); } return newRow; }); // Создаём Excel файл const worksheet = XLSX.utils.json_to_sheet(resultData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Результат'); // Генерируем имя файла const baseName = templateFileName.replace(/\.(xlsx|xls|csv)$/i, ''); const outputFileName = baseName + '_заполненный.xlsx'; // Создаём ссылку для скачивания const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const url = URL.createObjectURL(blob); const downloadLink = document.getElementById('download-link'); downloadLink.href = url; downloadLink.download = outputFileName; // Показываем результат document.getElementById('mapping-section').classList.add('hidden'); document.getElementById('result-section').classList.remove('hidden'); document.getElementById('step2').classList.remove('active'); document.getElementById('step2').classList.add('completed'); document.getElementById('step3').classList.add('active'); // Итоговая статистика const matchedCount = resultData.filter(row => { const keyValue = normalizeValue(row[templateKeyField]); return mainMap.has(keyValue); }).length; document.getElementById('result-stats').innerHTML = `
${templateData.length}
Всего записей обработано
${matchedCount}
Найдено совпадений
${totalFilled}
Полей заполнено
${outputFileName}
Файл готов
`; } function normalizeValue(value) { if (value === null || value === undefined) return ''; const str = String(value).trim(); // Удаляем все нецифровые символы для номеров и счетов return str.replace(/\D/g, ''); } function escapeHtml(text) { if (text === null || text === undefined) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }