templates/js/app.js.twig line 1

Open in your IDE?
  1. $('[data-remove-image-button]').click(function () {
  2.     if (confirm("Удалить изображение?")) {
  3.         let removeBtn = $(this)[0];
  4.         let image = Dom.findParentChild(removeBtn, "[data-remove-image]");
  5.         let flag = Dom.findParentChild(removeBtn, "[data-remove-image-flag]");
  6.         let fileInputContainer = Dom.findParentChild(removeBtn, ".new-file-input");
  7.         let fileInput = $(fileInputContainer).find('input');
  8.         image.remove();
  9.         flag.value = "true";
  10.         let addNewFileBtn = Dom.findParentChild($(this)[0], ".upload-new-file-btn");
  11.         if (addNewFileBtn) {
  12.             $(addNewFileBtn).attr("hidden", true);
  13.         }
  14.         $(fileInputContainer).attr("hidden", false);
  15.         $(fileInputContainer).removeClass("mb-2");
  16.         removeBtn.parentElement.remove();
  17.     }
  18. });
  19. $(".upload-new-file-btn").click(function () {
  20.     let hide = !$(this).parent().find(".new-file-input").attr("hidden");
  21.     let fileInputContainer = $(this).parent().find(".new-file-input")[0];
  22.     let fileInput = $(this).parent().find(".new-file-input input")[0];
  23.     let isImage = $(fileInput).attr('accept').indexOf("image") > -1;
  24.     $(fileInputContainer).attr("hidden", hide);
  25.     $(this).parent().find(".file-path").attr("hidden", !hide);
  26.     $(this).text(hide ? (isImage ? "Загрузить новое изображение" : "Загрузить новый файл") : (isImage ? "Не загружать новое изображение" : "Не загружать новый файл"));
  27.     $(this).parent().find(".file-upload-text").html(hide ? "Файл загружен:" : "");
  28.     let removeBtn = Dom.findParentChild($(this)[0], ".remove-file-btn");
  29.     if (removeBtn) {
  30.         $(removeBtn).attr("hidden", !hide);
  31.     }
  32. });
  33. let elementLogicObjects = {};
  34. $('.dropdown').each(function() {
  35.     if ($(this).hasClass('file-dropdown')) {
  36.         let selectFileElement = Dom.findParent($(this)[0], ".select-file");
  37.         if (selectFileElement) {
  38.             let fileDropdown = new FileDropdown(selectFileElement);
  39.             elementLogicObjects[selectFileElement] = fileDropdown;
  40.         }
  41.     } else {
  42.         let dropdownElement = $(this)[0];
  43.         let dropdown = new Dropdown(dropdownElement);
  44.         elementLogicObjects[dropdownElement] = dropdown;
  45.     }
  46. });
  47. function getElementLogicObject(element) {
  48.     if (element in elementLogicObjects) {
  49.         return elementLogicObjects[element];
  50.     }
  51.     return null;
  52. }
  53. const MODAL_WINDOW_BUTTON = {
  54.     BUTTON_1: "button_1"
  55. }
  56. async function showWindowModal(title, text, buttonText1 = "Yes", buttonText2 = "No",
  57.                                modalDialogStyle = null) {
  58.     return new Promise((resolve) => {
  59.         // Кешируем селекторы модального окна, чтобы избежать повторных выборок
  60.         const $windowModal = $('#windowModal');
  61.         const $primaryBtn = $windowModal.find('.btn-primary');
  62.         const $secondaryBtn = $windowModal.find('.btn-secondary');
  63.         const $modalDialog = $windowModal.find('.modal-dialog');
  64.         $windowModal.find('.modal-title').text(title);
  65.         $windowModal.find('.modal-body').html(text);
  66.         $primaryBtn.text(buttonText1);
  67.         if (buttonText2 === null) {
  68.             $secondaryBtn.hide();
  69.         } else {
  70.             $secondaryBtn.text(buttonText2);
  71.             $secondaryBtn.show();
  72.         }
  73.         if (modalDialogStyle) {
  74.             $modalDialog.attr('style', modalDialogStyle);
  75.         } else {
  76.             $modalDialog.removeAttr('style');
  77.         }
  78.         $primaryBtn.off('click');
  79.         // Удаляем предыдущие handler'ы и добавляем новые. Используем локальный флаг
  80.         // чтобы гарантировать единственное резолвинг-промиса после завершения анимации скрытия (fade out).
  81.         $windowModal.off('hidden.bs.modal');
  82.         let buttonClicked = false;
  83.         // При клике на primary кнопку — только помечаем, что подтверждена кнопка и прячем модал.
  84.         // Не резолвим здесь; дождёмся события hidden.bs.modal (fade out complete).
  85.         $primaryBtn.on('click', function() {
  86.             buttonClicked = true;
  87.             $windowModal.modal('hide');
  88.         });
  89.         // Слушаем окончание скрытия модального окна (fade out complete).
  90.         $windowModal.on('hidden.bs.modal', function onHidden() {
  91.             // Снимаем обработчик сразу — он уже не нужен.
  92.             $windowModal.off('hidden.bs.modal', onHidden);
  93.             // Резолвим промис по флагу: BUTTON_1 если нажали кнопку, иначе null (отмена/закрытие).
  94.             if (buttonClicked) {
  95.                 resolve(MODAL_WINDOW_BUTTON.BUTTON_1);
  96.             } else {
  97.                 resolve(null);
  98.             }
  99.         });
  100.         $windowModal.modal('show');
  101.     });
  102. }
  103. const TOAST_TYPE = {
  104.     INFO: "INFO",
  105.     ERROR: "ERROR",
  106.     SUCCESS: "SUCCESS",
  107. }
  108. function showToast(text, type = TOAST_TYPE.INFO, closeButton = false, duration = 5000) {
  109.     let data = {
  110.         // node: text,
  111.         close: closeButton,
  112.         text: text,
  113.         duration: duration,
  114.         gravity: "top",
  115.         position: "right",
  116.         // onClick: function () { },
  117.         className: "",
  118.     };
  119.     switch (type) {
  120.         case TOAST_TYPE.ERROR:
  121.             data.style =  {
  122.                 background: "linear-gradient(to right, #ff5f6d, #e65c32ff)",
  123.             }
  124.             break;
  125.         case TOAST_TYPE.SUCCESS:
  126.             data.style =  {
  127.                 background: "linear-gradient(to right, #00b09b, #96c93d)",
  128.             }
  129.             break;
  130.     }
  131.     const toast = Toastify(data);
  132.     toast.showToast();
  133. }
  134. function showTableCompareModal() {
  135.     return new Promise(async (resolve, reject) => {
  136.             const modalContent = `
  137. <div class="form-group">
  138.     <label for="compare-list">Список ФИО (каждое с новой строки):</label>
  139.     <textarea id="compare-list" class="form-control" rows="8"></textarea>
  140. </div>
  141. `.trim();
  142.             let answer = await showWindowModal('Сверка таблицы по ФИО', modalContent, 'Сверить', 'Отмена');
  143.             if (answer === MODAL_WINDOW_BUTTON.BUTTON_1) {
  144.                 resolve({compareList: $('#compare-list').val()});
  145.             } else {
  146.                 resolve(null);
  147.             }
  148.     });
  149. }
  150. function getCompareTableHtml(tableData, compareResult, copyTableColsElementId = null,
  151.                              copyTableFiltersContainerElementSelector = null, dataTableOptions = null) {
  152.     if (!compareResult) {
  153.         return '<div>Нет данных для отображения</div>';
  154.     }
  155.     let compareTableId = 'compare-data-table-' + Date.now();
  156.     let filtersHtml = '';
  157.     let modifiedOptions = null;
  158.     let clonedFiltersContainer = null;
  159.     // Клонирование существующих фильтров (если передан селектор)
  160.     if (copyTableFiltersContainerElementSelector) {
  161.         const $origFilters = $(copyTableFiltersContainerElementSelector);
  162.         if ($origFilters && $origFilters.length) {
  163.             let $clone = $origFilters.clone(true, true);
  164.             const origContainerId = $clone.attr('id');
  165.             if (origContainerId) {
  166.                 $clone.attr('id', 'compare_' + origContainerId);
  167.             }
  168.             $clone.find('[id]').each(function () {
  169.                 const oldId = $(this).attr('id');
  170.                 if (oldId && oldId.indexOf('compare_') !== 0) {
  171.                     $(this).attr('id', 'compare_' + oldId);
  172.                 }
  173.             });
  174.             clonedFiltersContainer = $clone; // сохраним для дальнейшего добавления нового фильтра
  175.             if (dataTableOptions) {
  176.                 try {
  177.                     modifiedOptions = JSON.parse(JSON.stringify(dataTableOptions));
  178.                     if (modifiedOptions.dropdownFiltersConfig && Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  179.                         modifiedOptions.dropdownFiltersConfig.elementsData.forEach(ed => {
  180.                             if (ed.dropdownId) ed.dropdownId = 'compare_' + ed.dropdownId;
  181.                             if (ed.badgeId) ed.badgeId = 'compare_' + ed.badgeId;
  182.                         });
  183.                     }
  184.                 } catch (e) {
  185.                     console.error('Failed to clone dataTableOptions for compare modal', e);
  186.                 }
  187.             }
  188.         }
  189.     }
  190.     // normalize структуры
  191.     const normalize = (res) => {
  192.         if (!res) return { findInOriginal: { foundStrings: [], notFoundStrings: [] }, notFoundOriginal: [], info: {} };
  193.         // already normalized
  194.         if (res.findInOriginal && (res.findInOriginal.foundStrings || res.findInOriginal.notFoundStrings)) {
  195.             return res;
  196.         }
  197.         // old backend format: findOriginalLinesResult + notFoundCompareLines
  198.         if (res.findOriginalLinesResult) {
  199.             const foundStrings = [];
  200.             const notFoundStrings = [];
  201.             const notFoundOriginal = [];
  202.             (res.findOriginalLinesResult || []).forEach(item => {
  203.                 const originalLine = item.originalLine || '';
  204.                 const foundCompareLine = item.foundCompareLine || null;
  205.                 const similarityPercent = (item.similarityPercent !== undefined) ? item.similarityPercent : 0;
  206.                 if (foundCompareLine) {
  207.                     foundStrings.push({ compareLine: foundCompareLine, foundOriginalString: originalLine, similarityPercent: similarityPercent });
  208.                 } else {
  209.                     notFoundOriginal.push(originalLine);
  210.                 }
  211.             });
  212.             (res.notFoundCompareLines || []).forEach(cl => {
  213.                 notFoundStrings.push({ compareLine: cl, similarityPercent: 0 });
  214.             });
  215.             const info = res.info || {};
  216.             return {
  217.                 findInOriginal: { foundStrings, notFoundStrings },
  218.                 notFoundOriginal,
  219.                 info: {
  220.                     originalLinesCount: info.originalLinesCount || 0,
  221.                     compareLinesCount: info.compareLinesCount || 0,
  222.                     foundStringsCount: info.foundCompareLinesCount || foundStrings.length,
  223.                     notFoundStringsCount: info.notFoundCompareLinesCount || notFoundStrings.length,
  224.                     notFoundOriginalCount: (info.notFoundOriginalCount !== undefined) ? info.notFoundOriginalCount : notFoundOriginal.length,
  225.                     foundCompareLinesCount: info.foundCompareLinesCount || 0,
  226.                     foundCompareLinesPartiallyCount: info.foundCompareLinesPartiallyCount || 0,
  227.                     // new counts introduced by updated StringUtils/compareStringArrays
  228.                     foundCompareLinesGoodSimilarityCount: info.foundCompareLinesGoodSimilarityCount || 0,
  229.                     foundCompareLinesBadSimilarityCount: info.foundCompareLinesBadSimilarityCount || 0,
  230.                     minSimilarityPercentage: info.minSimilarityPercentage || 0
  231.                  }
  232.              };
  233.          }
  234.         // fallback: best effort
  235.         const fio = res.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  236.         return {
  237.             findInOriginal: { foundStrings: fio.foundStrings || [], notFoundStrings: fio.notFoundStrings || [] },
  238.             notFoundOriginal: res.notFoundOriginal || [],
  239.             info: Object.assign({
  240.                 originalLinesCount: 0,
  241.                 compareLinesCount: 0,
  242.                 foundStringsCount: (fio.foundStrings || []).length,
  243.                 notFoundStringsCount: (fio.notFoundStrings || []).length,
  244.                 notFoundOriginalCount: (res.notFoundOriginal || []).length
  245.             }, res.info || {})
  246.         };
  247.     };
  248.     const normalized = normalize(compareResult);
  249.     const escapeHtml = (unsafe) => {
  250.         if (unsafe === null || unsafe === undefined) return '';
  251.         return String(unsafe)
  252.             .replace(/&/g, '&amp;')
  253.             .replace(/</g, '&lt;')
  254.             .replace(/>/g, '&gt;')
  255.             .replace(/"/g, '&quot;')
  256.             .replace(/'/g, '&#039;');
  257.     };
  258.     const findInOriginal = normalized.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  259.     const found = findInOriginal.foundStrings || [];
  260.     const notFound = findInOriginal.notFoundStrings || [];
  261.     const notFoundOriginal = normalized.notFoundOriginal || [];
  262.     const info = normalized.info || {};
  263.     const minSimilarity = info.minSimilarityPercentage;
  264.     let html = '';
  265.     // Подготовка исходных колонок
  266.     let srcCols = [];
  267.     let srcRows = [];
  268.     let srcLookup = null;
  269.     const tableDataProvided = Array.isArray(tableData);
  270.     if (tableDataProvided) {
  271.         srcRows = tableData.map(row => {
  272.             if (!Array.isArray(row)) return [];
  273.             return row.map(cell => (cell === null || cell === undefined) ? '' : String(cell).trim());
  274.         });
  275.         // build lookup by column index 1 (as template uses tableData[i][1])
  276.         srcLookup = new Map();
  277.         for (let r = 0; r < srcRows.length; r++) {
  278.             const row = srcRows[r];
  279.             // prefer column index 1; if missing, index 0 or any non-empty
  280.             let key = '';
  281.             if (row.length > 1 && row[1]) {
  282.                 key = row[1];
  283.             } else if (row.length > 0 && row[0]) {
  284.                 key = row[0];
  285.             } else {
  286.                 // find first non-empty
  287.                 for (let c = 0; c < row.length; c++) {
  288.                     if (row[c]) { key = row[c]; break; }
  289.                 }
  290.             }
  291.             if (key) {
  292.                 // if duplicate keys, keep first
  293.                 if (!srcLookup.has(key)) srcLookup.set(key, row);
  294.             }
  295.         }
  296.     }
  297.     if (copyTableColsElementId) {
  298.         const $src = $('#' + copyTableColsElementId);
  299.         if ($src && $src.length) {
  300.             // always read headers into srcCols (we may still use tableData for rows)
  301.             $src.find('thead th').each(function () {
  302.                 srcCols.push(String($(this).text().trim()));
  303.             });
  304.             // read tbody rows from DOM only if tableData was NOT provided
  305.             if (!tableDataProvided) {
  306.                 $src.find('tbody tr').each(function () {
  307.                     const rowArr = [];
  308.                     $(this).find('td').each(function () {
  309.                         rowArr.push(String($(this).text().trim()));
  310.                     });
  311.                     srcRows.push(rowArr);
  312.                 });
  313.             }
  314.          }
  315.      }
  316.     // If no DOM source and no tableData provided, leave srcCols/srcRows empty (matching previous behaviour)
  317.     const compareCols = ['Номер п/п', 'Статус', 'ФИО сверки', 'ФИО таблицы', 'Процент совпадения, %'];
  318.     const finalCols = [];
  319.     srcCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  320.     compareCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  321.     // --- Новый фильтр "Статус сверки" ---
  322.     const statusColIndex = finalCols.indexOf('Статус');
  323.     const statusFilterDropdownId = 'compareStatusDropdown_' + compareTableId;
  324.     // HTML нового фильтра
  325.     const statusFilterHtml = `
  326. <div class="dropdown-filter">
  327.   <label class="form-label">Статус сверки ФИО:</label>
  328.   <div class="dropdown">
  329.     <button class="btn btn-primary dropdown-toggle" type="button" id="${statusFilterDropdownId}" data-toggle="dropdown" aria-expanded="false" data-multiple="false">Все статусы</button>
  330.     <div class="dropdown-menu" aria-labelledby="${statusFilterDropdownId}">
  331.       <a class="dropdown-item" href="#" data-filter="all">Все статусы</a>
  332.       <div class="dropdown-divider"></div>
  333.       <!-- Dynamic items will be added here -->
  334.     </div>
  335.   </div>
  336. </div>`;
  337.     if (clonedFiltersContainer) {
  338.         // Добавляем новый фильтр в конец контейнера
  339.         clonedFiltersContainer.append($(statusFilterHtml));
  340.         filtersHtml = $('<div>').append(clonedFiltersContainer).html();
  341.     } else {
  342.         // Создаем новый контейнер только с фильтром статуса
  343.         filtersHtml = `<div id="compare-filters-${compareTableId}" class="mb-3 d-flex align-items-center" style="gap:20px; flex-wrap:wrap;">${statusFilterHtml}</div>`;
  344.     }
  345.     // Обновляем / создаём конфиг options для CustomDataTable
  346.     if (!modifiedOptions) {
  347.         modifiedOptions = {
  348.             dropdownFiltersConfig: { elementsData: [] },
  349.             drawRowNumbersForColIndex: 0,
  350.             lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'Все']]
  351.         };
  352.     }
  353.     if (!modifiedOptions.dropdownFiltersConfig) {
  354.         modifiedOptions.dropdownFiltersConfig = { elementsData: [] };
  355.     }
  356.     if (!Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  357.         modifiedOptions.dropdownFiltersConfig.elementsData = [];
  358.     }
  359.     if (statusColIndex !== -1) {
  360.         modifiedOptions.dropdownFiltersConfig.elementsData.push({
  361.             columnIndex: statusColIndex,
  362.             dropdownId: statusFilterDropdownId,
  363.             badgeId: null,
  364.             textForAll: 'Все статусы'
  365.         });
  366.     }
  367.     // Вставляем фильтры перед сводкой
  368.     if (filtersHtml) {
  369.         html += '<div class="mb-3">' + filtersHtml + '</div>';
  370.     }
  371.     // Сводка
  372.     html += '<div class="mb-3">';
  373.     html += '<strong>Всего в таблице:</strong> ' + escapeHtml(info.originalLinesCount || 0) + '<br/>';
  374.     html += '<strong>Всего для сверки:</strong> ' + escapeHtml(info.compareLinesCount || 0) + '<br/>';
  375.     html += '<strong>Найдено совпадений:</strong> ' + escapeHtml(info.foundStringsCount || found.length) + '<br/>';
  376.     html += '<strong>Частично найдено (более ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesGoodSimilarityCount || 0) + '<br/>';
  377.     html += '<strong>Частично найдено (до ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesBadSimilarityCount || 0) + '<br/>';
  378.     html += '<strong>Частично найдено всего:</strong> ' + ((info.foundCompareLinesBadSimilarityCount + info.foundCompareLinesGoodSimilarityCount) || 0) + '<br/>';
  379.     html += '<strong>Не найдено в таблице:</strong> ' + notFound.length + '<br/>';
  380.     html += '<strong>Не найдено в списке сверки:</strong> ' + (info.notFoundOriginalCount || notFoundOriginal.length) + '<br/>';
  381.     html += '</div>';
  382.     html += '<h6>Результаты сверки</h6>';
  383.     // Кнопка скачивания CSV
  384.     const downloadBtnId = 'download-csv-btn-' + compareTableId;
  385.     html += '<div class="mb-2 d-flex justify-content-end">';
  386.     html += '<button id="' + downloadBtnId + '" type="button" class="btn btn-secondary btn-sm">Скачать таблицу</button>';
  387.     html += '</div>';
  388.     html += '<div class="table-responsive"><table id="' + compareTableId + '" class="table table-sm table-bordered">';
  389.     html += '<thead><tr>';
  390.     finalCols.forEach(col => { html += '<th>' + col + '</th>'; });
  391.     html += '</tr></thead><tbody>';
  392.     let rowIndex = 0;
  393.     const findSrcRowByValue = (value) => {
  394.         if (!value) return null;
  395.         const valNorm = String(value).trim();
  396.         // if we have a lookup built from tableData, use it and return only rows from tableData
  397.         if (srcLookup) {
  398.             if (srcLookup.has(valNorm)) return srcLookup.get(valNorm);
  399.             return null; // do NOT search DOM or other columns when tableData provided
  400.         }
  401.         for (let r = 0; r < srcRows.length; r++) {
  402.             for (let c = 0; c < srcRows[r].length; c++) {
  403.                 if (srcRows[r][c] === valNorm) return srcRows[r];
  404.             }
  405.         }
  406.         return null;
  407.     };
  408.     const renderRow = (cells, extraClass) => {
  409.         html += '<tr' + (extraClass ? ' class="' + extraClass + '"' : '') + '>';
  410.         finalCols.forEach(fc => {
  411.             const val = (fc in cells) ? cells[fc] : '';
  412.             html += '<td>' + (val === null || val === undefined ? '' : val) + '</td>';
  413.         });
  414.         html += '</tr>';
  415.     };
  416.     const mapSrcRowToObj = (srcRowArr) => {
  417.         const obj = {};
  418.         srcCols.forEach((h, idx) => { obj[h] = srcRowArr[idx] || ''; });
  419.         return obj;
  420.     };
  421.     if (tableDataProvided && srcRows.length) {
  422.         // build map from original value -> found item
  423.         const foundByOriginal = new Map();
  424.         found.forEach(it => {
  425.             const key = (it.foundOriginalString || '').trim();
  426.             if (key) foundByOriginal.set(key, it);
  427.         });
  428.         // iterate original rows in order
  429.         for (let r = 0; r < srcRows.length; r++) {
  430.             const srcRow = srcRows[r];
  431.             // derive key from same logic as srcLookup
  432.             let key = '';
  433.             if (srcRow.length > 1 && srcRow[1]) key = srcRow[1];
  434.             else if (srcRow.length > 0 && srcRow[0]) key = srcRow[0];
  435.             else {
  436.                 for (let c = 0; c < srcRow.length; c++) { if (srcRow[c]) { key = srcRow[c]; break; } }
  437.             }
  438.             rowIndex++;
  439.             const cells = mapSrcRowToObj(srcRow);
  440.             const foundItem = key ? foundByOriginal.get(key) : null;
  441.             if (foundItem) {
  442.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  443.                 const pct = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? Number(foundItem.similarityPercent) : 0;
  444.                 let statusText = '';
  445.                 if (pct === 100) {
  446.                     statusText = 'Найдено';
  447.                 } else if (pct > minSimilarity) {
  448.                     statusText = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  449.                 } else {
  450.                     statusText = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  451.                 }
  452.                 cells['Статус'] = statusText;
  453.                 cells['ФИО сверки'] = escapeHtml(foundItem.compareLine);
  454.                 cells['ФИО таблицы'] = escapeHtml(foundItem.foundOriginalString || key);
  455.                 cells['Процент совпадения, %'] = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? escapeHtml(foundItem.similarityPercent) : '';
  456.                 const rowClass = (pct === 100) ? 'table-success' : 'table-warning';
  457.                 renderRow(cells, rowClass);
  458.                 foundByOriginal.delete(key);
  459.             } else {
  460.                 // red row - not found in compare
  461.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  462.                 cells['Статус'] = 'Не найдено в списке сверки';
  463.                 cells['ФИО сверки'] = '';
  464.                 cells['ФИО таблицы'] = escapeHtml(key);
  465.                 cells['Процент совпадения, %'] = '';
  466.                 renderRow(cells, 'table-danger');
  467.             }
  468.         }
  469.         // render remaining compare lines that weren't matched to any original
  470.         // these are in notFound OR leftover in foundByOriginal if any
  471.         // first render compare items from notFound (explicitly unmatched)
  472.         notFound.forEach(item => {
  473.             rowIndex++;
  474.             let cells = {};
  475.             cells['Номер п/п'] = escapeHtml(rowIndex);
  476.             cells['Статус'] = 'Не найдено в таблице';
  477.             cells['ФИО сверки'] = escapeHtml(item.compareLine);
  478.             cells['ФИО таблицы'] = '';
  479.             cells['Процент совпадения, %'] = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  480.             // use blue for notFound compare lines
  481.             renderRow(cells, 'table-info');
  482.         });
  483.         // any leftover found items that weren't matched by key (edge cases)
  484.         if (foundByOriginal.size) {
  485.             foundByOriginal.forEach(it => {
  486.                 rowIndex++;
  487.                 let cells = {};
  488.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  489.                 const pct2 = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? Number(it.similarityPercent) : 0;
  490.                 let statusText2 = '';
  491.                 if (pct2 === 100) {
  492.                     statusText2 = 'Найдено';
  493.                 } else if (pct2 > minSimilarity) {
  494.                     statusText2 = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  495.                 } else {
  496.                     statusText2 = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  497.                 }
  498.                 cells['Статус'] = statusText2;
  499.                 cells['ФИО сверки'] = escapeHtml(it.compareLine);
  500.                 cells['ФИО таблицы'] = escapeHtml(it.foundOriginalString || '');
  501.                 cells['Процент совпадения, %'] = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? escapeHtml(it.similarityPercent) : '';
  502.                 const rowClass2 = (pct2 === 100) ? 'table-success' : 'table-warning';
  503.                 renderRow(cells, rowClass2);
  504.             });
  505.         }
  506.     } else {
  507.         // fallback original behaviour: show found, then notFound, then notFoundOriginal
  508.         found.forEach(item => {
  509.             rowIndex++;
  510.             const compareString = escapeHtml(item.compareLine);
  511.             const foundOriginal = escapeHtml(item.foundOriginalString);
  512.             const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  513.             let cells = {};
  514.             if (srcCols.length && foundOriginal) {
  515.                 const srcRow = findSrcRowByValue(foundOriginal);
  516.                 if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  517.             }
  518.             const pctItem = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? Number(item.similarityPercent) : 0;
  519.             let statusTextItem = '';
  520.             if (pctItem === 100) {
  521.                 statusTextItem = 'Найдено';
  522.             } else if (pctItem > minSimilarity) {
  523.                 statusTextItem = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  524.             } else {
  525.                 statusTextItem = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  526.             }
  527.             cells['Номер п/п'] = escapeHtml(rowIndex);
  528.             cells['Статус'] = statusTextItem;
  529.             cells['ФИО сверки'] = compareString;
  530.             cells['ФИО таблицы'] = foundOriginal;
  531.             cells['Процент совпадения, %'] = percent;
  532.             const rowClassItem = (pctItem === 100) ? 'table-success' : 'table-warning';
  533.             renderRow(cells, rowClassItem);
  534.         });
  535.          notFound.forEach(item => {
  536.              rowIndex++;
  537.              const compareString = escapeHtml(item.compareLine);
  538.              const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  539.              let cells = {};
  540.              cells['Номер п/п'] = escapeHtml(rowIndex);
  541.              cells['Статус'] = 'Не найдено в таблице';
  542.              cells['ФИО сверки'] = compareString;
  543.              cells['ФИО таблицы'] = '';
  544.              cells['Процент совпадения, %'] = percent;
  545.              renderRow(cells, 'table-danger');
  546.          });
  547.          notFoundOriginal.forEach(item => {
  548.              rowIndex++;
  549.              const original = escapeHtml(item);
  550.              let cells = {};
  551.              if (srcCols.length) {
  552.                  const srcRow = findSrcRowByValue(item);
  553.                  if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  554.              }
  555.              cells['Номер п/п'] = escapeHtml(rowIndex);
  556.              cells['Статус'] = 'Не найдено в списке сверки';
  557.              cells['ФИО сверки'] = '';
  558.              cells['ФИО таблицы'] = original;
  559.              cells['Процент совпадения, %'] = '';
  560.              renderRow(cells, 'table-secondary');
  561.          });
  562.      }
  563.     // --- New: ensure initialization script with modifiedOptions ---
  564.     if (modifiedOptions) {
  565.         if (modifiedOptions.drawRowNumbersForColIndex === undefined) {
  566.             modifiedOptions.drawRowNumbersForColIndex = 0;
  567.         }
  568.         html += '</tbody></table></div>';
  569.         html += '<script>(function(){try{var opts=' + JSON.stringify(modifiedOptions) + ';var table=new CustomDataTable("#' + compareTableId + '",opts);table.initDataTable();var btn=document.getElementById("' + downloadBtnId + '");if(btn){btn.addEventListener("click",function(){table.downloadCsv("compare_table_export.csv");});}}catch(e){console.error("Failed to initialize compare CustomDataTable",e);}})();</'+'script>';
  570.     } else {
  571.         html += '</tbody></table></div>';
  572.     }
  573.     return html;
  574.  }
  575. function compareData(data1, data2) {
  576.     return new Promise((resolve, reject) => {
  577.         Utils.fetch(API_COMPARE_DATA_URL, 'POST', { data1: data1, data2: data2 })
  578.             .then(async (response) => {
  579.                 console.log(response);
  580.                 resolve(response.response);
  581.             })
  582.             .catch((response) => {
  583.                 showToast("Ошибка. Не удалось провести сравнение. Попробуйте повторить позднее.",
  584.                     TOAST_TYPE.ERROR);
  585.                 console.log(response);
  586.                 reject(response);
  587.             });
  588.     });
  589. }
  590. function updateDependentSelect(controllerSelector, $dependent) {
  591.     let selectedControllerVal = $(controllerSelector).val();
  592.     let optionAttrName = $dependent.data('controlling-select-option-attr-name');
  593.     $dependent.find('option').each(function() {
  594.         let $opt = $(this);
  595.         let optAttrVal = $opt.attr(optionAttrName);
  596.         if (optAttrVal == selectedControllerVal || $opt.val() == -1) {
  597.             $opt.show();
  598.         } else {
  599.             $opt.hide();
  600.         }
  601.     });
  602.     if ($dependent.find('option:selected').attr(optionAttrName) != selectedControllerVal) {
  603.         $dependent.val(-1);
  604.     }
  605. }
  606. $('[data-controlling-select-selector]').each(function() {
  607.     let $dependent = $(this);
  608.     let controllerSelector = $dependent.data('controlling-select-selector');
  609.     if (!controllerSelector) return;
  610.     $(controllerSelector).on('change', function() {
  611.         updateDependentSelect(controllerSelector, $dependent);
  612.     });
  613.     if ($(controllerSelector).length) {
  614.         updateDependentSelect(controllerSelector, $dependent);
  615.     }
  616. });