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;
}