templates/js/app.js.twig line 1

Open in your IDE?
  1. let domDispatcher = new DomDispatcher();
  2. domDispatcher.init();
  3. initBearer.set(INIT_BEARERS.GLOBAL_DOM_DISPATCHER, domDispatcher);
  4. $('[data-remove-image-button]').click(function () {
  5.     if (confirm("Удалить файл?")) {
  6.         let removeBtn = $(this)[0];
  7.         let image = Dom.findParentChild(removeBtn, "[data-remove-image]");
  8.         let flag = Dom.findParentChild(removeBtn, "[data-remove-image-flag]");
  9.         let fileInputContainer = Dom.findParentChild(removeBtn, ".new-file-input");
  10.         let fileInput = $(fileInputContainer).find('input');
  11.         image.remove();
  12.         flag.value = "true";
  13.         let addNewFileBtn = Dom.findParentChild($(this)[0], ".upload-new-file-btn");
  14.         if (addNewFileBtn) {
  15.             $(addNewFileBtn).attr("hidden", true);
  16.         }
  17.         $(fileInputContainer).attr("hidden", false);
  18.         $(fileInputContainer).removeClass("mb-2");
  19.         removeBtn.parentElement.remove();
  20.     }
  21. });
  22. $(".upload-new-file-btn").click(function () {
  23.     let hide = !$(this).parent().find(".new-file-input").attr("hidden");
  24.     let fileInputContainer = $(this).parent().find(".new-file-input")[0];
  25.     let fileInput = $(this).parent().find(".new-file-input input")[0];
  26.     let isImage = $(fileInput).attr('accept').indexOf("image") > -1;
  27.     $(fileInputContainer).attr("hidden", hide);
  28.     $(this).parent().find(".file-path").attr("hidden", !hide);
  29.     $(this).text(hide ? (isImage ? "Загрузить новое изображение" : "Загрузить новый файл") : (isImage ? "Не загружать новое изображение" : "Не загружать новый файл"));
  30.     $(this).parent().find(".file-upload-text").html(hide ? "Файл загружен:" : "");
  31.     let removeBtn = Dom.findParentChild($(this)[0], ".remove-file-btn");
  32.     if (removeBtn) {
  33.         $(removeBtn).attr("hidden", !hide);
  34.     }
  35. });
  36. let elementLogicObjects = {};
  37. $('.dropdown').each(function() {
  38.     if ($(this).hasClass('file-dropdown')) {
  39.         let selectFileElement = Dom.findParent($(this)[0], ".select-file");
  40.         if (selectFileElement) {
  41.             let fileDropdown = new FileDropdown(selectFileElement);
  42.             elementLogicObjects[selectFileElement] = fileDropdown;
  43.         }
  44.     } else {
  45.         let dropdownElement = $(this)[0];
  46.         let dropdown = new Dropdown(dropdownElement);
  47.         elementLogicObjects[dropdownElement] = dropdown;
  48.     }
  49. });
  50. $('.dropdown-with-input-filter').each(function() {
  51.     let dropdownWithInputFilterElement = $(this)[0];
  52.     // Найдём вложенный .dropdown внутри контейнера
  53.     let dropdownElement = $(this).find('.dropdown')[0];
  54.     if (!dropdownElement) return;
  55.     let dropdownWithInputFilter = new DropdownWithInputFilter(dropdownElement);
  56.     elementLogicObjects[dropdownWithInputFilterElement] = dropdownWithInputFilter;
  57. });
  58. // Инициализация MonthRangeSliderDropdown
  59. {#$('.month-range-slider-dropdown').each(function() {#}
  60. {#    let container = $(this)[0];#}
  61. {#    let dropdownElement = $(this).find('.dropdown')[0];#}
  62. {#    if (!dropdownElement) return;#}
  63. {#    if (!container._monthRangeSliderDropdownInitialized) {#}
  64. {#        let inst = new MonthRangeSliderDropdown(dropdownElement);#}
  65. {#        elementLogicObjects[container] = inst;#}
  66. {#        container._monthRangeSliderDropdownInitialized = true;#}
  67. {#    }#}
  68. {#});#}
  69. function getElementLogicObject(element) {
  70.     if (element in elementLogicObjects) {
  71.         return elementLogicObjects[element];
  72.     }
  73.     return null;
  74. }
  75. const MODAL_WINDOW_BUTTON = {
  76.     BUTTON_1: "button_1"
  77. }
  78. async function showWindowModal(title, text, buttonText1 = "Yes", buttonText2 = "No",
  79.                                modalDialogStyle = null, onWindowBuild = null) {
  80.     return new Promise((resolve) => {
  81.         // Кешируем селекторы модального окна, чтобы избежать повторных выборок
  82.         const $windowModal = $('#windowModal');
  83.         const $primaryBtn = $windowModal.find('.btn-primary');
  84.         const $secondaryBtn = $windowModal.find('.btn-secondary');
  85.         const $modalDialog = $windowModal.find('.modal-dialog');
  86.         $windowModal.find('.modal-title').text(title);
  87.         $windowModal.find('.modal-body').html(text);
  88.         $primaryBtn.text(buttonText1);
  89.         if (buttonText2 === null) {
  90.             $secondaryBtn.hide();
  91.         } else {
  92.             $secondaryBtn.text(buttonText2);
  93.             $secondaryBtn.show();
  94.         }
  95.         if (modalDialogStyle) {
  96.             $modalDialog.attr('style', modalDialogStyle);
  97.         } else {
  98.             $modalDialog.removeAttr('style');
  99.         }
  100.         $primaryBtn.off('click');
  101.         // Удаляем предыдущие handler'ы и добавляем новые. Используем локальный флаг
  102.         // чтобы гарантировать единственное резолвинг-промиса после завершения анимации скрытия (fade out).
  103.         $windowModal.off('hidden.bs.modal');
  104.         let buttonClicked = false;
  105.         // При клике на primary кнопку — только помечаем, что подтверждена кнопка и прячем модал.
  106.         // Не резолвим здесь; дождёмся события hidden.bs.modal (fade out complete).
  107.         $primaryBtn.on('click', function() {
  108.             buttonClicked = true;
  109.             $windowModal.modal('hide');
  110.         });
  111.         // Слушаем окончание скрытия модального окна (fade out complete).
  112.         $windowModal.on('hidden.bs.modal', function onHidden() {
  113.             // Снимаем обработчик сразу — он уже не нужен.
  114.             $windowModal.off('hidden.bs.modal', onHidden);
  115.             // Резолвим промис по флагу: BUTTON_1 если нажали кнопку, иначе null (отмена/закрытие).
  116.             if (buttonClicked) {
  117.                 resolve(MODAL_WINDOW_BUTTON.BUTTON_1);
  118.             } else {
  119.                 resolve(null);
  120.             }
  121.         });
  122.         if (onWindowBuild && typeof onWindowBuild === 'function') {
  123.             onWindowBuild($windowModal);
  124.         }
  125.         $windowModal.modal('show');
  126.     });
  127. }
  128. const TOAST_TYPE = {
  129.     INFO: "INFO",
  130.     ERROR: "ERROR",
  131.     SUCCESS: "SUCCESS",
  132. }
  133. function showToast(text, type = TOAST_TYPE.INFO, closeButton = false, duration = 5000) {
  134.     const node = document.createElement('div');
  135.     node.innerHTML = text;
  136.     let data = {
  137.         node: node,
  138.         close: closeButton,
  139.         {#text: text,#}
  140.         duration: duration,
  141.         gravity: "top",
  142.         position: "right",
  143.         // onClick: function () { },
  144.         className: "",
  145.     };
  146.     switch (type) {
  147.         case TOAST_TYPE.ERROR:
  148.             data.style =  {
  149.                 background: "linear-gradient(to right, #ff5f6d, #e65c32ff)",
  150.             }
  151.             break;
  152.         case TOAST_TYPE.SUCCESS:
  153.             data.style =  {
  154.                 background: "linear-gradient(to right, #00b09b, #96c93d)",
  155.             }
  156.             break;
  157.     }
  158.     const toast = Toastify(data);
  159.     toast.showToast();
  160.     return toast;
  161. }
  162. function showTableCompareModal() {
  163.     return new Promise(async (resolve, reject) => {
  164.             const modalContent = `
  165. <div class="form-group">
  166.     <label for="compare-list">Список ФИО (каждое с новой строки):</label>
  167.     <textarea id="compare-list" class="form-control" rows="8"></textarea>
  168. </div>
  169. `.trim();
  170.             let answer = await showWindowModal('Поиск по списку ФИО', modalContent, 'Найти', 'Отмена');
  171.             if (answer === MODAL_WINDOW_BUTTON.BUTTON_1) {
  172.                 resolve({compareList: $('#compare-list').val()});
  173.             } else {
  174.                 resolve(null);
  175.             }
  176.     });
  177. }
  178. function getCompareTableHtml(tableData, compareResult, copyTableColsElementId = null,
  179.                              copyTableFiltersContainerElementSelector = null, dataTableOptions = null) {
  180.     if (!compareResult) {
  181.         return '<div>Нет данных для отображения</div>';
  182.     }
  183.     let compareTableId = 'compare-data-table-' + Date.now();
  184.     let filtersHtml = '';
  185.     let modifiedOptions = null;
  186.     let clonedFiltersContainer = null;
  187.     // Клонирование существующих фильтров (если передан селектор)
  188.     if (copyTableFiltersContainerElementSelector) {
  189.         const $origFilters = $(copyTableFiltersContainerElementSelector);
  190.         if ($origFilters && $origFilters.length) {
  191.             let $clone = $origFilters.clone(true, true);
  192.             const origContainerId = $clone.attr('id');
  193.             if (origContainerId) {
  194.                 $clone.attr('id', 'compare_' + origContainerId);
  195.             }
  196.             $clone.find('[id]').each(function () {
  197.                 const oldId = $(this).attr('id');
  198.                 if (oldId && oldId.indexOf('compare_') !== 0) {
  199.                     $(this).attr('id', 'compare_' + oldId);
  200.                 }
  201.             });
  202.             clonedFiltersContainer = $clone; // сохраним для дальнейшего добавления нового фильтра
  203.             if (dataTableOptions) {
  204.                 try {
  205.                     modifiedOptions = JSON.parse(JSON.stringify(dataTableOptions));
  206.                     if (modifiedOptions.dropdownFiltersConfig && Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  207.                         modifiedOptions.dropdownFiltersConfig.elementsData.forEach(ed => {
  208.                             if (ed.dropdownId) ed.dropdownId = 'compare_' + ed.dropdownId;
  209.                             if (ed.badgeId) ed.badgeId = 'compare_' + ed.badgeId;
  210.                         });
  211.                     }
  212.                 } catch (e) {
  213.                     console.error('Failed to clone dataTableOptions for compare modal', e);
  214.                 }
  215.             }
  216.         }
  217.     }
  218.     // normalize структуры
  219.     const normalize = (res) => {
  220.         if (!res) return { findInOriginal: { foundStrings: [], notFoundStrings: [] }, notFoundOriginal: [], info: {} };
  221.         // already normalized
  222.         if (res.findInOriginal && (res.findInOriginal.foundStrings || res.findInOriginal.notFoundStrings)) {
  223.             return res;
  224.         }
  225.         // old backend format: findOriginalLinesResult + notFoundCompareLines
  226.         if (res.findOriginalLinesResult) {
  227.             const foundStrings = [];
  228.             const notFoundStrings = [];
  229.             const notFoundOriginal = [];
  230.             (res.findOriginalLinesResult || []).forEach(item => {
  231.                 const originalLine = item.originalLine || '';
  232.                 const foundCompareLine = item.foundCompareLine || null;
  233.                 const similarityPercent = (item.similarityPercent !== undefined) ? item.similarityPercent : 0;
  234.                 if (foundCompareLine) {
  235.                     foundStrings.push({ compareLine: foundCompareLine, foundOriginalString: originalLine, similarityPercent: similarityPercent });
  236.                 } else {
  237.                     notFoundOriginal.push(originalLine);
  238.                 }
  239.             });
  240.             (res.notFoundCompareLines || []).forEach(cl => {
  241.                 notFoundStrings.push({ compareLine: cl, similarityPercent: 0 });
  242.             });
  243.             const info = res.info || {};
  244.             return {
  245.                 findInOriginal: { foundStrings, notFoundStrings },
  246.                 notFoundOriginal,
  247.                 info: {
  248.                     originalLinesCount: info.originalLinesCount || 0,
  249.                     compareLinesCount: info.compareLinesCount || 0,
  250.                     foundStringsCount: info.foundCompareLinesCount || foundStrings.length,
  251.                     notFoundStringsCount: info.notFoundCompareLinesCount || notFoundStrings.length,
  252.                     notFoundOriginalCount: (info.notFoundOriginalCount !== undefined) ? info.notFoundOriginalCount : notFoundOriginal.length,
  253.                     foundCompareLinesCount: info.foundCompareLinesCount || 0,
  254.                     foundCompareLinesPartiallyCount: info.foundCompareLinesPartiallyCount || 0,
  255.                     // new counts introduced by updated StringUtils/compareStringArrays
  256.                     foundCompareLinesGoodSimilarityCount: info.foundCompareLinesGoodSimilarityCount || 0,
  257.                     foundCompareLinesBadSimilarityCount: info.foundCompareLinesBadSimilarityCount || 0,
  258.                     minSimilarityPercentage: info.minSimilarityPercentage || 0
  259.                  }
  260.              };
  261.          }
  262.         // fallback: best effort
  263.         const fio = res.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  264.         return {
  265.             findInOriginal: { foundStrings: fio.foundStrings || [], notFoundStrings: fio.notFoundStrings || [] },
  266.             notFoundOriginal: res.notFoundOriginal || [],
  267.             info: Object.assign({
  268.                 originalLinesCount: 0,
  269.                 compareLinesCount: 0,
  270.                 foundStringsCount: (fio.foundStrings || []).length,
  271.                 notFoundStringsCount: (fio.notFoundStrings || []).length,
  272.                 notFoundOriginalCount: (res.notFoundOriginal || []).length
  273.             }, res.info || {})
  274.         };
  275.     };
  276.     const normalized = normalize(compareResult);
  277.     const escapeHtml = (unsafe) => {
  278.         if (unsafe === null || unsafe === undefined) return '';
  279.         return String(unsafe)
  280.             .replace(/&/g, '&amp;')
  281.             .replace(/</g, '&lt;')
  282.             .replace(/>/g, '&gt;')
  283.             .replace(/"/g, '&quot;')
  284.             .replace(/'/g, '&#039;');
  285.     };
  286.     const findInOriginal = normalized.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  287.     const found = findInOriginal.foundStrings || [];
  288.     const notFound = findInOriginal.notFoundStrings || [];
  289.     const notFoundOriginal = normalized.notFoundOriginal || [];
  290.     const info = normalized.info || {};
  291.     const minSimilarity = info.minSimilarityPercentage;
  292.     let html = '';
  293.     // Подготовка исходных колонок
  294.     let srcCols = [];
  295.     let srcRows = [];
  296.     let srcLookup = null;
  297.     const tableDataProvided = Array.isArray(tableData);
  298.     if (tableDataProvided) {
  299.         srcRows = tableData.map(row => {
  300.             if (!Array.isArray(row)) return [];
  301.             return row.map(cell => (cell === null || cell === undefined) ? '' : String(cell).trim());
  302.         });
  303.         // build lookup by column index 1 (as template uses tableData[i][1])
  304.         srcLookup = new Map();
  305.         for (let r = 0; r < srcRows.length; r++) {
  306.             const row = srcRows[r];
  307.             // prefer column index 1; if missing, index 0 or any non-empty
  308.             let key = '';
  309.             if (row.length > 1 && row[1]) {
  310.                 key = row[1];
  311.             } else if (row.length > 0 && row[0]) {
  312.                 key = row[0];
  313.             } else {
  314.                 // find first non-empty
  315.                 for (let c = 0; c < row.length; c++) {
  316.                     if (row[c]) { key = row[c]; break; }
  317.                 }
  318.             }
  319.             if (key) {
  320.                 // if duplicate keys, keep first
  321.                 if (!srcLookup.has(key)) srcLookup.set(key, row);
  322.             }
  323.         }
  324.     }
  325.     if (copyTableColsElementId) {
  326.         const $src = $('#' + copyTableColsElementId);
  327.         if ($src && $src.length) {
  328.             // always read headers into srcCols (we may still use tableData for rows)
  329.             $src.find('thead th').each(function () {
  330.                 srcCols.push(String($(this).text().trim()));
  331.             });
  332.             // read tbody rows from DOM only if tableData was NOT provided
  333.             if (!tableDataProvided) {
  334.                 $src.find('tbody tr').each(function () {
  335.                     const rowArr = [];
  336.                     $(this).find('td').each(function () {
  337.                         rowArr.push(String($(this).text().trim()));
  338.                     });
  339.                     srcRows.push(rowArr);
  340.                 });
  341.             }
  342.          }
  343.      }
  344.     // If no DOM source and no tableData provided, leave srcCols/srcRows empty (matching previous behaviour)
  345.     const compareCols = ['Номер п/п', 'Статус', 'ФИО поиска', 'ФИО таблицы', 'Процент совпадения, %'];
  346.     const finalCols = [];
  347.     srcCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  348.     compareCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  349.     // --- Новый фильтр "Статус поиска" ---
  350.     const statusColIndex = finalCols.indexOf('Статус');
  351.     const statusFilterDropdownId = 'compareStatusDropdown_' + compareTableId;
  352.     // HTML нового фильтра
  353.     const statusFilterHtml = `
  354. <div class="dropdown-filter">
  355.   <label class="form-label">Статус поиска ФИО:</label>
  356.   <div class="dropdown">
  357.     <button class="btn btn-primary dropdown-toggle" type="button" id="${statusFilterDropdownId}" data-toggle="dropdown" aria-expanded="false" data-multiple="false">Все статусы</button>
  358.     <div class="dropdown-menu" aria-labelledby="${statusFilterDropdownId}">
  359.       <a class="dropdown-item" href="#" data-filter="all">Все статусы</a>
  360.       <div class="dropdown-divider"></div>
  361.       <!-- Dynamic items will be added here -->
  362.     </div>
  363.   </div>
  364. </div>`;
  365.     if (clonedFiltersContainer) {
  366.         // Добавляем новый фильтр в конец контейнера
  367.         clonedFiltersContainer.append($(statusFilterHtml));
  368.         filtersHtml = $('<div>').append(clonedFiltersContainer).html();
  369.     } else {
  370.         // Создаем новый контейнер только с фильтром статуса
  371.         filtersHtml = `<div id="compare-filters-${compareTableId}" class="mb-3 d-flex align-items-center" style="gap:20px; flex-wrap:wrap;">${statusFilterHtml}</div>`;
  372.     }
  373.     // Обновляем / создаём конфиг options для CustomDataTable
  374.     if (!modifiedOptions) {
  375.         modifiedOptions = {
  376.             dropdownFiltersConfig: { elementsData: [] },
  377.             drawRowNumbersForColIndex: 0,
  378.             lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'Все']]
  379.         };
  380.     }
  381.     if (!modifiedOptions.dropdownFiltersConfig) {
  382.         modifiedOptions.dropdownFiltersConfig = { elementsData: [] };
  383.     }
  384.     if (!Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  385.         modifiedOptions.dropdownFiltersConfig.elementsData = [];
  386.     }
  387.     if (statusColIndex !== -1) {
  388.         modifiedOptions.dropdownFiltersConfig.elementsData.push({
  389.             columnIndex: statusColIndex,
  390.             dropdownId: statusFilterDropdownId,
  391.             badgeId: null,
  392.             textForAll: 'Все статусы'
  393.         });
  394.     }
  395.     // Вставляем фильтры перед сводкой
  396.     if (filtersHtml) {
  397.         html += '<div class="mb-3">' + filtersHtml + '</div>';
  398.     }
  399.     // Сводка
  400.     html += '<div class="mb-3">';
  401.     html += '<strong>Всего в таблице:</strong> ' + escapeHtml(info.originalLinesCount || 0) + '<br/>';
  402.     html += '<strong>Всего в списке поиска:</strong> ' + escapeHtml(info.compareLinesCount || 0) + '<br/>';
  403.     html += '<strong>Найдено совпадений:</strong> ' + escapeHtml(info.foundStringsCount || found.length) + '<br/>';
  404.     html += '<strong>Частично найдено (более ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesGoodSimilarityCount || 0) + '<br/>';
  405.     html += '<strong>Частично найдено (до ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesBadSimilarityCount || 0) + '<br/>';
  406.     html += '<strong>Частично найдено всего:</strong> ' + ((info.foundCompareLinesBadSimilarityCount + info.foundCompareLinesGoodSimilarityCount) || 0) + '<br/>';
  407.     html += '<strong>Не найдено в таблице:</strong> ' + notFound.length + '<br/>';
  408.     html += '<strong>Не найдено в списке поиска:</strong> ' + (info.notFoundOriginalCount || notFoundOriginal.length) + '<br/>';
  409.     html += '</div>';
  410.     html += '<h6>Результаты поиска</h6>';
  411.     // Кнопка скачивания CSV
  412.     const downloadBtnId = 'download-csv-btn-' + compareTableId;
  413.     html += '<div class="mb-2 d-flex justify-content-end">';
  414.     html += '<button id="' + downloadBtnId + '" type="button" class="btn btn-secondary btn-sm">Скачать таблицу</button>';
  415.     html += '</div>';
  416.     html += '<div class="table-responsive"><table id="' + compareTableId + '" class="table table-sm table-bordered">';
  417.     html += '<thead><tr>';
  418.     finalCols.forEach(col => { html += '<th>' + col + '</th>'; });
  419.     html += '</tr></thead><tbody>';
  420.     let rowIndex = 0;
  421.     const findSrcRowByValue = (value) => {
  422.         if (!value) return null;
  423.         const valNorm = String(value).trim();
  424.         // if we have a lookup built from tableData, use it and return only rows from tableData
  425.         if (srcLookup) {
  426.             if (srcLookup.has(valNorm)) return srcLookup.get(valNorm);
  427.             return null; // do NOT search DOM or other columns when tableData provided
  428.         }
  429.         for (let r = 0; r < srcRows.length; r++) {
  430.             for (let c = 0; c < srcRows[r].length; c++) {
  431.                 if (srcRows[r][c] === valNorm) return srcRows[r];
  432.             }
  433.         }
  434.         return null;
  435.     };
  436.     const renderRow = (cells, extraClass) => {
  437.         html += '<tr' + (extraClass ? ' class="' + extraClass + '"' : '') + '>';
  438.         finalCols.forEach(fc => {
  439.             const val = (fc in cells) ? cells[fc] : '';
  440.             html += '<td>' + (val === null || val === undefined ? '' : val) + '</td>';
  441.         });
  442.         html += '</tr>';
  443.     };
  444.     const mapSrcRowToObj = (srcRowArr) => {
  445.         const obj = {};
  446.         srcCols.forEach((h, idx) => { obj[h] = srcRowArr[idx] || ''; });
  447.         return obj;
  448.     };
  449.     if (tableDataProvided && srcRows.length) {
  450.         // build map from original value -> found item
  451.         const foundByOriginal = new Map();
  452.         found.forEach(it => {
  453.             const key = (it.foundOriginalString || '').trim();
  454.             if (key) foundByOriginal.set(key, it);
  455.         });
  456.         // iterate original rows in order
  457.         for (let r = 0; r < srcRows.length; r++) {
  458.             const srcRow = srcRows[r];
  459.             // derive key from same logic as srcLookup
  460.             let key = '';
  461.             if (srcRow.length > 1 && srcRow[1]) key = srcRow[1];
  462.             else if (srcRow.length > 0 && srcRow[0]) key = srcRow[0];
  463.             else {
  464.                 for (let c = 0; c < srcRow.length; c++) { if (srcRow[c]) { key = srcRow[c]; break; } }
  465.             }
  466.             rowIndex++;
  467.             const cells = mapSrcRowToObj(srcRow);
  468.             const foundItem = key ? foundByOriginal.get(key) : null;
  469.             if (foundItem) {
  470.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  471.                 const pct = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? Number(foundItem.similarityPercent) : 0;
  472.                 let statusText = '';
  473.                 if (pct === 100) {
  474.                     statusText = 'Найдено';
  475.                 } else if (pct > minSimilarity) {
  476.                     statusText = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  477.                 } else {
  478.                     statusText = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  479.                 }
  480.                 cells['Статус'] = statusText;
  481.                 cells['ФИО поиска'] = escapeHtml(foundItem.compareLine);
  482.                 cells['ФИО таблицы'] = escapeHtml(foundItem.foundOriginalString || key);
  483.                 cells['Процент совпадения, %'] = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? escapeHtml(foundItem.similarityPercent) : '';
  484.                 const rowClass = (pct === 100) ? 'table-success' : 'table-warning';
  485.                 renderRow(cells, rowClass);
  486.                 foundByOriginal.delete(key);
  487.             } else {
  488.                 // red row - not found in compare
  489.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  490.                 cells['Статус'] = 'Не найдено в списке поиска';
  491.                 cells['ФИО поиска'] = '';
  492.                 cells['ФИО таблицы'] = escapeHtml(key);
  493.                 cells['Процент совпадения, %'] = '';
  494.                 renderRow(cells, 'table-danger');
  495.             }
  496.         }
  497.         // render remaining compare lines that weren't matched to any original
  498.         // these are in notFound OR leftover in foundByOriginal if any
  499.         // first render compare items from notFound (explicitly unmatched)
  500.         notFound.forEach(item => {
  501.             rowIndex++;
  502.             let cells = {};
  503.             cells['Номер п/п'] = escapeHtml(rowIndex);
  504.             cells['Статус'] = 'Не найдено в таблице';
  505.             cells['ФИО поиска'] = escapeHtml(item.compareLine);
  506.             cells['ФИО таблицы'] = '';
  507.             cells['Процент совпадения, %'] = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  508.             // use blue for notFound compare lines
  509.             renderRow(cells, 'table-info');
  510.         });
  511.         // any leftover found items that weren't matched by key (edge cases)
  512.         if (foundByOriginal.size) {
  513.             foundByOriginal.forEach(it => {
  514.                 rowIndex++;
  515.                 let cells = {};
  516.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  517.                 const pct2 = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? Number(it.similarityPercent) : 0;
  518.                 let statusText2 = '';
  519.                 if (pct2 === 100) {
  520.                     statusText2 = 'Найдено';
  521.                 } else if (pct2 > minSimilarity) {
  522.                     statusText2 = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  523.                 } else {
  524.                     statusText2 = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  525.                 }
  526.                 cells['Статус'] = statusText2;
  527.                 cells['ФИО поиска'] = escapeHtml(it.compareLine);
  528.                 cells['ФИО таблицы'] = escapeHtml(it.foundOriginalString || '');
  529.                 cells['Процент совпадения, %'] = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? escapeHtml(it.similarityPercent) : '';
  530.                 const rowClass2 = (pct2 === 100) ? 'table-success' : 'table-warning';
  531.                 renderRow(cells, rowClass2);
  532.             });
  533.         }
  534.     } else {
  535.         // fallback original behaviour: show found, then notFound, then notFoundOriginal
  536.         found.forEach(item => {
  537.             rowIndex++;
  538.             const compareString = escapeHtml(item.compareLine);
  539.             const foundOriginal = escapeHtml(item.foundOriginalString);
  540.             const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  541.             let cells = {};
  542.             if (srcCols.length && foundOriginal) {
  543.                 const srcRow = findSrcRowByValue(foundOriginal);
  544.                 if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  545.             }
  546.             const pctItem = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? Number(item.similarityPercent) : 0;
  547.             let statusTextItem = '';
  548.             if (pctItem === 100) {
  549.                 statusTextItem = 'Найдено';
  550.             } else if (pctItem > minSimilarity) {
  551.                 statusTextItem = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  552.             } else {
  553.                 statusTextItem = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  554.             }
  555.             cells['Номер п/п'] = escapeHtml(rowIndex);
  556.             cells['Статус'] = statusTextItem;
  557.             cells['ФИО поиска'] = compareString;
  558.             cells['ФИО таблицы'] = foundOriginal;
  559.             cells['Процент совпадения, %'] = percent;
  560.             const rowClassItem = (pctItem === 100) ? 'table-success' : 'table-warning';
  561.             renderRow(cells, rowClassItem);
  562.         });
  563.          notFound.forEach(item => {
  564.              rowIndex++;
  565.              const compareString = escapeHtml(item.compareLine);
  566.              const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  567.              let cells = {};
  568.              cells['Номер п/п'] = escapeHtml(rowIndex);
  569.              cells['Статус'] = 'Не найдено в таблице';
  570.              cells['ФИО поиска'] = compareString;
  571.              cells['ФИО таблицы'] = '';
  572.              cells['Процент совпадения, %'] = percent;
  573.              renderRow(cells, 'table-danger');
  574.          });
  575.          notFoundOriginal.forEach(item => {
  576.              rowIndex++;
  577.              const original = escapeHtml(item);
  578.              let cells = {};
  579.              if (srcCols.length) {
  580.                  const srcRow = findSrcRowByValue(item);
  581.                  if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  582.              }
  583.              cells['Номер п/п'] = escapeHtml(rowIndex);
  584.              cells['Статус'] = 'Не найдено в списке поиска';
  585.              cells['ФИО поиска'] = '';
  586.              cells['ФИО таблицы'] = original;
  587.              cells['Процент совпадения, %'] = '';
  588.              renderRow(cells, 'table-secondary');
  589.          });
  590.      }
  591.     // --- New: ensure initialization script with modifiedOptions ---
  592.     if (modifiedOptions) {
  593.         if (modifiedOptions.drawRowNumbersForColIndex === undefined) {
  594.             modifiedOptions.drawRowNumbersForColIndex = 0;
  595.         }
  596.         html += '</tbody></table></div>';
  597.         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>';
  598.     } else {
  599.         html += '</tbody></table></div>';
  600.     }
  601.     return html;
  602.  }
  603. function compareData(data1, data2) {
  604.     return new Promise((resolve, reject) => {
  605.         Utils.fetch(API_COMPARE_DATA_URL, 'POST', { data1: data1, data2: data2 })
  606.             .then(async (response) => {
  607.                 console.log(response);
  608.                 resolve(response.response);
  609.             })
  610.             .catch((response) => {
  611.                 showToast("Ошибка. Не удалось провести сравнение. Попробуйте повторить позднее.",
  612.                     TOAST_TYPE.ERROR);
  613.                 console.log(response);
  614.                 reject(response);
  615.             });
  616.     });
  617. }
  618. function updateDependentElementVisibility(controllerSelector, $dependent) {
  619.     const $controllerElement = $(controllerSelector);
  620.     let optionAttrName = $dependent.data('controlling-select-option-attr-name');
  621.     let selectValues = $dependent.data('controlling-element-value');
  622.     if (selectValues !== null && selectValues !== undefined) {
  623.         if (!Array.isArray(selectValues)) {
  624.             selectValues = [selectValues];
  625.         }
  626.     }
  627.     const controllingElementTag = $controllerElement.prop('tagName').toLowerCase();
  628.     let selectedControllerVal = null;
  629.     switch (controllingElementTag) {
  630.         case 'input':
  631.             selectedControllerVal = !!$controllerElement.is(':checked');
  632.             break;
  633.         default:
  634.             selectedControllerVal = $controllerElement.val();
  635.     }
  636.     if (optionAttrName) {
  637.         $dependent.find('option').each(function () {
  638.             let $opt = $(this);
  639.             let optAttrVal = $opt.attr(optionAttrName);
  640.             if (optAttrVal == selectedControllerVal || $opt.val() == -1) {
  641.                 $opt.show();
  642.             } else {
  643.                 $opt.hide();
  644.             }
  645.         });
  646.         if ($dependent.find('option:selected').attr(optionAttrName) != selectedControllerVal) {
  647.             $dependent.val(-1);
  648.         }
  649.     } else if (selectValues) {
  650.         switch (controllingElementTag) {
  651.             case 'input':
  652.             case 'select':
  653.                 $dependent.toggle(selectValues.includes(selectedControllerVal));
  654.                 break;
  655.             default:
  656.                 throw new Error('Unsupported controlling element tag for data-controlling-element-value: ' + controllingElementTag);
  657.         }
  658.     }
  659. }
  660. function bindDependentElementVisibility(element) {
  661.         let $dependent = $(element);
  662.         let controllerSelector = $dependent.data('controlling-element-selector');
  663.         if (!controllerSelector) return;
  664.         $(controllerSelector).on('change', function() {
  665.             updateDependentElementVisibility(controllerSelector, $dependent);
  666.         });
  667.         if ($(controllerSelector).length) {
  668.             updateDependentElementVisibility(controllerSelector, $dependent);
  669.         }
  670. }
  671. $('[data-controlling-element-selector]').each(function() {
  672.     bindDependentElementVisibility($(this)[0]);
  673. });
  674. function createEntityCollection(element) {
  675.     let ec = new EntityCollection(element);
  676.     elementLogicObjects[element] = ec;
  677. }
  678. $('[data-init="entity-collection"]').each(function() {
  679.     createEntityCollection($(this)[0]);
  680. });
  681. function wrapTextInTag(textarea, tag) {
  682.     let start = textarea.selectionStart;
  683.     let end = textarea.selectionEnd;
  684.     let selectedText = textarea.value.substring(start, end);
  685.     if (selectedText) {
  686.         let openTag = '<' + tag + '>';
  687.         let closeTag = '</' + tag + '>';
  688.         let textBefore = textarea.value.substring(start - openTag.length, start);
  689.         let textAfter = textarea.value.substring(end, end + closeTag.length);
  690.         if (textBefore === openTag && textAfter === closeTag) {
  691.             textarea.value = textarea.value.substring(0, start - openTag.length) + selectedText + textarea.value.substring(end + closeTag.length);
  692.             let newStart = start - openTag.length;
  693.             let newEnd = newStart + selectedText.length;
  694.             textarea.focus();
  695.             textarea.setSelectionRange(newStart, newEnd);
  696.         } else {
  697.             let wrappedText = openTag + selectedText + closeTag;
  698.             textarea.value = textarea.value.substring(0, start) + wrappedText + textarea.value.substring(end);
  699.             textarea.focus();
  700.             textarea.setSelectionRange(start + openTag.length, start + openTag.length + selectedText.length);
  701.         }
  702.         $(textarea).trigger('change');
  703.     }
  704. }
  705. function bindTextEditButton(element) {
  706.     let $button = $(element);
  707.     let $formGroup = $button.closest('.form-group');
  708.     let $textarea = $formGroup.find('textarea');
  709.     $button.off('click').on('click', function() {
  710.         if ($textarea.length) {
  711.             let tag = '';
  712.             if ($button.hasClass('bold-text-button')) {
  713.                 tag = 'b';
  714.             } else if ($button.hasClass('italic-text-button')) {
  715.                 tag = 'i';
  716.             } else if ($button.hasClass('underline-text-button')) {
  717.                 tag = 'u';
  718.             }
  719.             if (tag) {
  720.                 wrapTextInTag($textarea[0], tag);
  721.             }
  722.         }
  723.     });
  724. }
  725. $('[data-dom-listener-groups*="textEditButton"]').each(function() {
  726.     bindTextEditButton($(this)[0]);
  727. });
  728. domDispatcher.eventDispatcher.on(
  729.     DomDispatcher.EVENTS.ON_DOM_NODE_ADDED,
  730.     (data, event) => {
  731.         if (data.groupNames.includes("dependentElementVisibility")) {
  732.             bindDependentElementVisibility(data.element);
  733.         } else if (data.groupNames.includes("entityCollection")) {
  734.             createEntityCollection(data.element);
  735.         } else if (data.groupNames.includes("textEditButton")) {
  736.             bindTextEditButton(data.element);
  737.         }
  738.     }, {
  739.         groupNames: ["dependentElementVisibility", "entityCollection", "textEditButton"],
  740. });
  741. /** @type {DateRangePickerWithButton} */
  742. let calendarEventsSelectorComponent = null;
  743. {#initBearer.wait([#}
  744. {#    INIT_BEARERS.GLOBAL_USER_SETTINGS_MODIFIER,#}
  745. {#    INIT_BEARERS.GLOBAL_USER_SETTINGS_FORM_SAVER,#}
  746. {#]).then(() => {#}
  747. {#    let monthSliderRangeDropdownElement = $('.month-range-slider-dropdown .dropdown').first()[0];#}
  748. {#    monthRangeSliderRangeDropdown =#}
  749. {#        new MonthRangeSliderDropdown(monthSliderRangeDropdownElement, {#}
  750. {#            monthRangeSliderOptions: {#}
  751. {#                minDate: new Date("2025-01-01") ,#}
  752. {#                maxDate:  new Date(new Date().getFullYear() + 1, 11, 31),#}
  753. {#                startDate: new Date(userSettings.startCalendarEventsDate),#}
  754. {#                endDate: new Date(userSettings.endCalendarEventsDate),#}
  755. {#            }#}
  756. {#        });#}
  757. {#    userSettingsModifier.bindMonthRangeSliderDropdown('startCalendarEventsDate',#}
  758. {#        'endCalendarEventsDate', monthRangeSliderRangeDropdown, false);#}
  759. {#    userSettingsFormSaver.eventDispatcher.on(userSettingsFormSaver.EVENTS.ON_SAVE, () => {#}
  760. {#        window.location.reload();#}
  761. {#    });#}
  762. {#    initBearer.set(INIT_BEARERS.GLOBAL_MONTH_RANGE_SLIDER_RANGE_DROPDOWN,#}
  763. {#        monthRangeSliderRangeDropdown);#}
  764. {#});#}
  765. initBearer.wait([
  766.     INIT_BEARERS.GLOBAL_USER_SETTINGS_MODIFIER,
  767.     INIT_BEARERS.GLOBAL_USER_SETTINGS_FORM_SAVER,
  768. ]).then(() => {
  769.     let element = $('.date-range-picker-with-button')[0];
  770.     if (element) {
  771.         calendarEventsSelectorComponent = new DateRangePickerWithButton(element, {
  772.             startDate: new Date(userSettings.startCalendarEventsDate),
  773.             endDate: new Date(userSettings.endCalendarEventsDate),
  774.             datePickerOptions: {
  775.                 rangeLabels: {
  776.                     TODAY: 'За сегодня',
  777.                     YESTERDAY: 'За вчера',
  778.                     LAST_7_DAYS: 'За последние 7 дней',
  779.                     LAST_30_DAYS: 'За последние 30 дней',
  780.                     THIS_MONTH: 'За этот месяц',
  781.                     LAST_MONTH: 'За прошлый месяц',
  782.                     CURRENT_AND_FUTURE: 'Текущие и будущие',
  783.                     ALL_TIME: 'За всё время',
  784.                 }
  785.             }
  786.         });
  787.         userSettingsModifier.bindDateRangePickerWithButton('startCalendarEventsDate',
  788.             'endCalendarEventsDate', calendarEventsSelectorComponent, false, "calendarEventsDateRangeName");
  789.         userSettingsFormSaver.eventDispatcher.on(userSettingsFormSaver.EVENTS.ON_SAVE, () => {
  790.             window.location.reload();
  791.         });
  792.         initBearer.set(INIT_BEARERS.GLOBAL_CALENDAR_EVENTS_SELECTOR_COMPONENT,
  793.             calendarEventsSelectorComponent);
  794.     }
  795. });
  796. {#initBearer.wait([INIT_BEARERS.GLOBAL_CALENDAR_EVENTS_SELECTOR_COMPONENT]).then(() => {#}
  797. {#    let monthRangeSliderTipToast = null;#}
  798. {#    calendarEventsSelectorComponent.eventDispatcher.on(#}
  799. {#        calendarEventsSelectorComponent.EVENTS.ON_DROPDOWN_SHOW, () => { #}{# for MonthRangeSliderDropdown #}
  800. {#            let toastMsg = `Подсказка<br><br>#}
  801. {#Для изменения периода мероприятий:<br><br>#}
  802. {#<b>1</b>. Измените положение <div class="month-range-thumb month-range-thumb-start" data-thumb="start"#}
  803. {#style="position: static; display: inline-block; transform: translate(0, 5px);"></div> <b>красного</b>#}
  804. {#и/или <div class="month-range-thumb month-range-thumb-end" data-thumb="end"#}
  805. {#style="position: static; display: inline-block; transform: translate(0, 5px);"></div> <b>синего</b><br>#}
  806. {#ползунков с помощью мыши, потянув<br>#}
  807. {#их влево или вправо.<br><br>#}
  808. {#<b>2</b>. Нажмите кнопку<br>#}
  809. {#«Применить».#}
  810. {#<br><br>#}
  811. {#<label><input type="checkbox"#}
  812. {#    data-dom-listener-groups='["userTipSettingsModifier"]'#}
  813. {#    data-json-to-component-bind-key-name="showCalendarEventsPeriodTip"#}
  814. {#> Не показывать эту подсказку в дальнейшем</label>#}
  815. {#<br>#}
  816. {#`;#}
  817. {#            if (userTipSettings.showCalendarEventsPeriodTip) {#}
  818. {#                monthRangeSliderTipToast = showToast(toastMsg, TOAST_TYPE.INFO, true, 60000 * 3);#}
  819. {#            }#}
  820. {#        });#}
  821. {#    calendarEventsSelectorComponent.eventDispatcher.on(#}
  822. {#        calendarEventsSelectorComponent.EVENTS.ON_DROPDOWN_HIDE, () => {#}
  823. {#            if (monthRangeSliderTipToast) {#}
  824. {#                monthRangeSliderTipToast.hideToast();#}
  825. {#            }#}
  826. {#        }#}
  827. {#    );#}
  828. {#});#}