sozday-sovremennyy-veb-sayt/excel-editor.js

386 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));
}
});
// Заполняем чекбоксы полей для заполнения
const fieldsContainer = document.getElementById('fields-to-fill');
fieldsContainer.innerHTML = '';
templateFields.forEach(field => {
const div = document.createElement('div');
div.style.cssText = 'display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); margin-right: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-2);';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'field-' + field;
checkbox.value = field;
checkbox.checked = true; // По умолчанию все поля выбраны
checkbox.onchange = processMapping;
const label = document.createElement('label');
label.htmlFor = 'field-' + field;
label.textContent = field;
label.style.fontSize = 'var(--kt-ai-text-sm)';
div.appendChild(checkbox);
div.appendChild(label);
fieldsContainer.appendChild(div);
});
processMapping();
}
function processMapping() {
const mainKeyField = document.getElementById('main-key-field').value;
const templateKeyField = document.getElementById('template-key-field').value;
if (!mainKeyField || !templateKeyField) return;
// Собираем поля для заполнения
const fieldsToFill = [];
document.querySelectorAll('#fields-to-fill input[type="checkbox"]:checked').forEach(cb => {
fieldsToFill.push(cb.value);
});
// Создаём карту соответствий
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++;
// Заполняем пустые поля в копии
fieldsToFill.forEach(field => {
const isEmpty = rowCopy[field] === null || rowCopy[field] === undefined || rowCopy[field] === '';
if (isEmpty && mainRow[field] !== null && mainRow[field] !== undefined) {
filled++;
rowCopy[field] = mainRow[field];
}
});
} 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 = `
<div class="stat-card">
<div class="stat-value">${total}</div>
<div class="stat-label">Всего записей в шаблоне</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-success);">${matched}</div>
<div class="stat-label">Сопоставлено (${matchPercent}%)</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-warning);">${notMatched}</div>
<div class="stat-label">Не найдено совпадений</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-primary);">${filled}</div>
<div class="stat-label">Полей заполнено (в предпросмотре)</div>
</div>
`;
// Предпросмотр
const table = document.getElementById('mapping-preview');
const allFields = new Set([...Object.keys(templateData[0] || {}), ...fieldsToFill]);
let headerHtml = '<thead><tr>';
allFields.forEach(field => {
headerHtml += '<th>' + escapeHtml(field) + '</th>';
});
headerHtml += '<th>Статус</th></tr></thead>';
let bodyHtml = '<tbody>';
previewRows.forEach(row => {
bodyHtml += '<tr class="' + (row.matched ? 'matched' : 'no-match') + '">';
allFields.forEach(field => {
const originalValue = row.originalRow[field];
const newValue = row.templateRow[field];
const wasEmpty = originalValue === null || originalValue === undefined || String(originalValue).trim() === '';
const filled = wasEmpty && newValue !== null && newValue !== undefined;
const cellStyle = filled ? 'background: var(--kt-ai-success-bg); font-weight: 600;' : '';
bodyHtml += '<td style="' + cellStyle + '">' + escapeHtml(newValue !== null && newValue !== undefined ? newValue : '') + '</td>';
});
bodyHtml += '<td>' + (row.matched ? '✓' : '✗') + '</td></tr>';
});
bodyHtml += '</tbody>';
table.innerHTML = headerHtml + bodyHtml;
// Сохраняем результат для дальнейшей обработки
mappedResult = { templateData, fieldsToFill, mainMap, templateKeyField };
document.getElementById('btn-process').disabled = matched === 0;
}
function processFiles() {
if (!mappedResult) return;
const { templateData, fieldsToFill, 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) {
fieldsToFill.forEach(field => {
// Проверяем на пустое значение (null, undefined, пустая строка)
const isEmpty = newRow[field] === null || newRow[field] === undefined || String(newRow[field]).trim() === '';
const hasValueInMain = mainRow[field] !== null && mainRow[field] !== undefined;
if (isEmpty && hasValueInMain) {
newRow[field] = mainRow[field];
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 = `
<div class="stat-card">
<div class="stat-value">${templateData.length}</div>
<div class="stat-label">Всего записей обработано</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-success);">${matchedCount}</div>
<div class="stat-label">Найдено совпадений</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-primary);">${totalFilled}</div>
<div class="stat-label">Полей заполнено</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: var(--kt-ai-fg-muted);">${outputFileName}</div>
<div class="stat-label">Файл готов</div>
</div>
`;
}
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;
}