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 Редактор
+
Отчёты