🚀 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!
This commit is contained in:
Fedor
2025-10-24 19:59:28 +03:00
parent 3fb2ad5f60
commit 9245768987
1062 changed files with 161778 additions and 16212 deletions

View File

@@ -38,7 +38,7 @@ function openNextcloudEditor(recordId, fileName) {
console.log('🚀 NEXTCLOUD EDITOR: Function called!', recordId, fileName);
// ПРОСТОЕ РЕШЕНИЕ - используем промежуточную страницу для редиректа!
const redirectUrl = `/crm_extensions/file_storage/api/open_file.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}`;
const redirectUrl = `/crm_extensions/file_storage/api/open_file_v2.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}`;
console.log('🎯 Opening editor via redirect:', redirectUrl);

View File

@@ -39,6 +39,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки контрагента в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openAccountFolder('{$RECORD->getId()}', '{$RECORD->get("accountname")|escape:javascript}')" title="Открыть папку контрагента в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
<div class="info-row">
<i class="fa fa-map-marker"></i>&nbsp;
<a class="showMap" href="javascript:void(0);" onclick='Vtiger_Index_Js.showMap(this);' data-module='{$RECORD->getModule()->getName()}' data-record='{$RECORD->getId()}'>{vtranslate('LBL_SHOW_MAP', $MODULE_NAME)}</a>

View File

@@ -49,6 +49,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки контакта в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openContactFolder('{$RECORD->getId()}', '{$RECORD->get("firstname")|escape:javascript}', '{$RECORD->get("lastname")|escape:javascript}')" title="Открыть папку контакта в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
<div class="info-row">
<i class="fa fa-map-marker"></i>&nbsp;
<a class="showMap" href="javascript:void(0);" onclick='Vtiger_Index_Js.showMap(this);' data-module='{$RECORD->getModule()->getName()}' data-record='{$RECORD->getId()}'>{vtranslate('LBL_SHOW_MAP', $MODULE_NAME)}</a>

View File

@@ -24,7 +24,7 @@
<span aria-hidden="true" class='fa fa-close'></span></button>
{* Кнопка редактирования - показываем для ВСЕХ файлов *}
<div class="col-lg-2">
<button type="button" class="btn btn-success btn-small pull-right" onclick="openNextcloudEditor('{$RECORD_ID}', '{$FILE_NAME|escape:'javascript'}')" style="margin-right: 5px;" title="Редактировать в Nextcloud">
<button type="button" class="btn btn-success btn-small pull-right" onclick="console.log('🔍 DEBUG: RECORD_ID={$RECORD_ID}, FULL_FILE_NAME={$FULL_FILE_NAME|escape:'javascript'}, FILE_NAME={$FILE_NAME|escape:'javascript'}'); console.log('🔍 DEBUG: FULL_FILE_NAME length:', '{$FULL_FILE_NAME|escape:'javascript'}'.length); openNextcloudEditor('{$RECORD_ID}', '{$FULL_FILE_NAME|escape:'javascript'}')" style="margin-right: 5px;" title="Редактировать в Nextcloud">
<i class="fa fa-edit"></i> Редактировать
</button>
</div>

View File

@@ -1,48 +1,59 @@
{*<!--
/*********************************************************************************
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
* ("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}
<div class="col-sm-6 col-lg-6 col-md-6">
<div class="record-header clearfix">
<div class="hidden-sm hidden-xs recordImage bghelpdesk app-{$SELECTED_MENU_CATEGORY}">
<div class="name"><span><strong>{$MODULE_MODEL->getModuleIcon()}</strong></span></div>
</div>
<div class="recordBasicInfo">
<div class="info-row">
<h4>
<span class="recordLabel pushDown" title="{$RECORD->getName()}">
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
{if $FIELD_MODEL->getPermissions()}
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>&nbsp;
{/if}
{/foreach}
</span>
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{*
{assign var=PRIORITY value=$RECORD->get('ticketpriorities')}
{if !empty($PRIORITY)}
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('ticketpriorities')}
<div class="col-lg-7 fieldLabel">
<span class="muted" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('ticketpriorities')}">
{$RECORD->getDisplayValue('ticketpriorities')}
</span>
</div>
</div>
{/if}
*}
</div>
</div>
</div>
{*<!--
/*********************************************************************************
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
* ("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}
<div class="col-sm-6 col-lg-6 col-md-6">
<div class="record-header clearfix">
<div class="hidden-sm hidden-xs recordImage bghelpdesk app-{$SELECTED_MENU_CATEGORY}">
<div class="name"><span><strong>{$MODULE_MODEL->getModuleIcon()}</strong></span></div>
</div>
<div class="recordBasicInfo">
<div class="info-row">
<h4>
<span class="recordLabel pushDown" title="{$RECORD->getName()}">
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
{if $FIELD_MODEL->getPermissions()}
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>&nbsp;
{/if}
{/foreach}
</span>
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки тикета в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('HelpDesk', '{$RECORD->getId()}', '{$RECORD->get("ticket_no")|escape:javascript}')" title="Открыть папку тикета в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
{assign var=PRIORITY value=$RECORD->get('ticketpriorities')}
{if !empty($PRIORITY)}
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('ticketpriorities')}
<div class="col-lg-7 fieldLabel">
<span class="muted" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('ticketpriorities')}">
{$RECORD->getDisplayValue('ticketpriorities')}
</span>
</div>
</div>
{/if}
*}
</div>
</div>
</div>
{/strip}

View File

@@ -39,6 +39,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки счета в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('Invoice', '{$RECORD->getId()}', '{$RECORD->get("invoice_no")|escape:javascript}')" title="Открыть папку счета в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
<div class="row info-row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('contact_id')}

View File

@@ -44,6 +44,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки лида в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('Leads', '{$RECORD->getId()}', '{$RECORD->get("firstname")|escape:javascript}_{$RECORD->get("lastname")|escape:javascript}')" title="Открыть папку лида в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('email')}

View File

@@ -1,73 +1,84 @@
{*<!--
/*********************************************************************************
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
* ("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}
<div class="col-sm-6 col-lg-6 col-md-6">
<div class="record-header clearfix">
<div class="hidden-sm hidden-xs recordImage bgpotentials app-{$SELECTED_MENU_CATEGORY}">
<div class="name"><span><strong>{$MODULE_MODEL->getModuleIcon()}</strong></span></div>
</div>
<div class="recordBasicInfo">
<div class="info-row">
<h4>
<span class="recordLabel pushDown" title="{$RECORD->getName()}">
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
{if $FIELD_MODEL->getPermissions()}
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>&nbsp;
{/if}
{/foreach}
</span>
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{*
{assign var=RELATED_TO value=$RECORD->get('related_to')}
{if !empty($RELATED_TO)}
<div class="row info-row">
<div class="col-lg-7 fieldLabel">
<span class="muted" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->getDisplayValue('related_to')}">
{$RECORD->getDisplayValue('related_to')}</span>
</div>
</div>
{/if}
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('email')}
<div class="col-lg-7 fieldLabel">
<span class="email" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('email')}">
{$RECORD->getDisplayValue("email")}
</span>
</div>
</div>
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('amount')}
<div class="col-lg-7 fieldLabel">
<span class="amount" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('amount')}">
{$RECORD->getDisplayValue("amount")}
</span>
</div>
</div>
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('sales_stage')}
<div class="col-lg-7 fieldLabel">
<span class="salesstage" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('sales_stage')}">{$RECORD->get('sales_stage')}</span>
</div>
</div>
*}
</div>
</div>
</div>
{*<!--
/*********************************************************************************
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
* ("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}
<div class="col-sm-6 col-lg-6 col-md-6">
<div class="record-header clearfix">
<div class="hidden-sm hidden-xs recordImage bgpotentials app-{$SELECTED_MENU_CATEGORY}">
<div class="name"><span><strong>{$MODULE_MODEL->getModuleIcon()}</strong></span></div>
</div>
<div class="recordBasicInfo">
<div class="info-row">
<h4>
<span class="recordLabel pushDown" title="{$RECORD->getName()}">
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
{if $FIELD_MODEL->getPermissions()}
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>&nbsp;
{/if}
{/foreach}
</span>
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки сделки в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('Potentials', '{$RECORD->getId()}', '{$RECORD->get("potentialname")|escape:javascript}')" title="Открыть папку сделки в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
{assign var=RELATED_TO value=$RECORD->get('related_to')}
{if !empty($RELATED_TO)}
<div class="row info-row">
<div class="col-lg-7 fieldLabel">
<span class="muted" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->getDisplayValue('related_to')}">
{$RECORD->getDisplayValue('related_to')}</span>
</div>
</div>
{/if}
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('email')}
<div class="col-lg-7 fieldLabel">
<span class="email" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('email')}">
{$RECORD->getDisplayValue("email")}
</span>
</div>
</div>
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('amount')}
<div class="col-lg-7 fieldLabel">
<span class="amount" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('amount')}">
{$RECORD->getDisplayValue("amount")}
</span>
</div>
</div>
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('sales_stage')}
<div class="col-lg-7 fieldLabel">
<span class="salesstage" title="{vtranslate($FIELD_MODEL->get('label'),$MODULE)} : {$RECORD->get('sales_stage')}">{$RECORD->get('sales_stage')}</span>
</div>
</div>
*}
</div>
</div>
</div>
{/strip}

View File

@@ -38,7 +38,7 @@
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="layouts/v7/lib/nextcloud-editor.js"></script>
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
{assign var=RELATED_TO value=$RECORD->get('linktoaccountscontacts')}

View File

@@ -39,6 +39,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки закупки в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('PurchaseOrder', '{$RECORD->getId()}', '{$RECORD->get("purchaseorder_no")|escape:javascript}')" title="Открыть папку закупки в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
<div class="info-row row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('contact_id')}

View File

@@ -39,6 +39,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки предложения в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('Quotes', '{$RECORD->getId()}', '{$RECORD->get("quote_no")|escape:javascript}')" title="Открыть папку предложения в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
<div class="row info-row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('account_id')}

View File

@@ -39,6 +39,17 @@
</h4>
</div>
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
{* Кнопка открытия папки заказа в Nextcloud *}
<div class="info-row" style="margin-top: 10px;">
<button type="button" class="btn btn-info btn-sm" onclick="openRecordFolder('SalesOrder', '{$RECORD->getId()}', '{$RECORD->get("salesorder_no")|escape:javascript}')" title="Открыть папку заказа в Nextcloud">
<i class="fa fa-folder-open"></i> Папка в Nextcloud
</button>
</div>
{* Подключаем Nextcloud Editor JS *}
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
{*
<div class="row info-row">
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField('contact_id')}

View File

@@ -37,10 +37,16 @@
</div>
<script type="text/javascript">
var workflow_data = {$workflowData|@json_encode};
var workflow_id = {$workflowID};
var maxConnections = {$maxConnections};
var workflow_data = {if isset($workflowData) && $workflowData}{$workflowData|@json_encode}{else}{ldelim}{rdelim}{/if};
var workflow_id = {$workflowID|default:0};
var maxConnections = {$maxConnections|default:0};
var copyHash = {if !empty($COPYHASH)}'{$COPYHASH}'{else}null{/if};
// Диагностика для отладки
if(!workflow_data || typeof workflow_data !== 'object') {
console.error('ОШИБКА: workflow_data не является объектом!', workflow_data);
workflow_data = {ldelim}{rdelim};
}
</script>
<div id='modalWindow' style='display:none;'></div>

View File

@@ -36,7 +36,7 @@
</div>
<script type="text/javascript">
var workflow_data = {$workflowData|@json_encode};
var workflow_data = {if isset($workflowData) && $workflowData}{$workflowData|@json_encode}{else}{ldelim}{rdelim}{/if};
var workflow_id = {$workflowID};
var maxConnections = {$maxConnections};
</script>

View File

@@ -1,4 +1,4 @@
@import url(http://fonts.googleapis.com/css?family=Source+Code+Pro);
@import url(https://fonts.googleapis.com/css?family=Source+Code+Pro);
html.WF_configWindow,
html.WF_configWindow body {
height: 100%;

View File

@@ -501,7 +501,7 @@ div.statLayer {
.vtFooter p {
border-top:none;
}
@import url(http://fonts.googleapis.com/css?family=Source+Code+Pro);
@import url(https://fonts.googleapis.com/css?family=Source+Code+Pro);
input.textfield.taskTitle {
width:60%;

View File

@@ -247,6 +247,13 @@
{/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')}"