From cd32645377802ef7f762f84942ac64d0e217c2c5 Mon Sep 17 00:00:00 2001 From: Nurs Date: Wed, 16 Sep 2026 12:21:29 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20Excel=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=20=D0=BD=D0=BE=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D1=83=20=D1=82=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0/=D0=BB=D0=B8=D1=86=D0=B5=D0=B2=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=81=D1=87=D0=B5=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- excel-editor.html | 300 +++++++++++++++++++++++++++++++++++++++++ excel-editor.js | 334 ++++++++++++++++++++++++++++++++++++++++++++++ index.html | 4 + 3 files changed, 638 insertions(+) create mode 100644 excel-editor.html create mode 100644 excel-editor.js diff --git a/excel-editor.html b/excel-editor.html new file mode 100644 index 0000000..5a17b1e --- /dev/null +++ b/excel-editor.html @@ -0,0 +1,300 @@ + + + + + +Excel Редактор | Сопоставление данных + + + + + + + + + +
+
+
+ + Excel Редактор +
+ + ← Назад +
+
+ +
+
+
+

Сопоставление Excel файлов

+

Заполните пустые столбцы шаблона данными из основного файла по номеру телефона или лицевому счету

+
+
+ +
+
+
1. Загрузка файлов
+
2. Настройка полей
+
3. Результат
+
+ + +
+
+
+
+

Загрузите файлы

+

Основной файл содержит данные для заполнения, шаблон — файл для заполнения

+
+ +
+
+

Основной файл

+
+ +

Основной файл с данными

+

.xlsx, .xls, .csv

+
+
+ +
+ +
+

Файл-шаблон

+
+ +

Шаблон для заполнения

+

.xlsx, .xls, .csv

+
+
+ +
+
+ +
+ +
+
+
+
+ + + + + + +
+
+ + + + + + + diff --git a/excel-editor.js b/excel-editor.js new file mode 100644 index 0000000..2510a40 --- /dev/null +++ b/excel-editor.js @@ -0,0 +1,334 @@ +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 = ` +
+
${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 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 += '' + escapeHtml(value) + ''; + }); + bodyHtml += '' + (row.matched ? '✓' : '✗') + ''; + }); + bodyHtml += ''; + + 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 = ` +
+
${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; +} diff --git a/index.html b/index.html index 8f6f028..8860295 100644 --- a/index.html +++ b/index.html @@ -68,6 +68,10 @@ Добавить оборудование + + + Excel Редактор + Отчёты