Добавлено ручное сопоставление конкретных столбцов: шаблон -> основной файл

This commit is contained in:
Nurs 2026-09-17 03:49:10 +00:00
parent c1a2b091f2
commit a875b8ab7b
2 changed files with 113 additions and 46 deletions

View File

@ -218,22 +218,34 @@
</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);">
<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">
<label>Поле из основного файла</label>
<label>В основном файле</label>
<select class="kt-ai-select" id="main-key-field" onchange="processMapping()"></select>
</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">
<label>Поле из шаблона</label>
<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>
<h3 style="margin: var(--kt-ai-space-6) 0 var(--kt-ai-space-4);">2. Какие столбцы заполнять</h3>
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3);">Укажите, какие столбцы из какого поля брать</p>
<div id="column-mappings" style="margin-bottom: var(--kt-ai-space-4);">
<!-- Сюда добавляются пары столбцов -->
</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>

View File

@ -153,45 +153,84 @@ function populateFieldSelectors() {
}
});
// Заполняем чекбоксы полей для заполнения
const fieldsContainer = document.getElementById('fields-to-fill');
fieldsContainer.innerHTML = '';
// Создаём первую строку сопоставления
addMappingRow();
}
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 => {
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);
templateOptions += '<option value="' + escapeHtml(field) + '">' + escapeHtml(field) + '</option>';
});
// Заполняем столбец основного файла
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();
}
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() {
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 columnMappings = getColumnMappings();
// Создаём карту соответствий
const mainMap = new Map();
@ -215,12 +254,16 @@ function processMapping() {
if (mainRow) {
matched++;
// Заполняем пустые поля в копии
fieldsToFill.forEach(field => {
const isEmpty = rowCopy[field] === null || rowCopy[field] === undefined || rowCopy[field] === '';
if (isEmpty && mainRow[field] !== null && mainRow[field] !== undefined) {
// Заполняем пустые поля согласно сопоставлениям
columnMappings.forEach(mapping => {
const templateCol = mapping.templateColumn;
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++;
rowCopy[field] = mainRow[field];
rowCopy[templateCol] = mainRow[mainCol];
}
});
} else {
@ -274,9 +317,18 @@ function processMapping() {
allFields.forEach(field => {
const originalValue = row.originalRow[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>' + (row.matched ? '✓' : '✗') + '</td></tr>';
@ -286,7 +338,7 @@ function processMapping() {
table.innerHTML = headerHtml + bodyHtml;
// Сохраняем результат для дальнейшей обработки
mappedResult = { templateData, fieldsToFill, mainMap, templateKeyField };
mappedResult = { templateData, columnMappings, mainMap, templateKeyField };
document.getElementById('btn-process').disabled = matched === 0;
}
@ -294,7 +346,7 @@ function processMapping() {
function processFiles() {
if (!mappedResult) return;
const { templateData, fieldsToFill, mainMap, templateKeyField } = mappedResult;
const { templateData, columnMappings, mainMap, templateKeyField } = mappedResult;
// Полная обработка всех данных (без мутации!)
let totalFilled = 0;
@ -305,13 +357,16 @@ function processFiles() {
const mainRow = mainMap.get(keyValue);
if (mainRow) {
fieldsToFill.forEach(field => {
columnMappings.forEach(mapping => {
const templateCol = mapping.templateColumn;
const mainCol = mapping.mainColumn;
// Проверяем на пустое значение (null, undefined, пустая строка)
const isEmpty = newRow[field] === null || newRow[field] === undefined || String(newRow[field]).trim() === '';
const hasValueInMain = mainRow[field] !== null && mainRow[field] !== undefined;
const isEmpty = newRow[templateCol] === null || newRow[templateCol] === undefined || String(newRow[templateCol]).trim() === '';
const hasValueInMain = mainRow[mainCol] !== null && mainRow[mainCol] !== undefined;
if (isEmpty && hasValueInMain) {
newRow[field] = mainRow[field];
newRow[templateCol] = mainRow[mainCol];
totalFilled++;
}
});