Files
crm.clientright.ru/layouts/v7/modules/Vtiger/Header.tpl
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

302 lines
16 KiB
Smarty
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.

{*+**********************************************************************************
* 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">&times;</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>