✨ 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!
302 lines
16 KiB
Smarty
302 lines
16 KiB
Smarty
{*+**********************************************************************************
|
||
* The contents of this file are subject to the vtiger CRM Public License Version 1.1
|
||
* ("License"); You may not use this file except in compliance with the License
|
||
* The Original Code is: vtiger CRM Open Source
|
||
* The Initial Developer of the Original Code is vtiger.
|
||
* Portions created by vtiger are Copyright (C) vtiger.
|
||
* All Rights Reserved.
|
||
************************************************************************************}
|
||
{strip}
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>{vtranslate($PAGETITLE, $QUALIFIED_MODULE)}</title>
|
||
<link rel="SHORTCUT ICON" href="layouts/v7/skins/images/favicon.ico">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/todc/css/bootstrap.min.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/todc/css/docs.min.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/todc/css/todc-bootstrap.min.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/font-awesome/css/font-awesome.min.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/jquery/select2/select2.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/select2-bootstrap/select2-bootstrap.css'>
|
||
<link type='text/css' rel='stylesheet' href='libraries/bootstrap/js/eternicode-bootstrap-datepicker/css/datepicker3.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/jquery/jquery-ui-1.11.3.custom/jquery-ui.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/vt-icons/style.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/animate/animate.min.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/jquery/malihu-custom-scrollbar/jquery.mCustomScrollbar.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/jquery/jquery.qtip.custom/jquery.qtip.css'>
|
||
<link type='text/css' rel='stylesheet' href='layouts/v7/lib/jquery/daterangepicker/daterangepicker.css'>
|
||
{*Salesplatform.ru begin PBXManager porting*}
|
||
<link type='text/css' rel='stylesheet' href='libraries/jquery/pnotify/jquery.pnotify.default.css'>
|
||
{*Salesplatform.ru end PBXManager porting*}
|
||
<input type="hidden" id="inventoryModules" value={ZEND_JSON::encode($INVENTORY_MODULES)}>
|
||
|
||
{assign var=V7_THEME_PATH value=Vtiger_Theme::getv7AppStylePath($SELECTED_MENU_CATEGORY)}
|
||
{if strpos($V7_THEME_PATH,".less")!== false}
|
||
<link type="text/css" rel="stylesheet/less" href="{vresource_url($V7_THEME_PATH)}" media="screen" />
|
||
{else}
|
||
<link type="text/css" rel="stylesheet" href="{vresource_url($V7_THEME_PATH)}" media="screen" />
|
||
{/if}
|
||
|
||
{foreach key=index item=cssModel from=$STYLES}
|
||
<link type="text/css" rel="{$cssModel->getRel()}" href="{vresource_url($cssModel->getHref())}" media="{$cssModel->getMedia()}" />
|
||
{/foreach}
|
||
|
||
{* For making pages - print friendly *}
|
||
<style type="text/css">
|
||
@media print {
|
||
.noprint { display:none; }
|
||
}
|
||
</style>
|
||
<script type="text/javascript">var __pageCreationTime = (new Date()).getTime();</script>
|
||
<script src="{vresource_url('layouts/v7/lib/jquery/jquery.min.js')}"></script>
|
||
<script src="{vresource_url('layouts/v7/lib/jquery/jquery-migrate-1.0.0.js')}"></script>
|
||
|
||
<!-- Подключаем функцию редактирования в Nextcloud -->
|
||
<script src="{vresource_url('layouts/v7/lib/nextcloud-editor.js')}"></script>
|
||
|
||
<script type="text/javascript">
|
||
var _META = { 'module': "{$MODULE}", view: "{$VIEW}", 'parent': "{$PARENT_MODULE}", 'notifier':"{$NOTIFIER_URL}", 'app':"{$SELECTED_MENU_CATEGORY}" };
|
||
{if $EXTENSION_MODULE}
|
||
var _EXTENSIONMETA = { 'module': "{$EXTENSION_MODULE}", view: "{$EXTENSION_VIEW}"};
|
||
{/if}
|
||
var _USERMETA;
|
||
{if $CURRENT_USER_MODEL}
|
||
_USERMETA = { 'id' : "{$CURRENT_USER_MODEL->get('id')}", 'menustatus' : "{$CURRENT_USER_MODEL->get('leftpanelhide')}",
|
||
'currency' : "{$USER_CURRENCY_SYMBOL}", 'currencySymbolPlacement' : "{$CURRENT_USER_MODEL->get('currency_symbol_placement')}",
|
||
'currencyGroupingPattern' : "{$CURRENT_USER_MODEL->get('currency_grouping_pattern')}", 'truncateTrailingZeros' : "{$CURRENT_USER_MODEL->get('truncate_trailing_zeros')}"};
|
||
{/if}
|
||
</script>
|
||
|
||
{* AI Drawer - подключение внешних файлов только для авторизованных пользователей *}
|
||
{if $CURRENT_USER_MODEL}
|
||
<link rel="stylesheet" href="layouts/v7/resources/css/ai-drawer.css?v=2.1">
|
||
<script src="layouts/v7/resources/js/ai-drawer-simple.js?v=6.1"></script>
|
||
<script src="ai_drawer_improvements.js"></script>
|
||
<script type="text/javascript">
|
||
// Функция для удаления документа из детального просмотра
|
||
window.deleteDocumentFromDetail = function(documentId) {
|
||
if (typeof Documents_Index_Js !== 'undefined') {
|
||
var documentsInstance = Documents_Index_Js.getInstance();
|
||
documentsInstance.deleteDocumentFromDetail(documentId);
|
||
} else {
|
||
console.error('Documents_Index_Js not found');
|
||
alert('Ошибка: модуль Documents не загружен');
|
||
}
|
||
};
|
||
|
||
// Функция для удаления документа из виджета
|
||
window.deleteDocumentFromWidget = function(documentId, element) {
|
||
console.log('deleteDocumentFromWidget called with ID:', documentId);
|
||
|
||
// Подтверждение удаления
|
||
var message = 'Вы уверены, что хотите удалить этот документ?';
|
||
if(confirm(message)) {
|
||
console.log('User confirmed document deletion from widget');
|
||
|
||
var postData = {
|
||
'module' : 'Documents',
|
||
'action' : 'DeleteAjax',
|
||
'record' : documentId
|
||
}
|
||
|
||
console.log('Sending delete request:', postData);
|
||
|
||
// Используем jQuery AJAX вместо app.request.post
|
||
jQuery.ajax({
|
||
url: 'index.php',
|
||
type: 'POST',
|
||
data: postData,
|
||
success: function(data) {
|
||
console.log('Delete response:', data);
|
||
console.log('Deletion successful, removing from widget');
|
||
|
||
// Удаляем элемент из виджета
|
||
var listItem = element.closest('li');
|
||
listItem.fadeOut(300, function(){
|
||
listItem.remove();
|
||
});
|
||
|
||
// Показываем уведомление об успехе
|
||
alert('Документ успешно удален');
|
||
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.log('Deletion failed:', error);
|
||
alert('Ошибка при удалении документа');
|
||
}
|
||
});
|
||
} else {
|
||
console.log('User cancelled document deletion from widget');
|
||
}
|
||
};
|
||
|
||
// Функция для отвязки документа из виджета
|
||
window.unlinkDocumentFromWidget = function(documentId, element) {
|
||
console.log('unlinkDocumentFromWidget called with ID:', documentId);
|
||
|
||
// Подтверждение отвязки
|
||
var message = 'Вы уверены, что хотите отвязать этот документ от контакта?';
|
||
if(confirm(message)) {
|
||
console.log('User confirmed document unlinking from widget');
|
||
|
||
// Получаем информацию о текущем контакте
|
||
var parentId = jQuery('#recordId').val();
|
||
// Получаем модуль из URL или используем Contacts по умолчанию
|
||
var parentModule = 'Contacts'; // По умолчанию для контактов
|
||
var currentUrl = window.location.href;
|
||
if(currentUrl.indexOf('module=') > -1) {
|
||
var moduleMatch = currentUrl.match(/module=([^&]+)/);
|
||
if(moduleMatch) {
|
||
parentModule = moduleMatch[1];
|
||
}
|
||
}
|
||
|
||
if(parentId && parentModule) {
|
||
var postData = {
|
||
'module' : parentModule,
|
||
'action' : 'UnlinkRelation',
|
||
'record' : parentId,
|
||
'relatedModule' : 'Documents',
|
||
'relatedRecord' : documentId
|
||
}
|
||
|
||
console.log('Sending unlink request:', postData);
|
||
|
||
// Используем jQuery AJAX вместо app.request.post
|
||
jQuery.ajax({
|
||
url: 'index.php',
|
||
type: 'POST',
|
||
data: postData,
|
||
success: function(data) {
|
||
console.log('Unlink response:', data);
|
||
console.log('Unlinking successful, removing from widget');
|
||
|
||
// Удаляем элемент из виджета
|
||
var listItem = element.closest('li');
|
||
listItem.fadeOut(300, function(){
|
||
listItem.remove();
|
||
});
|
||
|
||
// Показываем уведомление об успехе
|
||
alert('Документ успешно отвязан от контакта');
|
||
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.log('Unlinking failed:', error);
|
||
alert('Ошибка при отвязке документа');
|
||
}
|
||
});
|
||
} else {
|
||
console.error('Parent ID or module not found');
|
||
alert('Ошибка: не удалось определить контакт');
|
||
}
|
||
} else {
|
||
console.log('User cancelled document unlinking from widget');
|
||
}
|
||
};
|
||
|
||
|
||
// Тестовая функция для отладки
|
||
</script>
|
||
<script type="text/javascript">
|
||
// Инициализация нового AI Drawer только для авторизованных пользователей
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
try {
|
||
console.log('AI Drawer: Initializing new version');
|
||
// Инициализируем новый AI Drawer
|
||
if (typeof AIDrawer !== 'undefined') {
|
||
window.aiDrawerInstance = new AIDrawer();
|
||
console.log('AI Drawer: New version initialized successfully');
|
||
|
||
// Отслеживаем смену URL для обновления истории
|
||
let currentURL = window.location.href;
|
||
const urlObserver = setInterval(function() {
|
||
if (window.location.href !== currentURL) {
|
||
currentURL = window.location.href;
|
||
console.log('AI Drawer: URL changed, refreshing history');
|
||
if (window.aiDrawerInstance && typeof window.aiDrawerInstance.refreshPreloadedHistory === 'function') {
|
||
// Обновляем историю с небольшой задержкой чтобы страница успела загрузиться
|
||
setTimeout(() => {
|
||
window.aiDrawerInstance.refreshPreloadedHistory();
|
||
}, 1000);
|
||
}
|
||
}
|
||
}, 1000);
|
||
|
||
// Также слушаем события навигации
|
||
window.addEventListener('popstate', function() {
|
||
console.log('AI Drawer: Popstate event, refreshing history');
|
||
if (window.aiDrawerInstance && typeof window.aiDrawerInstance.refreshPreloadedHistory === 'function') {
|
||
setTimeout(() => {
|
||
window.aiDrawerInstance.refreshPreloadedHistory();
|
||
}, 1000);
|
||
}
|
||
});
|
||
|
||
} else {
|
||
console.error('AI Drawer: AIDrawer class not found');
|
||
}
|
||
} catch (error) {
|
||
console.error('AI Drawer: Initialization error:', error);
|
||
}
|
||
});
|
||
</script>
|
||
{/if}
|
||
|
||
<!-- Простая функция Nextcloud Editor -->
|
||
|
||
<!-- File Sync: Long Polling синхронизация файлов -->
|
||
{* ОТКЛЮЧЕНО - создает лишнюю нагрузку *}
|
||
{* if $CURRENT_USER_MODEL}
|
||
<script type="text/javascript" src="crm_extensions/file_storage/js/file_sync.js"></script>
|
||
{/if *}
|
||
|
||
</head>
|
||
<body data-skinpath="{Vtiger_Theme::getBaseThemePath()}" data-language="{$LANGUAGE}"{if $CURRENT_USER_MODEL} data-user-decimalseparator="{$CURRENT_USER_MODEL->get('currency_decimal_separator')}" data-user-dateformat="{$CURRENT_USER_MODEL->get('date_format')}"
|
||
data-user-groupingseparator="{$CURRENT_USER_MODEL->get('currency_grouping_separator')}" data-user-numberofdecimals="{$CURRENT_USER_MODEL->get('no_of_currency_decimals')}" data-user-hourformat="{$CURRENT_USER_MODEL->get('hour_format')}"
|
||
data-user-calendar-reminder-interval="{$CURRENT_USER_MODEL->getCurrentUserActivityReminderInSeconds()}"{/if}>
|
||
{if $CURRENT_USER_MODEL}<input type="hidden" id="start_day" value="{$CURRENT_USER_MODEL->get('dayoftheweek')}" />{/if}
|
||
{* SalesPlatform.ru begin #5116 fixed localization *}
|
||
<input type="hidden" name="locale" value='{json_encode($LOCALE)}'>
|
||
{* SalesPlatform.ru end *}
|
||
{*
|
||
<!-- AI Drawer HTML - ЗАКОММЕНТИРОВАНО для предотвращения дублирования -->
|
||
<!-- JavaScript создает AI Drawer динамически, поэтому статический HTML не нужен -->
|
||
{if $CURRENT_USER_MODEL}
|
||
<div class="ai-drawer font-normal">
|
||
<div class="ai-drawer-header">
|
||
<span>AI Ассистент</span>
|
||
<button type="button" class="ai-drawer-close">×</button>
|
||
</div>
|
||
<div class="ai-font-controls">
|
||
<label>Размер шрифта:</label>
|
||
<button type="button" class="font-btn" data-size="small">Мелкий</button>
|
||
<button type="button" class="font-btn active" data-size="normal">Обычный</button>
|
||
<button type="button" class="font-btn" data-size="large">Крупный</button>
|
||
<button type="button" class="font-btn" data-size="extra-large">Очень крупный</button>
|
||
</div>
|
||
<div class="ai-avatar-controls">
|
||
<label>Аватарка ассистента:</label>
|
||
<button type="button" class="avatar-btn active" data-type="default">🤖</button>
|
||
<button type="button" class="avatar-btn" data-type="friendly">😊</button>
|
||
<button type="button" class="avatar-btn" data-type="helpful">💡</button>
|
||
<button type="button" class="avatar-btn" data-type="smart">🧠</button>
|
||
</div>
|
||
<div class="ai-drawer-content">
|
||
<div class="ai-chat-messages"></div>
|
||
<div class="ai-chat-input-container">
|
||
<textarea class="ai-chat-input" placeholder="Задайте вопрос AI ассистенту..."></textarea>
|
||
<button class="ai-send-btn">Отправить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="ai-drawer-toggle">AI</button>
|
||
{/if}
|
||
{*}
|
||
|
||
<div id="page">
|
||
<div id="pjaxContainer" class="hide noprint"></div>
|
||
<div id="messageBar" class="hide"></div> |