Добавлено ручное сопоставление конкретных столбцов: шаблон -> основной файл
This commit is contained in:
parent
c1a2b091f2
commit
a875b8ab7b
@ -218,21 +218,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4);">
|
<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);">
|
<h3 style="margin-bottom: var(--kt-ai-space-4);">1. Ключевое поле для поиска</h3>
|
||||||
|
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3);">По какому полю искать совпадения (номер телефона или лицевой счет)</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--kt-ai-space-4); align-items: end;">
|
||||||
<div class="kt-ai-field">
|
<div class="kt-ai-field">
|
||||||
<label>Поле из основного файла</label>
|
<label>В основном файле</label>
|
||||||
<select class="kt-ai-select" id="main-key-field" onchange="processMapping()"></select>
|
<select class="kt-ai-select" id="main-key-field" onchange="processMapping()"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="padding-top: var(--kt-ai-space-6);">
|
||||||
|
<svg class="kt-icon kt-icon-lg" style="color: var(--kt-ai-primary);" aria-hidden="true"><use href="#arrowRight"></use></svg>
|
||||||
|
</div>
|
||||||
<div class="kt-ai-field">
|
<div class="kt-ai-field">
|
||||||
<label>Поле из шаблона</label>
|
<label>В шаблоне</label>
|
||||||
<select class="kt-ai-select" id="template-key-field" onchange="processMapping()"></select>
|
<select class="kt-ai-select" id="template-key-field" onchange="processMapping()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: var(--kt-ai-space-4);">
|
<h3 style="margin: var(--kt-ai-space-6) 0 var(--kt-ai-space-4);">2. Какие столбцы заполнять</h3>
|
||||||
<label style="font-weight: var(--kt-ai-weight-semibold); display: block; margin-bottom: var(--kt-ai-space-2);">Поля для заполнения:</label>
|
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3);">Укажите, какие столбцы из какого поля брать</p>
|
||||||
<div id="fields-to-fill"></div>
|
|
||||||
|
<div id="column-mappings" style="margin-bottom: var(--kt-ai-space-4);">
|
||||||
|
<!-- Сюда добавляются пары столбцов -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="kt-ai-btn" data-variant="ghost" onclick="addMappingRow()" style="margin-bottom: var(--kt-ai-space-4);">
|
||||||
|
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#plus"></use></svg>
|
||||||
|
Добавить пару столбцов
|
||||||
|
</button>
|
||||||
|
|
||||||
<div id="mapping-stats" class="stats-grid"></div>
|
<div id="mapping-stats" class="stats-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
135
excel-editor.js
135
excel-editor.js
@ -153,45 +153,84 @@ function populateFieldSelectors() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Заполняем чекбоксы полей для заполнения
|
// Создаём первую строку сопоставления
|
||||||
const fieldsContainer = document.getElementById('fields-to-fill');
|
addMappingRow();
|
||||||
fieldsContainer.innerHTML = '';
|
}
|
||||||
|
|
||||||
|
function addMappingRow() {
|
||||||
|
const container = document.getElementById('column-mappings');
|
||||||
|
const mainFields = Object.keys(mainData[0] || {});
|
||||||
|
const templateFields = Object.keys(templateData[0] || {});
|
||||||
|
|
||||||
|
const rowId = 'mapping-' + Date.now();
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.id = rowId;
|
||||||
|
row.className = 'mapping-row';
|
||||||
|
row.style.cssText = 'display: grid; grid-template-columns: 1fr auto 1fr auto; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-2);';
|
||||||
|
|
||||||
|
// Заполняем столбец шаблона
|
||||||
|
let templateOptions = '';
|
||||||
templateFields.forEach(field => {
|
templateFields.forEach(field => {
|
||||||
const div = document.createElement('div');
|
templateOptions += '<option value="' + escapeHtml(field) + '">' + escapeHtml(field) + '</option>';
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Заполняем столбец основного файла
|
||||||
|
let mainOptions = '';
|
||||||
|
mainFields.forEach(field => {
|
||||||
|
mainOptions += '<option value="' + escapeHtml(field) + '">' + escapeHtml(field) + '</option>';
|
||||||
|
});
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div class="kt-ai-field" style="margin: 0;">
|
||||||
|
<label style="font-size: var(--kt-ai-text-xs);">Столбец шаблона</label>
|
||||||
|
<select class="kt-ai-select template-column" onchange="processMapping()">${templateOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div style="padding-top: var(--kt-ai-space-5);">
|
||||||
|
<svg class="kt-icon" style="color: var(--kt-ai-primary);" aria-hidden="true"><use href="#arrowRight"></use></svg>
|
||||||
|
</div>
|
||||||
|
<div class="kt-ai-field" style="margin: 0;">
|
||||||
|
<label style="font-size: var(--kt-ai-text-xs);">Столбец основного файла</label>
|
||||||
|
<select class="kt-ai-select main-column" onchange="processMapping()">${mainOptions}</select>
|
||||||
|
</div>
|
||||||
|
<button class="kt-ai-btn" data-variant="ghost" onclick="removeMappingRow('${rowId}')" style="margin-top: var(--kt-ai-space-5);">
|
||||||
|
<svg class="kt-icon" aria-hidden="true"><use href="#trash"></use></svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(row);
|
||||||
processMapping();
|
processMapping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeMappingRow(rowId) {
|
||||||
|
const row = document.getElementById(rowId);
|
||||||
|
if (row) {
|
||||||
|
row.remove();
|
||||||
|
processMapping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnMappings() {
|
||||||
|
const mappings = [];
|
||||||
|
document.querySelectorAll('.template-column').forEach((select, index) => {
|
||||||
|
const mainSelect = document.querySelectorAll('.main-column')[index];
|
||||||
|
if (select && mainSelect) {
|
||||||
|
mappings.push({
|
||||||
|
templateColumn: select.value,
|
||||||
|
mainColumn: mainSelect.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|
||||||
function processMapping() {
|
function processMapping() {
|
||||||
const mainKeyField = document.getElementById('main-key-field').value;
|
const mainKeyField = document.getElementById('main-key-field').value;
|
||||||
const templateKeyField = document.getElementById('template-key-field').value;
|
const templateKeyField = document.getElementById('template-key-field').value;
|
||||||
|
|
||||||
if (!mainKeyField || !templateKeyField) return;
|
if (!mainKeyField || !templateKeyField) return;
|
||||||
|
|
||||||
// Собираем поля для заполнения
|
// Получаем ручные сопоставления столбцов
|
||||||
const fieldsToFill = [];
|
const columnMappings = getColumnMappings();
|
||||||
document.querySelectorAll('#fields-to-fill input[type="checkbox"]:checked').forEach(cb => {
|
|
||||||
fieldsToFill.push(cb.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создаём карту соответствий
|
// Создаём карту соответствий
|
||||||
const mainMap = new Map();
|
const mainMap = new Map();
|
||||||
@ -215,12 +254,16 @@ function processMapping() {
|
|||||||
|
|
||||||
if (mainRow) {
|
if (mainRow) {
|
||||||
matched++;
|
matched++;
|
||||||
// Заполняем пустые поля в копии
|
// Заполняем пустые поля согласно сопоставлениям
|
||||||
fieldsToFill.forEach(field => {
|
columnMappings.forEach(mapping => {
|
||||||
const isEmpty = rowCopy[field] === null || rowCopy[field] === undefined || rowCopy[field] === '';
|
const templateCol = mapping.templateColumn;
|
||||||
if (isEmpty && mainRow[field] !== null && mainRow[field] !== undefined) {
|
const mainCol = mapping.mainColumn;
|
||||||
|
const isEmpty = rowCopy[templateCol] === null || rowCopy[templateCol] === undefined || String(rowCopy[templateCol]).trim() === '';
|
||||||
|
const hasValueInMain = mainRow[mainCol] !== null && mainRow[mainCol] !== undefined;
|
||||||
|
|
||||||
|
if (isEmpty && hasValueInMain) {
|
||||||
filled++;
|
filled++;
|
||||||
rowCopy[field] = mainRow[field];
|
rowCopy[templateCol] = mainRow[mainCol];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -274,9 +317,18 @@ function processMapping() {
|
|||||||
allFields.forEach(field => {
|
allFields.forEach(field => {
|
||||||
const originalValue = row.originalRow[field];
|
const originalValue = row.originalRow[field];
|
||||||
const newValue = row.templateRow[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;' : '';
|
let wasFilled = false;
|
||||||
|
columnMappings.forEach(mapping => {
|
||||||
|
if (mapping.templateColumn === field) {
|
||||||
|
const wasEmpty = originalValue === null || originalValue === undefined || String(originalValue).trim() === '';
|
||||||
|
const hasValueInMain = row.mainRow && row.mainRow[mapping.mainColumn] !== null && row.mainRow[mapping.mainColumn] !== undefined;
|
||||||
|
wasFilled = wasEmpty && hasValueInMain;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cellStyle = wasFilled ? 'background: var(--kt-ai-success-bg); font-weight: 600;' : '';
|
||||||
bodyHtml += '<td style="' + cellStyle + '">' + escapeHtml(newValue !== null && newValue !== undefined ? newValue : '') + '</td>';
|
bodyHtml += '<td style="' + cellStyle + '">' + escapeHtml(newValue !== null && newValue !== undefined ? newValue : '') + '</td>';
|
||||||
});
|
});
|
||||||
bodyHtml += '<td>' + (row.matched ? '✓' : '✗') + '</td></tr>';
|
bodyHtml += '<td>' + (row.matched ? '✓' : '✗') + '</td></tr>';
|
||||||
@ -286,7 +338,7 @@ function processMapping() {
|
|||||||
table.innerHTML = headerHtml + bodyHtml;
|
table.innerHTML = headerHtml + bodyHtml;
|
||||||
|
|
||||||
// Сохраняем результат для дальнейшей обработки
|
// Сохраняем результат для дальнейшей обработки
|
||||||
mappedResult = { templateData, fieldsToFill, mainMap, templateKeyField };
|
mappedResult = { templateData, columnMappings, mainMap, templateKeyField };
|
||||||
|
|
||||||
document.getElementById('btn-process').disabled = matched === 0;
|
document.getElementById('btn-process').disabled = matched === 0;
|
||||||
}
|
}
|
||||||
@ -294,7 +346,7 @@ function processMapping() {
|
|||||||
function processFiles() {
|
function processFiles() {
|
||||||
if (!mappedResult) return;
|
if (!mappedResult) return;
|
||||||
|
|
||||||
const { templateData, fieldsToFill, mainMap, templateKeyField } = mappedResult;
|
const { templateData, columnMappings, mainMap, templateKeyField } = mappedResult;
|
||||||
|
|
||||||
// Полная обработка всех данных (без мутации!)
|
// Полная обработка всех данных (без мутации!)
|
||||||
let totalFilled = 0;
|
let totalFilled = 0;
|
||||||
@ -305,13 +357,16 @@ function processFiles() {
|
|||||||
const mainRow = mainMap.get(keyValue);
|
const mainRow = mainMap.get(keyValue);
|
||||||
|
|
||||||
if (mainRow) {
|
if (mainRow) {
|
||||||
fieldsToFill.forEach(field => {
|
columnMappings.forEach(mapping => {
|
||||||
|
const templateCol = mapping.templateColumn;
|
||||||
|
const mainCol = mapping.mainColumn;
|
||||||
|
|
||||||
// Проверяем на пустое значение (null, undefined, пустая строка)
|
// Проверяем на пустое значение (null, undefined, пустая строка)
|
||||||
const isEmpty = newRow[field] === null || newRow[field] === undefined || String(newRow[field]).trim() === '';
|
const isEmpty = newRow[templateCol] === null || newRow[templateCol] === undefined || String(newRow[templateCol]).trim() === '';
|
||||||
const hasValueInMain = mainRow[field] !== null && mainRow[field] !== undefined;
|
const hasValueInMain = mainRow[mainCol] !== null && mainRow[mainCol] !== undefined;
|
||||||
|
|
||||||
if (isEmpty && hasValueInMain) {
|
if (isEmpty && hasValueInMain) {
|
||||||
newRow[field] = mainRow[field];
|
newRow[templateCol] = mainRow[mainCol];
|
||||||
totalFilled++;
|
totalFilled++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user