This commit is contained in:
2026-02-15 16:59:54 +03:00
parent 0b95dd62e1
commit ea28427c3d
25 changed files with 7336 additions and 0 deletions
+808
View File
@@ -0,0 +1,808 @@
// singing.js
(function () {
// Глобальная переменная для хранения текущего уведомления
let currentMessageEl = null;
let messageTimeout = null;
let isSending = false;
// Функция для отображения сообщений
function showMessage(message, type = 'success', duration = 3000) {
// Удаляем предыдущее уведомление, если оно есть
if (currentMessageEl && currentMessageEl.parentNode) {
clearTimeout(messageTimeout);
document.body.removeChild(currentMessageEl);
currentMessageEl = null;
}
const messageEl = document.createElement('div');
messageEl.className = `el-message el-message--${type}`;
messageEl.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
min-width: 380px;
padding: 15px 15px 15px 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background-color: ${type === 'success' ? '#f0f9eb' :
type === 'info' ? '#f4f4f5' : '#fef0f0'};
border: 1px solid ${type === 'success' ? '#e1f3d8' :
type === 'info' ? '#e9e9eb' : '#fde2e2'};
color: ${type === 'success' ? '#67c23a' :
type === 'info' ? '#909399' : '#f56c6c'};
display: flex;
align-items: center;
`;
const iconClass = type === 'success' ? 'el-icon-success' :
type === 'info' ? 'el-icon-info' : 'el-icon-error';
const iconColor = type === 'success' ? '#67c23a' :
type === 'info' ? '#909399' : '#f56c6c';
messageEl.innerHTML = `
<i class="el-icon ${iconClass}" style="font-size: 16px; color: ${iconColor}; margin-right: 10px;"></i>
<span style="font-size: 14px;">${message}</span>
`;
document.body.appendChild(messageEl);
currentMessageEl = messageEl;
// Скрываем через указанное время
messageTimeout = setTimeout(() => {
if (currentMessageEl === messageEl && messageEl.parentNode) {
document.body.removeChild(messageEl);
currentMessageEl = null;
}
}, duration);
}
// Функция отправки сообщения
function sendMessageToContent(type, payload) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Таймаут ожидания ответа'));
}, 60000);
const messageHandler = (event) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'medods-extension' ||
event.data.type !== `${type}Response`
) {
return;
}
window.removeEventListener('message', messageHandler);
clearTimeout(timeoutId);
resolve(event.data.payload);
};
window.addEventListener('message', messageHandler);
window.postMessage({
source: 'medods-extension',
type: type,
payload: payload,
}, '*');
});
}
// Обработчик для финальных результатов от background
function setupBackgroundResultHandler() {
window.addEventListener('message', (event) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'medods-extension' ||
event.data.type !== 'backgroundProcessingResult'
) {
return;
}
console.log('Получен финальный результат от background:', event.data);
const result = event.data.payload;
if (result.success) {
showMessage(`Успешно отправлено документов: ${result.sentCount || 1}`, 'success');
} else {
const errorMessage = result.message || 'Неизвестная ошибка';
showMessage(`Ошибка: ${errorMessage}`, 'error');
}
});
}
// Проверка совместимости документов
function checkDocumentsCompatibility(documents) {
if (documents.length === 0) {
return { compatible: true, type: null };
}
let hasIds = false;
let hasNonIds = false;
for (const doc of documents) {
if (doc.title.includes('ИДС')) {
hasIds = true;
} else {
hasNonIds = true;
}
if (hasIds && hasNonIds) {
return {
compatible: false,
type: 'error',
message: 'Нельзя одновременно отправлять документы требующие для подписания авторизацию ЕСИА (ГосУслуги) - "ИДС" и другие документы. Выберите документы одного типа.'
};
}
}
const docType = hasIds ? 'через ЕСИА (ГосУслуги)' : 'ПЭП';
return {
compatible: true,
type: docType
};
}
// Функция получения выбранных документов
function getSelectedDocuments() {
const table = document.querySelector('.m-table.m-table-generator.m-si-generator__table');
if (!table) return [];
const checkboxes = table.querySelectorAll('.el-checkbox__original');
const selectedDocs = [];
checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
const row = checkbox.closest('.m-table-row');
if (row) {
const rowData = {};
const numberCell = row.querySelector('.col__number .m-table-row-cell__struct');
const titleCell = row.querySelector('.col__title .m-table-row-cell__struct');
if (numberCell) rowData.number = numberCell.textContent.trim();
if (titleCell) rowData.title = titleCell.textContent.trim();
if (rowData.number && rowData.title) {
selectedDocs.push(rowData);
}
}
}
});
return selectedDocs;
}
// Функция проверки ограничения в 5 документов
function validateDocumentCount(documents) {
if (documents.length === 0) {
return { valid: false, message: 'Выберите хотя бы один документ для отправки' };
}
if (documents.length > 5) {
return { valid: false, message: 'Нельзя отправить более 5 документов за раз' };
}
return { valid: true, message: '' };
}
// Функция создания модального окна предварительной проверки
function showPreSingingDialog(documents, preSingingData) {
// Проверяем данные
const signature = preSingingData.signature || {};
const inProgress = preSingingData.inProgress || [];
const complete = preSingingData.complete || [];
const daysRemainingControl = preSingingData.daysRemainingControl || 30;
// Определяем тип документов
const hasIds = documents.some(doc => doc.title.includes('ИДС'));
// Проверяем соответствие типов
let typeMatch = false;
if (signature.type === 'eSignature' && hasIds) {
typeMatch = true;
} else if (signature.type === 'eAttorney' && !hasIds) {
typeMatch = true;
}
// Проверяем срок действия
let expirationInfo = '';
let expirationFormatted = '';
let expirationClass = 'info';
let expirationTitle = '';
let isExpired = false;
let daysRemaining = 0;
if (signature.expiration) {
const expirationDate = new Date(signature.expiration);
const today = new Date();
const timeDiff = expirationDate - today;
daysRemaining = Math.ceil(timeDiff / (1000 * 3600 * 24));
// Форматируем дату
const dateFormatter = new Intl.DateTimeFormat('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
expirationFormatted = dateFormatter.format(expirationDate);
expirationInfo = expirationFormatted;
// Определяем тип для заголовка
if (signature.type === 'eSignature') {
expirationTitle = 'Электронная подпись';
} else if (signature.type === 'eAttorney') {
expirationTitle = 'Электронная доверенность';
}
if (daysRemaining < 0) {
expirationClass = 'danger';
isExpired = true;
} else if (daysRemaining < daysRemainingControl) {
expirationClass = 'warning';
} else {
expirationClass = 'success';
}
}
// Определяем статусы документов
const documentsWithStatus = documents.map(doc => {
const docNumber = parseInt(doc.number, 10);
let status = 'ready';
let statusClass = 'success';
let statusIcon = 'el-icon-success';
if (inProgress.includes(docNumber)) {
status = 'in-progress';
statusClass = 'info';
statusIcon = 'el-icon-time';
} else if (complete.includes(docNumber)) {
status = 'complete';
statusClass = 'primary';
statusIcon = 'el-icon-check';
}
return {
...doc,
status,
statusClass,
statusIcon,
isEsia: doc.title.includes('ИДС')
};
});
// Определяем, можно ли отправлять
let canSend = typeMatch && !isExpired;
// Проверяем статусы документов
const hasInProgress = documentsWithStatus.some(doc => doc.status === 'in-progress');
const hasComplete = documentsWithStatus.some(doc => doc.status === 'complete');
if (hasInProgress || hasComplete) {
canSend = false;
}
// Если нет signature - нельзя отправлять
if (!signature.type) {
canSend = false;
}
// Если типы не соответствуют
if (!typeMatch && signature.type) {
canSend = false;
}
// Создаем элементы модального окна напрямую
const modalWrapper = document.createElement('div');
modalWrapper.className = 'el-dialog__wrapper';
modalWrapper.style.cssText = 'z-index: 2001; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow: auto;';
const backdrop = document.createElement('div');
backdrop.className = 'v-modal';
backdrop.style.cssText = 'z-index: 2001;';
backdrop.tabIndex = 0;
const dialog = document.createElement('div');
dialog.className = 'el-dialog';
dialog.style.cssText = 'margin-top: 15vh; width: 700px; z-index: 2002; position: relative; margin: 15vh auto 50px; background: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);';
// Создаем заголовок
const dialogHeader = document.createElement('div');
dialogHeader.className = 'el-dialog__header';
const dialogTitle = document.createElement('span');
dialogTitle.className = 'el-dialog__title';
dialogTitle.style.cssText = 'line-height: 24px; font-size: 18px; color: #303133;';
dialogTitle.textContent = 'Подготовка документов к отправке для электронного подписания';
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.className = 'el-dialog__headerbtn';
closeButton.style.cssText = 'position: absolute; top: 20px; right: 20px; padding: 0; background: 0 0; border: none; outline: 0; cursor: pointer; font-size: 16px;';
const closeIcon = document.createElement('i');
closeIcon.className = 'el-dialog__close el-icon el-icon-close';
closeButton.appendChild(closeIcon);
dialogHeader.appendChild(dialogTitle);
dialogHeader.appendChild(closeButton);
// Создаем тело диалога
const dialogBody = document.createElement('div');
dialogBody.className = 'el-dialog__body';
dialogBody.style.cssText = 'padding: 15px; color: #606266; font-size: 14px;';
// Создаем контент
const dialogContent = document.createElement('div');
dialogContent.className = 'dialog-content';
// Предупреждения (ближе к заголовку)
if (!canSend) {
const warningsSection = document.createElement('div');
warningsSection.className = 'warnings-section';
warningsSection.style.cssText = 'margin-bottom: 15px;';
// Собираем все предупреждения в один массив
const warnings = [];
if (!signature.type) {
warnings.push({
type: 'error',
title: 'Нет прав на отправку',
message: 'У вас нет прав на электронное подписание документов'
});
}
if (daysRemaining < 0) {
let typeName = 'документа';
if (signature.type === 'eSignature') {
typeName = 'электронной подписи';
} else if (signature.type === 'eAttorney') {
typeName = 'электронной доверенности';
}
warnings.push({
type: 'error',
title: 'Срок действия истек',
message: `Срок действия ${typeName} истек ${Math.abs(daysRemaining)} дней назад`
});
}
if (!typeMatch && signature.type) {
const availableType = signature.type === 'eSignature' ? 'электронная подпись' : 'электронная доверенность';
const neededType = hasIds ? 'электронная подпись' : 'электронная доверенность';
warnings.push({
type: 'warning',
title: 'Несоответствие типов документов',
message: `Для подписания подготовлены документы для которых требуется "${neededType}", но у вас доступна "${availableType}"`
});
}
if (hasInProgress || hasComplete) {
warnings.push({
type: 'warning',
title: 'Невозможно отправить документы',
message: 'Среди выбранных документов есть уже подписанные (✔️) или направленные для подписания (🕒)'
});
}
// Отображаем все предупреждения
warnings.forEach(warning => {
const warningEl = document.createElement('div');
warningEl.className = `el-alert el-alert--${warning.type} is-light`;
warningEl.style.cssText = 'margin-bottom: 8px; padding: 8px; border-radius: 4px;';
const warningContent = document.createElement('div');
warningContent.className = 'el-alert__content';
const warningTitle = document.createElement('span');
warningTitle.className = 'el-alert__title';
warningTitle.style.cssText = 'font-size: 13px; color: ' +
(warning.type === 'error' ? '#f56c6c' : '#e6a23c') + ';';
warningTitle.textContent = warning.title;
const warningDesc = document.createElement('p');
warningDesc.className = 'el-alert__description';
warningDesc.style.cssText = 'font-size: 12px; margin-top: 3px; color: ' +
(warning.type === 'error' ? '#f56c6c' : '#e6a23c') + ';';
warningDesc.textContent = warning.message;
warningContent.appendChild(warningTitle);
warningContent.appendChild(warningDesc);
warningEl.appendChild(warningContent);
warningsSection.appendChild(warningEl);
});
dialogContent.appendChild(warningsSection);
}
// Список документов
const documentsSection = document.createElement('div');
documentsSection.className = 'documents-section';
const documentsTitle = document.createElement('h4');
documentsTitle.style.cssText = 'margin: 0 0 10px 0; color: #303133; font-size: 16px;';
documentsTitle.textContent = 'Список документов:';
const documentsList = document.createElement('div');
documentsList.style.cssText = 'border: 1px solid #EBEEF5; border-radius: 4px; padding: 0 10px; max-height: 200px; overflow-y: auto;';
// Добавляем документы в список
documentsWithStatus.forEach((doc, index) => {
const docItem = document.createElement('div');
// Для последнего элемента не добавляем нижнюю границу
if (index === documentsWithStatus.length - 1) {
docItem.style.cssText = 'display: flex; align-items: center; padding: 8px 0;';
} else {
docItem.style.cssText = 'display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #F0F0F0;';
}
// Статус (иконка слева)
const docStatusIcon = document.createElement('div');
docStatusIcon.style.cssText = 'flex: 0 0 24px; margin-right: 8px;';
const statusIcon = document.createElement('i');
statusIcon.className = doc.statusIcon;
statusIcon.style.cssText = `font-size: 16px; color: ${doc.status === 'ready' ? '#67c23a' :
doc.status === 'in-progress' ? '#909399' : '#409eff'
};`;
docStatusIcon.appendChild(statusIcon);
// Номер документа (стилизован в зависимости от статуса)
const docNumber = document.createElement('div');
docNumber.style.cssText = 'flex: 0 0 60px;';
const numberTag = document.createElement('span');
if (doc.status === 'ready') {
numberTag.className = 'el-tag el-tag--success el-tag--small';
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
} else if (doc.status === 'in-progress') {
numberTag.className = 'el-tag el-tag--info el-tag--small';
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
} else {
numberTag.className = 'el-tag el-tag--primary el-tag--small';
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
}
numberTag.textContent = `${doc.number}`;
docNumber.appendChild(numberTag);
// Название документа (занимает все оставшееся пространство)
const docTitle = document.createElement('div');
docTitle.style.cssText = 'flex: 1; color: #606266; font-size: 13px; padding: 0 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
docTitle.textContent = doc.title;
docItem.appendChild(docStatusIcon);
docItem.appendChild(docNumber);
docItem.appendChild(docTitle);
documentsList.appendChild(docItem);
});
documentsSection.appendChild(documentsTitle);
documentsSection.appendChild(documentsList);
dialogContent.appendChild(documentsSection);
dialogBody.appendChild(dialogContent);
// Создаем футер
const dialogFooter = document.createElement('div');
dialogFooter.className = 'el-dialog__footer';
dialogFooter.style.cssText = 'display: flex; justify-content: space-between; align-items: center;';
// Левая часть футера (информация о сроке действия разрешения на подпись)
const footerLeft = document.createElement('div');
footerLeft.style.cssText = 'flex: 1; display: flex; flex-direction: column; align-items: flex-start;';
if (signature.expiration && signature.type) {
// Тип разрешения
const typeContainer = document.createElement('div');
typeContainer.style.cssText = 'margin-bottom: 5px; margin-top: 0px;';
const typeText = document.createElement('span');
typeText.style.cssText = 'font-size: 12px; font-weight: 500; color: #606266;';
if (signature.type === 'eSignature') {
typeText.textContent = 'Электронная подпись';
} else if (signature.type === 'eAttorney') {
typeText.textContent = 'Электронная доверенность';
}
typeContainer.appendChild(typeText);
// Срок действия
const expirationContainer = document.createElement('div');
expirationContainer.style.cssText = 'display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 0px;';
const expirationLabel = document.createElement('span');
expirationLabel.style.cssText = 'font-size: 12px; color: #606266; margin-right: 5px;';
expirationLabel.textContent = 'Срок действия:';
const expirationDate = document.createElement('span');
expirationDate.style.cssText = 'font-size: 12px; color: #606266; margin-right: 8px;';
expirationDate.textContent = expirationFormatted;
expirationContainer.appendChild(expirationLabel);
expirationContainer.appendChild(expirationDate);
// Остаточный срок (показываем справа от даты если меньше 30 дней)
if (daysRemaining >= 0 && daysRemaining < 30) {
const warningIcon = document.createElement('i');
warningIcon.className = 'el-icon-warning';
warningIcon.style.cssText = 'font-size: 14px; color: #e6a23c; margin-left: 4px; margin-right: 4px;';
const remainingText = document.createElement('span');
remainingText.style.cssText = 'font-size: 12px; color: #e6a23c; font-weight: 500;';
remainingText.textContent = `осталось ${daysRemaining} ${getDaysText(daysRemaining)}`;
typeContainer.appendChild(warningIcon);
typeContainer.appendChild(remainingText);
}
footerLeft.appendChild(typeContainer);
footerLeft.appendChild(expirationContainer);
}
// Вспомогательная функция для склонения дней
function getDaysText(days) {
const lastDigit = days % 10;
const lastTwoDigits = days % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
return 'дней';
}
switch (lastDigit) {
case 1: return 'день';
case 2:
case 3:
case 4: return 'дня';
default: return 'дней';
}
}
// Правая часть футера (кнопки)
const footerRight = document.createElement('div');
footerRight.className = 'dialog-footer';
footerRight.style.cssText = 'display: flex; align-items: center; padding-top: 10px;';
// Кнопка "Отмена" - стиль как у кнопки на главной странице
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'el-button m-button el-button--small is-plain button-with-icon cancel-button';
const cancelIcon = document.createElement('i');
cancelIcon.className = 'm-icon fa-times button-icon fad';
cancelIcon.style.cssText = 'font-size: 14px; margin-right: 6px;';
const cancelText = document.createElement('span');
cancelText.textContent = 'Отмена';
cancelBtn.appendChild(cancelIcon);
cancelBtn.appendChild(cancelText);
// Кнопка "Отправить документы" - стиль как у кнопки "подписать электронно"
const sendBtn = document.createElement('button');
sendBtn.type = 'button';
if (canSend) {
sendBtn.className = 'el-button m-button el-button--small is-plain send-button button-with-icon';
} else {
sendBtn.className = 'el-button m-button el-button--small is-plain send-button button-with-icon is-disabled';
}
const sendIcon = document.createElement('i');
sendIcon.className = 'm-icon fa-file-signature button-icon fad';
sendIcon.style.cssText = 'font-size: 14px; margin-right: 6px;';
const sendText = document.createElement('span');
sendText.textContent = 'Отправить документы';
sendBtn.appendChild(sendIcon);
sendBtn.appendChild(sendText);
sendBtn.disabled = !canSend;
footerRight.appendChild(cancelBtn);
footerRight.appendChild(sendBtn);
dialogFooter.appendChild(footerLeft);
dialogFooter.appendChild(footerRight);
// Собираем диалог
dialog.appendChild(dialogHeader);
dialog.appendChild(dialogBody);
dialog.appendChild(dialogFooter);
// Собираем модальное окно
modalWrapper.appendChild(backdrop);
modalWrapper.appendChild(dialog);
document.body.appendChild(modalWrapper);
// Обработчик отправки
async function handleSend() {
if (!canSend) return;
closeModal();
// Отправляем документы на подписание
try {
const docNumbers = documents.map(doc => parseInt(doc.number, 10));
const result = await sendMessageToContent('sendDocuments', {
docNumbers: docNumbers
});
if (result && result.response) {
if (result.response.status === 'processing') {
showMessage('Документы приняты в обработку. Ожидайте результата...', 'info', 5000);
}
}
} catch (error) {
console.error('Ошибка при отправке:', error);
showMessage(`Ошибка: ${error.message}`, 'error');
}
}
function closeModal() {
if (modalWrapper.parentNode) {
document.body.removeChild(modalWrapper);
}
}
// Назначаем обработчики
closeButton.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
backdrop.addEventListener('click', closeModal);
if (canSend) {
sendBtn.addEventListener('click', handleSend);
}
// Закрытие по ESC
const escHandler = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
// Удаляем обработчик ESC при закрытии
const originalClose = closeModal;
closeModal = () => {
document.removeEventListener('keydown', escHandler);
originalClose();
};
}
// Функция предварительной проверки документов
async function preSingingCheck(documents) {
try {
// Проверяем количество документов
const countValidation = validateDocumentCount(documents);
if (!countValidation.valid) {
showMessage(countValidation.message, 'error');
return;
}
// Проверяем совместимость документов
const compatibility = checkDocumentsCompatibility(documents);
if (!compatibility.compatible) {
showMessage(compatibility.message, 'error');
return;
}
// Отправляем запрос на предварительную проверку
showMessage('Проверка документов...', 'info');
// Отправляем запрос в content.js, чтобы он собрал и отправил данные
const result = await sendMessageToContent('preSingingCheck', {
// Передаем только флаг о типе документов для совместимости
docNumbers: documents.map(doc => parseInt(doc.number))
});
if (result && result.success && result.data) {
// Показываем диалоговое окно с результатами проверки
showPreSingingDialog(documents, result.data);
} else {
showMessage('Ошибка при проверке документов', 'error');
}
} catch (error) {
console.error('Ошибка предварительной проверки:', error);
showMessage(`Ошибка: ${error.message}`, 'error');
}
}
// Функция добавления кнопки
function addSendBtn() {
const sendBtn = document.createElement('button');
sendBtn.className = 'el-button m-button el-button--small is-disabled is-plain button-with-icon';
sendBtn.innerHTML = `
<span>
<i class="m-icon fa-file-signature button-icon fad" style="font-size: 16px;"></i>
<span class="m-button__text use-indent">Подписать электронно</span>
</span>
`;
sendBtn.disabled = true;
// Локальный обработчик для кнопки
async function handleSendClick() {
if (isSending) {
console.log('Уже идет отправка, игнорируем клик');
return;
}
const selectedDocs = getSelectedDocuments();
// Запускаем предварительную проверку
preSingingCheck(selectedDocs);
}
function checkSelectedDocuments() {
return getSelectedDocuments().length > 0;
}
function updateButtonState() {
const hasSelectedDocs = checkSelectedDocuments();
sendBtn.disabled = !hasSelectedDocs || isSending;
if (sendBtn.disabled) {
sendBtn.classList.add('is-disabled');
} else {
sendBtn.classList.remove('is-disabled');
}
}
function setupTableObserver() {
const table = document.querySelector('.m-table.m-table-generator.m-si-generator__table');
if (!table) {
console.warn('Таблица документов не найдена');
return;
}
table.addEventListener('change', (e) => {
if (e.target.classList.contains('el-checkbox__original')) {
updateButtonState();
}
});
table.addEventListener('click', (e) => {
if (e.target.closest('.el-checkbox') ||
e.target.closest('.el-checkbox__inner')) {
setTimeout(updateButtonState, 10);
}
});
setTimeout(updateButtonState, 100);
}
const m_panel_footer = document.querySelector('.m-panel__footer');
if (m_panel_footer) {
m_panel_footer.prepend(sendBtn);
sendBtn.addEventListener('click', handleSendClick);
setupTableObserver();
updateButtonState();
setupBackgroundResultHandler();
} else {
location.reload();
}
}
// Запуск
setTimeout(() => {
addSendBtn();
}, 500);
})();