Files
crm.clientright.ru/crm_extensions/nextcloud_editor/js/nextcloud-editor.js
Fedor 9245768987 🚀 CRM Files Migration & Real-time Features
 Features:
- Migrated ALL files to new S3 structure (Projects, Contacts, Accounts, HelpDesk, Invoice, etc.)
- Added Nextcloud folder buttons to ALL modules
- Fixed Nextcloud editor integration
- WebSocket server for real-time updates
- Redis Pub/Sub integration
- File path manager for organized storage
- Redis caching for performance (Functions.php)

📁 New Structure:
Documents/Project/ProjectName_ID/file_docID.ext
Documents/Contacts/FirstName_LastName_ID/file_docID.ext
Documents/Accounts/AccountName_ID/file_docID.ext

🔧 Technical:
- FilePathManager for standardized paths
- S3StorageService integration
- WebSocket server (Node.js + Docker)
- Redis cache for getBasicModuleInfo()
- Predis library for Redis connectivity

📝 Scripts:
- Migration scripts for all modules
- Test pages for WebSocket/SSE/Polling
- Documentation (MIGRATION_*.md, REDIS_*.md)

🎯 Result: 15,000+ files migrated successfully!
2025-10-24 19:59:28 +03:00

664 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Nextcloud Editor Integration JavaScript
* JavaScript для интеграции редактора документов Nextcloud
*/
/**
* Открытие папки проекта в Nextcloud
*/
function openProjectFolder(projectId, projectName) {
// Нормализуем имя проекта как в FilePathManager::sanitizeFileName
if (projectName) {
// Убираем HTML entities
projectName = projectName.replace(/"/g, '"').replace(/'/g, "'");
// Заменяем проблемные символы на подчеркивания (как в FilePathManager::sanitizeFileName)
projectName = projectName.replace(/[/\\:*?"<>|№]/g, '_');
// Заменяем пробелы и запятые на подчеркивания
projectName = projectName.replace(/[\s,]+/g, '_');
// Убираем множественные подчеркивания
projectName = projectName.replace(/_+/g, '_');
// Убираем подчеркивания с концов
projectName = projectName.replace(/^_+|_+$/g, '');
}
// Формируем URL для папки проекта в Nextcloud
const folderName = projectName ? `${projectName}_${projectId}` : `project_${projectId}`;
const encodedFolderName = encodeURIComponent(folderName);
const nextcloudUrl = 'https://office.clientright.ru:8443';
// URL для папки проекта в Nextcloud External Storage (новая структура)
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Project/${encodedFolderName}`;
console.log('🔗 Opening project folder:', { projectId, projectName, folderName, folderUrl });
// Открываем папку в новом окне
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
/**
* Обёртка для вызова из шаблонов (с подстановкой Smarty переменных)
*/
function openProjectFolderInNextcloud() {
// Эта функция будет вызываться из шаблона, где переменные уже подставлены
// См. DetailViewHeaderTitle.tpl - там прямой вызов с параметрами
console.warn('⚠️ openProjectFolderInNextcloud() called without parameters - use openProjectFolder(projectId, projectName) instead');
}
/**
* Открытие папки контакта в Nextcloud
*/
function openContactFolder(contactId, firstName, lastName) {
// Формируем полное имя контакта
let contactName = '';
if (firstName) {
contactName = firstName.trim();
}
if (lastName) {
contactName = contactName ? `${contactName}_${lastName.trim()}` : lastName.trim();
}
// Нормализуем имя контакта как в FilePathManager::sanitizeFileName
if (contactName) {
// Убираем HTML entities
contactName = contactName.replace(/&quot;/g, '"').replace(/&apos;/g, "'");
// Заменяем проблемные символы на подчеркивания
contactName = contactName.replace(/[/\\:*?"<>|№]/g, '_');
// Заменяем пробелы и запятые на подчеркивания
contactName = contactName.replace(/[\s,]+/g, '_');
// Убираем множественные подчеркивания
contactName = contactName.replace(/_+/g, '_');
// Убираем подчеркивания с концов
contactName = contactName.replace(/^_+|_+$/g, '');
}
// Формируем URL для папки контакта в Nextcloud
const folderName = contactName ? `${contactName}_${contactId}` : `contact_${contactId}`;
const encodedFolderName = encodeURIComponent(folderName);
const nextcloudUrl = 'https://office.clientright.ru:8443';
// URL для папки контакта в Nextcloud External Storage (новая структура)
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Contacts/${encodedFolderName}`;
console.log('🔗 Opening contact folder:', { contactId, firstName, lastName, contactName, folderName, folderUrl });
// Открываем папку в новом окне
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
/**
* Открытие папки контрагента в Nextcloud
*/
function openAccountFolder(accountId, accountName) {
// Нормализуем имя контрагента как в FilePathManager::sanitizeFileName
if (accountName) {
// Убираем HTML entities
accountName = accountName.replace(/&quot;/g, '"').replace(/&apos;/g, "'");
// Заменяем проблемные символы на подчеркивания
accountName = accountName.replace(/[/\\:*?"<>|№]/g, '_');
// Заменяем пробелы и запятые на подчеркивания
accountName = accountName.replace(/[\s,]+/g, '_');
// Убираем множественные подчеркивания
accountName = accountName.replace(/_+/g, '_');
// Убираем подчеркивания с концов
accountName = accountName.replace(/^_+|_+$/g, '');
}
// Формируем URL для папки контрагента в Nextcloud
const folderName = accountName ? `${accountName}_${accountId}` : `account_${accountId}`;
const encodedFolderName = encodeURIComponent(folderName);
const nextcloudUrl = 'https://office.clientright.ru:8443';
// URL для папки контрагента в Nextcloud External Storage (новая структура)
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Accounts/${encodedFolderName}`;
console.log('🔗 Opening account folder:', { accountId, accountName, folderName, folderUrl });
// Открываем папку в новом окне
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
/**
* Универсальная функция открытия папки записи в Nextcloud
* Работает для любых модулей (HelpDesk, Invoice, Leads, Act, ProjectTask, SPPayments и т.д.)
*/
function openRecordFolder(moduleName, recordId, recordName) {
// Нормализуем имя записи как в FilePathManager::sanitizeFileName
if (recordName) {
// Убираем HTML entities
recordName = recordName.replace(/&quot;/g, '"').replace(/&apos;/g, "'");
// Для HelpDesk и Invoice: убираем все кроме цифр, дефисов и подчеркиваний
// Это превратит "ЗАЯВКА_762" → "762", "инв_18" → "18" (как в скрипте миграции)
if (moduleName === 'HelpDesk' || moduleName === 'Invoice') {
recordName = recordName.replace(/[^a-zA-Z0-9\-_]/g, '_');
} else {
// Для других модулей: заменяем только проблемные символы
recordName = recordName.replace(/[/\\:*?"<>|№]/g, '_');
}
// Заменяем пробелы и запятые на подчеркивания
recordName = recordName.replace(/[\s,]+/g, '_');
// Убираем множественные подчеркивания
recordName = recordName.replace(/_+/g, '_');
// Убираем подчеркивания с концов
recordName = recordName.replace(/^_+|_+$/g, '');
}
// Формируем URL для папки записи в Nextcloud
const folderName = recordName ? `${recordName}_${recordId}` : `${moduleName}_${recordId}`;
const encodedFolderName = encodeURIComponent(folderName);
const nextcloudUrl = 'https://office.clientright.ru:8443';
// URL для папки записи в Nextcloud External Storage (новая структура)
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/${moduleName}/${encodedFolderName}`;
console.log('🔗 Opening record folder:', { moduleName, recordId, recordName, folderName, folderUrl });
// Открываем папку в новом окне
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
/**
* Открытие редактора Nextcloud для документа
*/
function openNextcloudEditor(recordId, fileName) {
// ПРОСТОЕ РЕШЕНИЕ - используем промежуточную страницу для редиректа!
const cacheVersion = Date.now(); // Принудительное обновление кеша
const redirectUrl = `/crm_extensions/file_storage/api/open_file_v2.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}&v=${cacheVersion}`;
// Открываем редактор в новом окне через промежуточную страницу
window.open(redirectUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
function testSimpleAPI(recordId, fileName) {
console.log('🧪 Testing simple API...', recordId, fileName);
$.ajax({
url: '/crm_extensions/file_storage/api/simple_test.php',
method: 'GET',
data: {
recordId: recordId,
fileName: fileName
},
dataType: 'json',
success: function(response) {
console.log('✅ Simple API works:', response);
if (response.success) {
// Если простой API работает, пробуем открыть редактор напрямую
console.log('🎯 Opening editor directly...');
// Создаем различные варианты URL прямо в JavaScript
const urls = createEditUrls(response.data.edit_url, recordId, fileName, response.data.file_id);
openEditor(urls.recommended, {
...response.data,
urls: urls.all,
recommended: 'correct_path'
});
} else {
console.error('❌ Simple API error:', response.error);
showError('Ошибка простого API: ' + response.error);
}
},
error: function(xhr, status, error) {
console.error('❌ Simple API failed:', error);
console.error('❌ Response:', xhr.responseText);
// Если простой API не работает, пробуем основной
callMainAPI(recordId, fileName);
}
});
}
function createEditUrls(baseEditUrl, recordId, fileName, fileId = 662) {
console.log('🔗 Creating edit URLs in JavaScript...', { fileId, fileName });
// Извлекаем базовый URL из базовой ссылки
const baseUrl = 'https://office.clientright.ru:8443';
const encodedFileName = encodeURIComponent(fileName);
// Определяем структуру пути в зависимости от модуля
let filePath;
if (window.app && window.app.getModuleName && window.app.getModuleName() === 'Project') {
// Для проектов используем новую структуру Project/название_ID/
const projectName = window.app.getRecordName ? window.app.getRecordName() : 'project';
// Нормализуем имя проекта как в FilePathManager::sanitizeFileName
let sanitizedProjectName = projectName;
if (sanitizedProjectName) {
// Убираем HTML entities
sanitizedProjectName = sanitizedProjectName.replace(/&quot;/g, '"').replace(/&apos;/g, "'");
// Заменяем проблемные символы на подчеркивания (как в FilePathManager::sanitizeFileName)
sanitizedProjectName = sanitizedProjectName.replace(/[/\\:*?"<>|№]/g, '_');
// Заменяем пробелы и запятые на подчеркивания
sanitizedProjectName = sanitizedProjectName.replace(/[\s,]+/g, '_');
// Убираем множественные подчеркивания
sanitizedProjectName = sanitizedProjectName.replace(/_+/g, '_');
// Убираем подчеркивания с концов
sanitizedProjectName = sanitizedProjectName.replace(/^_+|_+$/g, '');
}
filePath = `/crm/crm2/CRM_Active_Files/Documents/Project/${sanitizedProjectName}_${recordId}/${encodedFileName}`;
} else {
// Для других модулей используем старую структуру
filePath = `/crm/crm2/CRM_Active_Files/Documents/${recordId}/${encodedFileName}`;
}
// Токен для RichDocuments (из настроек Nextcloud)
const richDocumentsToken = '1sanuq71b3n4fm1ldkbb';
const urls = {
// ЛУЧШИЙ СПОСОБ! - редирект через нашу промежуточную страницу (БЕЗ CSRF!)
'redirect_to_nextcloud': fileId ? `/crm_extensions/file_storage/api/open_file.php?fileId=${fileId}&fileName=${encodedFileName}&recordId=${recordId}` : null,
// РАБОЧИЙ СПОСОБ! - прямая ссылка на Nextcloud
'files_editing_auto': fileId ? `${baseUrl}/apps/files/files/${fileId}?dir=/&editing=true&openfile=true` : null,
// Вариант без автоматического редактирования
'files_editing': fileId ? `${baseUrl}/apps/files/files/${fileId}?dir=/&editing=false&openfile=true` : null,
// Collabora Editor
'collabora_editor': fileId ? `${baseUrl}/index.php/apps/richdocuments/index?fileId=${fileId}` : null,
// OnlyOffice Editor
'onlyoffice_editor': fileId ? `${baseUrl}/apps/onlyoffice?fileId=${fileId}` : null,
// Прямое открытие файла
'files_direct': fileId ? `${baseUrl}/apps/files/files/${fileId}` : `${baseUrl}/apps/files/?dir=/&openfile=${encodedFileName}`,
// Файловый менеджер
'files_manager': `${baseUrl}/apps/files/?dir=/&openfile=${encodedFileName}`
};
// Убираем null значения
Object.keys(urls).forEach(key => {
if (urls[key] === null) {
delete urls[key];
}
});
return {
all: urls,
// РЕДИРЕКТ через нашу страницу - лучший способ (обходит CSRF)
recommended: urls.redirect_to_nextcloud || urls.files_editing_auto || urls.files_editing || urls.collabora_editor
};
}
function getEditUrls(recordId, fileName, simpleData) {
console.log('🔗 Getting edit URLs...');
$.ajax({
url: '/crm_extensions/file_storage/api/get_edit_urls.php',
method: 'GET',
data: {
recordId: recordId,
fileName: fileName
},
dataType: 'json',
success: function(response) {
console.log('✅ Edit URLs received:', response);
if (response.success) {
// Пробуем открыть рекомендуемый URL
const recommendedUrl = response.data.urls[response.data.recommended];
console.log('🎯 Trying recommended URL:', recommendedUrl);
openEditor(recommendedUrl, {
...simpleData,
urls: response.data.urls,
recommended: response.data.recommended
});
} else {
// Если не получилось, используем простой API
openEditor(simpleData.edit_url, simpleData);
}
},
error: function(xhr, status, error) {
console.error('❌ Failed to get edit URLs:', error);
// Если не получилось, используем простой API
openEditor(simpleData.edit_url, simpleData);
}
});
}
function callMainAPI(recordId, fileName) {
console.log('🎯 Calling main API...', recordId, fileName);
// Показываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.showProgress) {
app.helper.showProgress({
message: 'Подготовка файла к редактированию...'
});
}
// Вызываем API v2 для подготовки файла
$.ajax({
url: '/crm_extensions/file_storage/api/prepare_edit_v2.php',
method: 'GET',
data: {
recordId: recordId,
fileName: fileName,
module: window.app && window.app.getModuleName ? window.app.getModuleName() : 'Project'
},
dataType: 'json',
success: function(response) {
console.log('📡 API Response:', response);
// Скрываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.hideProgress) {
app.helper.hideProgress();
}
if (response.success) {
console.log('✅ File prepared successfully');
// Открываем редактор
openEditor(response.data.edit_url, response.data);
} else {
console.error('❌ API Error:', response.error);
showError('Ошибка подготовки файла: ' + response.error);
}
},
error: function(xhr, status, error) {
console.error('❌ AJAX Error:', error);
console.error('❌ Status:', status);
console.error('❌ Response:', xhr.responseText);
console.error('❌ Status Code:', xhr.status);
// Скрываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.hideProgress) {
app.helper.hideProgress();
}
// Показываем подробную ошибку
let errorMessage = 'Ошибка подключения к серверу: ' + error;
if (xhr.responseText) {
errorMessage += '\n\nОтвет сервера:\n' + xhr.responseText;
}
showError(errorMessage);
}
});
}
// Функция для открытия редактора
function openEditor(editUrl, fileData) {
console.log('🎯 Opening editor with URL:', editUrl);
console.log('📋 All available URLs:', fileData.urls);
// Открываем редактор в новом окне
var win = window.open(editUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
if (win) {
console.log('✅ Editor opened successfully');
// Показываем уведомление об успехе
if (typeof app !== 'undefined' && app.helper && app.helper.showSuccessNotification) {
app.helper.showSuccessNotification({
message: 'Редактор открыт! Файл: ' + fileData.file_name
});
} else {
// alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
}
// Показываем информацию о файле
showFileInfo(fileData);
} else {
console.log('❌ Failed to open editor window - popup blocked or error');
console.log('📋 Showing modal with alternative URLs');
// Показываем модальное окно с альтернативными вариантами
showModalWithUrls(editUrl, fileData);
}
}
// Функция для отображения информации о файле
function showFileInfo(fileData) {
// Проверяем, есть ли нужные поля в fileData
var location = 'Неизвестно';
var nextcloudPath = 'Не указан';
var status = 'Не указан';
if (fileData.file_location && fileData.file_location.type) {
location = fileData.file_location.type === 's3' ? 'S3 хранилище' : 'Локальное хранилище';
}
if (fileData.nextcloud_path) {
nextcloudPath = fileData.nextcloud_path;
}
if (fileData.message) {
status = fileData.message;
}
var infoHtml = `
<div class="alert alert-info" style="margin: 10px 0;">
<h5><i class="fa fa-info-circle"></i> Информация о файле</h5>
<p><strong>Файл:</strong> ${fileData.file_name}</p>
<p><strong>Расположение:</strong> ${location}</p>
<p><strong>Путь в Nextcloud:</strong> ${nextcloudPath}</p>
<p><strong>Статус:</strong> ${status}</p>
</div>
`;
// Если есть альтернативные URL, добавляем их
if (fileData.urls) {
infoHtml += `
<div class="alert alert-warning" style="margin: 10px 0;">
<h6><i class="fa fa-link"></i> Альтернативные способы открытия:</h6>
<div class="btn-group-vertical" style="width: 100%;">
`;
Object.keys(fileData.urls).forEach(function(key) {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const isRecommended = key === fileData.recommended;
const btnClass = isRecommended ? 'btn-success' : 'btn-default';
const icon = isRecommended ? 'fa-star' : 'fa-external-link';
infoHtml += `
<a href="${fileData.urls[key]}" target="_blank" class="btn ${btnClass} btn-sm" style="margin: 2px; text-align: left;">
<i class="fa ${icon}"></i> ${label}${isRecommended ? ' (рекомендуется)' : ''}
</a>
`;
});
infoHtml += `
</div>
</div>
`;
}
// Добавляем информацию в модальное окно или показываем отдельно
if ($('#nextcloudEditModal').length) {
$('#nextcloudEditModal .modal-body').prepend(infoHtml);
} else {
// Создаём временное уведомление
var notification = $('<div class="alert alert-info" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">' + infoHtml + '</div>');
$('body').append(notification);
// Автоматически скрываем через 5 секунд
setTimeout(function() {
notification.fadeOut(function() {
notification.remove();
});
}, 5000);
}
}
// Функция для отображения ошибок
function showError(message) {
console.error('🚨 Error:', message);
if (typeof app !== 'undefined' && app.helper && app.helper.showErrorNotification) {
app.helper.showErrorNotification({
message: message
});
} else {
alert('❌ Ошибка: ' + message);
}
}
// Функция для показа модального окна с URL
function showModalWithUrls(editUrl, fileData) {
console.log('📋 Showing modal with URLs for file:', fileData.file_name);
if (fileData.urls) {
// Используем существующую функцию showEditOptions
showEditOptions(fileData.urls, fileData.file_name, fileData.record_id);
} else {
// Если нет альтернативных URL, показываем ошибку
showError('Не удалось открыть редактор. Попробуйте открыть файл вручную в Nextcloud.');
}
}
/**
* Показ вариантов открытия файла
*/
function showEditOptions(urls, fileName, recordId) {
console.log('🎯 Showing edit options for:', fileName);
console.log('📋 Available URLs:', urls);
var buttonsHtml = '';
// Если urls - это объект (новый формат)
if (typeof urls === 'object' && !Array.isArray(urls)) {
Object.keys(urls).forEach(function(key, index) {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const url = urls[key];
const isRecommended = key === 'correct_path';
const btnClass = isRecommended ? 'btn-success' : 'btn-primary';
const icon = isRecommended ? 'fa-star' : 'fa-external-link';
buttonsHtml += `
<a href="${url}" target="_blank" class="btn ${btnClass} btn-sm" style="margin: 3px; display: block;">
<i class="fa ${icon}"></i> ${label}${isRecommended ? ' (рекомендуется)' : ''}
</a>
`;
});
} else {
// Старый формат - массив URL
var labels = [
'С параметром openfile',
'С action=edit',
'С edit=true',
'Через RichDocuments',
'Через OnlyOffice'
];
var icons = ['fa-file', 'fa-edit', 'fa-pencil', 'fa-file-text', 'fa-file-word-o'];
var colors = ['btn-primary', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger'];
urls.forEach(function(url, index) {
buttonsHtml += `
<a href="${url}" target="_blank" class="btn ${colors[index]} btn-sm" style="margin: 3px; display: block;">
<i class="fa ${icons[index]}"></i> ${labels[index]}
</a>
`;
});
}
var modalHtml = `
<div class="modal fade" id="nextcloudEditModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fa fa-edit"></i> Варианты открытия файла
</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p><strong>Файл:</strong> ${fileName}</p>
<p>Попробуйте эти варианты для прямого открытия в редакторе:</p>
<div class="btn-group-vertical" style="width: 100%;">
${buttonsHtml}
</div>
<div class="alert alert-info" style="margin-top: 15px;">
<small><strong>💡 Совет:</strong> Попробуйте варианты сверху вниз. Один из них должен открыть файл сразу в редакторе!</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
`;
// Удаляем старое модальное окно
$('#nextcloudEditModal').remove();
// Добавляем новое
$('body').append(modalHtml);
$('#nextcloudEditModal').modal('show');
}
/**
* Синхронизация изменений файла (заглушка)
*/
function syncFileChanges(recordId, fileName) {
console.log('Syncing file changes for:', recordId, fileName);
if (typeof app !== 'undefined' && app.helper && app.helper.showSuccessNotification) {
app.helper.showSuccessNotification({
message: 'Изменения синхронизированы!'
});
} else {
alert('✅ Изменения синхронизированы!');
}
}
/**
* Открытие редактора в новом окне
*/
function openInNewWindow(editUrl) {
window.open(editUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes,toolbar=no,location=no');
console.log('Opened Nextcloud editor in new window');
}
// Алиас функции для обратной совместимости
function editInNextcloud(recordId, fileName) {
console.log('📝 editInNextcloud called (alias)');
return openNextcloudEditor(recordId, fileName);
}
// Автоматическое подключение при загрузке страницы
$(document).ready(function() {
console.log('Nextcloud Editor integration loaded');
// Добавляем CSS стили для модального окна
if (!$('#nextcloud-editor-styles').length) {
$('<style id="nextcloud-editor-styles">')
.html(`
.nextcloud-edit-btn {
background: #0082c9;
border-color: #0082c9;
}
.nextcloud-edit-btn:hover {
background: #006ba6;
border-color: #006ba6;
}
`)
.appendTo('head');
}
});// Version: 1761125337