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

335 lines
12 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.

const XLSX = window.XLSX;
let mainData = null;
let templateData = null;
let mainFileName = '';
let templateFileName = '';
let mappedResult = null;
// Обработка выбора файла
function handleFileSelect(event, type) {
const file = event.target.files[0];
if (!file) return;
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) => {
mainData = data;
checkFilesReady();
});
} else {
templateFileName = file.name;
readExcel(file, (data) => {
templateData = data;
checkFilesReady();
});
}
}
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) {
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
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);
callback(json);
};
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 keyValue = normalizeValue(templateRow[templateKeyField]);
const mainRow = mainMap.get(keyValue);
if (mainRow) {
matched++;
// Заполняем пустые поля
fieldsToFill.forEach(field => {
if (!templateRow[field] && mainRow[field]) {
filled++;
templateRow[field] = mainRow[field];
}
});
} else {
notMatched++;
}
return {
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 value = row.templateRow[field] !== undefined ? row.templateRow[field] : '';
const fromMain = row.mainRow && row.mainRow[field] && !row.templateRow[field];
const cellStyle = fromMain ? 'background: var(--kt-ai-success-bg); font-weight: 600;' : '';
bodyHtml += '<td style="' + cellStyle + '">' + escapeHtml(value) + '</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 keyValue = normalizeValue(row[templateKeyField]);
const mainRow = mainMap.get(keyValue);
if (mainRow) {
fieldsToFill.forEach(field => {
if (!row[field] && mainRow[field]) {
row[field] = mainRow[field];
totalFilled++;
}
});
}
return row;
});
// Создаём 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;
}