Добавлен Excel редактор для сопоставления данных по номеру телефона/лицевому счету
This commit is contained in:
parent
cccf382818
commit
cd32645377
300
excel-editor.html
Normal file
300
excel-editor.html
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" data-theme="auto">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Excel Редактор | Сопоставление данных</title>
|
||||||
|
<link rel="icon" href="design-system/brand/kt-ai-logo.png">
|
||||||
|
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||||
|
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||||
|
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||||
|
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||||||
|
<style>
|
||||||
|
.kt-ai-btn { text-decoration: none; }
|
||||||
|
.upload-zone {
|
||||||
|
border: 2px dashed var(--kt-ai-border);
|
||||||
|
border-radius: var(--kt-ai-radius-xl);
|
||||||
|
padding: var(--kt-ai-space-8);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: var(--kt-ai-bg-soft);
|
||||||
|
}
|
||||||
|
.upload-zone:hover {
|
||||||
|
border-color: var(--kt-ai-primary);
|
||||||
|
background: var(--kt-ai-primary-bg);
|
||||||
|
}
|
||||||
|
.upload-zone.has-file {
|
||||||
|
border-color: var(--kt-ai-success);
|
||||||
|
background: var(--kt-ai-success-bg);
|
||||||
|
}
|
||||||
|
.file-info {
|
||||||
|
margin-top: var(--kt-ai-space-3);
|
||||||
|
font-size: var(--kt-ai-text-sm);
|
||||||
|
color: var(--kt-ai-fg-muted);
|
||||||
|
}
|
||||||
|
.mapping-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
gap: var(--kt-ai-space-3);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--kt-ai-space-3);
|
||||||
|
border-radius: var(--kt-ai-radius-lg);
|
||||||
|
background: var(--kt-ai-bg-soft);
|
||||||
|
margin-bottom: var(--kt-ai-space-2);
|
||||||
|
}
|
||||||
|
.mapping-row.matched {
|
||||||
|
background: var(--kt-ai-success-bg);
|
||||||
|
}
|
||||||
|
.mapping-row.no-match {
|
||||||
|
background: var(--kt-ai-warning-bg);
|
||||||
|
}
|
||||||
|
.column-badge {
|
||||||
|
padding: var(--kt-ai-space-1) var(--kt-ai-space-2);
|
||||||
|
border-radius: var(--kt-ai-radius-md);
|
||||||
|
background: var(--kt-ai-card-bg);
|
||||||
|
border: 1px solid var(--kt-ai-border);
|
||||||
|
font-size: var(--kt-ai-text-xs);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.preview-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--kt-ai-text-sm);
|
||||||
|
}
|
||||||
|
.preview-table th,
|
||||||
|
.preview-table td {
|
||||||
|
padding: var(--kt-ai-space-2) var(--kt-ai-space-3);
|
||||||
|
border: 1px solid var(--kt-ai-border);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.preview-table th {
|
||||||
|
background: var(--kt-ai-bg-soft);
|
||||||
|
font-weight: var(--kt-ai-weight-semibold);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.preview-table tr:nth-child(even) {
|
||||||
|
background: var(--kt-ai-bg-soft);
|
||||||
|
}
|
||||||
|
.preview-table tr:hover {
|
||||||
|
background: var(--kt-ai-primary-bg);
|
||||||
|
}
|
||||||
|
.table-container {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--kt-ai-border);
|
||||||
|
border-radius: var(--kt-ai-radius-lg);
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--kt-ai-space-4);
|
||||||
|
margin: var(--kt-ai-space-4) 0;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
padding: var(--kt-ai-space-4);
|
||||||
|
border-radius: var(--kt-ai-radius-lg);
|
||||||
|
background: var(--kt-ai-card-bg);
|
||||||
|
border: 1px solid var(--kt-ai-border);
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--kt-ai-text-2xl);
|
||||||
|
font-weight: var(--kt-ai-weight-bold);
|
||||||
|
color: var(--kt-ai-fg);
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--kt-ai-text-xs);
|
||||||
|
color: var(--kt-ai-fg-muted);
|
||||||
|
margin-top: var(--kt-ai-space-1);
|
||||||
|
}
|
||||||
|
.step-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--kt-ai-space-2);
|
||||||
|
margin-bottom: var(--kt-ai-space-6);
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--kt-ai-space-3);
|
||||||
|
border-radius: var(--kt-ai-radius-lg);
|
||||||
|
background: var(--kt-ai-bg-soft);
|
||||||
|
border: 2px solid var(--kt-ai-border);
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--kt-ai-text-sm);
|
||||||
|
font-weight: var(--kt-ai-weight-semibold);
|
||||||
|
}
|
||||||
|
.step.active {
|
||||||
|
border-color: var(--kt-ai-primary);
|
||||||
|
background: var(--kt-ai-primary-bg);
|
||||||
|
color: var(--kt-ai-primary-fg);
|
||||||
|
}
|
||||||
|
.step.completed {
|
||||||
|
border-color: var(--kt-ai-success);
|
||||||
|
background: var(--kt-ai-success-bg);
|
||||||
|
}
|
||||||
|
.hidden { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="kt-ai-pagehead">
|
||||||
|
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||||
|
<div style="display: flex; align-items: center; gap: var(--kt-ai-space-3);">
|
||||||
|
<img src="design-system/brand/kt-ai-logo.png" alt="" height="24">
|
||||||
|
<strong>Excel Редактор</strong>
|
||||||
|
</div>
|
||||||
|
<span class="kt-ai-spacer"></span>
|
||||||
|
<a class="kt-ai-btn" data-variant="ghost" href="index.html">← Назад</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="kt-ai-hero" data-align="center">
|
||||||
|
<div class="kt-ai-wrap">
|
||||||
|
<h1>Сопоставление Excel файлов</h1>
|
||||||
|
<p class="kt-ai-hero-sub">Заполните пустые столбцы шаблона данными из основного файла по номеру телефона или лицевому счету</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="kt-ai-wrap">
|
||||||
|
<div class="step-indicator">
|
||||||
|
<div class="step active" id="step1">1. Загрузка файлов</div>
|
||||||
|
<div class="step" id="step2">2. Настройка полей</div>
|
||||||
|
<div class="step" id="step3">3. Результат</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 1: UPLOAD -->
|
||||||
|
<div id="upload-section">
|
||||||
|
<div class="kt-ai-section">
|
||||||
|
<div class="kt-ai-wrap">
|
||||||
|
<div class="kt-ai-section-head">
|
||||||
|
<h2>Загрузите файлы</h2>
|
||||||
|
<p>Основной файл содержит данные для заполнения, шаблон — файл для заполнения</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--kt-ai-space-6); margin-top: var(--kt-ai-space-6);">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin-bottom: var(--kt-ai-space-3);">Основной файл</h3>
|
||||||
|
<div class="upload-zone" id="main-upload" onclick="document.getElementById('main-file').click()">
|
||||||
|
<svg class="kt-icon kt-icon-lg" style="color: var(--kt-ai-primary);" aria-hidden="true"><use href="#file"></use></svg>
|
||||||
|
<p style="margin-top: var(--kt-ai-space-3); font-weight: var(--kt-ai-weight-medium);">Основной файл с данными</p>
|
||||||
|
<p style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1);">.xlsx, .xls, .csv</p>
|
||||||
|
<div class="file-info" id="main-file-info"></div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="main-file" accept=".xlsx,.xls,.csv" style="display: none;" onchange="handleFileSelect(event, 'main')">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 style="margin-bottom: var(--kt-ai-space-3);">Файл-шаблон</h3>
|
||||||
|
<div class="upload-zone" id="template-upload" onclick="document.getElementById('template-file').click()">
|
||||||
|
<svg class="kt-icon kt-icon-lg" style="color: var(--kt-ai-primary);" aria-hidden="true"><use href="#file"></use></svg>
|
||||||
|
<p style="margin-top: var(--kt-ai-space-3); font-weight: var(--kt-ai-weight-medium);">Шаблон для заполнения</p>
|
||||||
|
<p style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1);">.xlsx, .xls, .csv</p>
|
||||||
|
<div class="file-info" id="template-file-info"></div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="template-file" accept=".xlsx,.xls,.csv" style="display: none;" onchange="handleFileSelect(event, 'template')">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: var(--kt-ai-space-6); text-align: center;">
|
||||||
|
<button class="kt-ai-btn" data-variant="primary" data-size="lg" id="btn-mapping" onclick="goToMapping()" disabled>
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#arrowRight"></use></svg>
|
||||||
|
Продолжить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 2: MAPPING -->
|
||||||
|
<div id="mapping-section" class="hidden">
|
||||||
|
<div class="kt-ai-section">
|
||||||
|
<div class="kt-ai-wrap">
|
||||||
|
<div class="kt-ai-section-head">
|
||||||
|
<h2>Настройте поля для сопоставления</h2>
|
||||||
|
<p>Укажите, какие поля использовать для поиска соответствий</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kt-ai-card" style="padding: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4);">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-4);">
|
||||||
|
<div class="kt-ai-field">
|
||||||
|
<label>Поле из основного файла</label>
|
||||||
|
<select class="kt-ai-select" id="main-key-field" onchange="processMapping()"></select>
|
||||||
|
</div>
|
||||||
|
<div class="kt-ai-field">
|
||||||
|
<label>Поле из шаблона</label>
|
||||||
|
<select class="kt-ai-select" id="template-key-field" onchange="processMapping()"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: var(--kt-ai-space-4);">
|
||||||
|
<label style="font-weight: var(--kt-ai-weight-semibold); display: block; margin-bottom: var(--kt-ai-space-2);">Поля для заполнения:</label>
|
||||||
|
<div id="fields-to-fill"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mapping-stats" class="stats-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kt-ai-card" style="padding: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4);">
|
||||||
|
<h3 style="margin-bottom: var(--kt-ai-space-4);">Предпросмотр сопоставления</h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="preview-table" id="mapping-preview"></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: var(--kt-ai-space-6); display: flex; gap: var(--kt-ai-space-3);">
|
||||||
|
<button class="kt-ai-btn" data-variant="ghost" onclick="goBackToUpload()">Назад</button>
|
||||||
|
<button class="kt-ai-btn" data-variant="primary" data-size="lg" id="btn-process" onclick="processFiles()" disabled>
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#rocket"></use></svg>
|
||||||
|
Заполнить и скачать результат
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 3: RESULT -->
|
||||||
|
<div id="result-section" class="hidden">
|
||||||
|
<div class="kt-ai-section">
|
||||||
|
<div class="kt-ai-wrap">
|
||||||
|
<div class="kt-ai-section-head">
|
||||||
|
<h2>Готово!</h2>
|
||||||
|
<p>Файл успешно обработан</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6); text-align: center;">
|
||||||
|
<svg class="kt-icon kt-icon-lg" style="color: var(--kt-ai-success);" aria-hidden="true"><use href="#check"></use></svg>
|
||||||
|
<h3 style="margin: var(--kt-ai-space-4) 0;">Файл готов к скачиванию</h3>
|
||||||
|
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-4);">Все пустые столбцы заполнены данными из основного файла</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: var(--kt-ai-space-3); justify-content: center; margin-top: var(--kt-ai-space-4);">
|
||||||
|
<a class="kt-ai-btn" data-variant="primary" data-size="lg" id="download-link" href="#">
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#download"></use></svg>
|
||||||
|
Скачать Excel
|
||||||
|
</a>
|
||||||
|
<button class="kt-ai-btn" data-variant="ghost" onclick="location.reload()">
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#refresh"></use></svg>
|
||||||
|
Обработать ещё
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid" id="result-stats"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||||||
|
<span>© 2026 Excel Редактор</span>
|
||||||
|
<span class="kt-ai-spacer"></span>
|
||||||
|
<span>Сопоставление данных по ключевым полям</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
|
||||||
|
<script src="excel-editor.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
334
excel-editor.js
Normal file
334
excel-editor.js
Normal file
@ -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 = `
|
||||||
|
<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;
|
||||||
|
}
|
||||||
@ -68,6 +68,10 @@
|
|||||||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#plus"></use></svg>
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#plus"></use></svg>
|
||||||
Добавить оборудование
|
Добавить оборудование
|
||||||
</a>
|
</a>
|
||||||
|
<a class="kt-ai-nav-item" href="excel-editor.html" target="_blank">
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#table"></use></svg>
|
||||||
|
Excel Редактор
|
||||||
|
</a>
|
||||||
<a class="kt-ai-nav-item" href="#" data-tab="reports">
|
<a class="kt-ai-nav-item" href="#" data-tab="reports">
|
||||||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#chart"></use></svg>
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#chart"></use></svg>
|
||||||
Отчёты
|
Отчёты
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user