Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
484 lines
19 KiB
Markdown
484 lines
19 KiB
Markdown
# 🎨 ИНТЕРФЕЙС РЕДАКТОРА: ПАНЕЛЬ ПЕРЕМЕННЫХ + ONLYOFFICE
|
||
|
||
## 🎯 КОНЦЕПЦИЯ
|
||
|
||
**Двухпанельный интерфейс:**
|
||
- **Слева:** Список полей и переменных (как в PDFMaker)
|
||
- **Справа:** OnlyOffice редактор в iframe
|
||
- **При клике:** Переменная вставляется в документ
|
||
|
||
---
|
||
|
||
## 📐 МАКЕТ ИНТЕРФЕЙСА
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Редактирование шаблона: Претензия [💾 Сохранить] │
|
||
├──────────────────────┬──────────────────────────────────────┤
|
||
│ │ │
|
||
│ ПЕРЕМЕННЫЕ │ ONLYOFFICE РЕДАКТОР │
|
||
│ │ ┌──────────────────────────────┐ │
|
||
│ [🔍 Поиск...] │ │ │ │
|
||
│ │ │ ПРЕТЕНЗИЯ │ │
|
||
│ Прямые поля: │ │ │ │
|
||
│ ├─ projectname │ │ Кому: УК "Жилищник" │ │
|
||
│ ├─ createdtime │ │ От: {CLIENT_NAME} │ │
|
||
│ ├─ cf_1885 │ │ │ │
|
||
│ └─ ... │ │ Дата: {DATE} │ │
|
||
│ │ │ │ │
|
||
│ Связанные модули: │ │ Текст: {CLAIM_TEXT} │ │
|
||
│ ├─ Contacts │ │ │ │
|
||
│ │ ├─ lastname │ │ Сумма: {AMOUNT} ₽ │ │
|
||
│ │ └─ firstname │ │ │ │
|
||
│ ├─ Accounts │ │ │ │
|
||
│ │ └─ accountname │ │ │ │
|
||
│ └─ ... │ │ │ │
|
||
│ │ │ │ │
|
||
│ Функции: │ └──────────────────────────────┘ │
|
||
│ ├─ formatDate() │ │
|
||
│ ├─ formatCurrency() │ │
|
||
│ └─ ... │ │
|
||
│ │ │
|
||
│ [📋 Копировать переменную] │
|
||
│ │ │
|
||
└──────────────────────┴──────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 РЕАЛИЗАЦИЯ
|
||
|
||
### **1. Структура View**
|
||
|
||
```php
|
||
class DocTemplate_Edit_View extends Vtiger_Index_View {
|
||
|
||
public function process(Vtiger_Request $request) {
|
||
$templateId = $request->get('record');
|
||
$template = DocTemplate_Template_Model::getInstanceById($templateId);
|
||
$module = $template->getModule();
|
||
|
||
// Получить список полей (как в PDFMaker)
|
||
$fieldsModel = new PDFMaker_Fields_Model();
|
||
$moduleFields = $fieldsModel->getSelectModuleFields($module);
|
||
|
||
// Получить URL для OnlyOffice
|
||
$onlyOfficeUrl = $this->getOnlyOfficeEditorUrl($template);
|
||
|
||
$viewer = $this->getViewer($request);
|
||
$viewer->assign('MODULE_FIELDS', $moduleFields);
|
||
$viewer->assign('ONLYOFFICE_URL', $onlyOfficeUrl);
|
||
$viewer->assign('TEMPLATE', $template);
|
||
$viewer->assign('MODULE', $module);
|
||
|
||
$viewer->view('Edit.tpl', 'DocTemplate');
|
||
}
|
||
|
||
/**
|
||
* Получить URL для OnlyOffice редактора
|
||
*/
|
||
private function getOnlyOfficeEditorUrl($template) {
|
||
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||
$filename = $template->getFilename();
|
||
$path = $template->getNextcloudPath();
|
||
|
||
// URL для открытия в OnlyOffice через Nextcloud
|
||
$fileUrl = $nextcloudUrl . '/remote.php/dav/files/admin' . $path . $filename;
|
||
|
||
// Получить конфигурацию OnlyOffice Document Server
|
||
$config = [
|
||
'document' => [
|
||
'fileType' => 'docx',
|
||
'key' => md5($template->getId() . time()),
|
||
'title' => $filename,
|
||
'url' => $fileUrl
|
||
],
|
||
'documentType' => 'text',
|
||
'editorConfig' => [
|
||
'mode' => 'edit',
|
||
'callbackUrl' => 'https://crm.clientright.ru/index.php?module=DocTemplate&action=OnlyOfficeCallback'
|
||
]
|
||
];
|
||
|
||
// URL OnlyOffice Document Server
|
||
$onlyOfficeServer = 'https://onlyoffice.clientright.ru'; // или где у вас
|
||
$editorUrl = $onlyOfficeServer . '/web-apps/apps/documenteditor/main/index.html';
|
||
|
||
return $editorUrl . '?config=' . base64_encode(json_encode($config));
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **2. Template (Edit.tpl)**
|
||
|
||
```html
|
||
<div class="doc-template-editor">
|
||
<div class="row">
|
||
<!-- Левая панель: Переменные -->
|
||
<div class="col-md-3 variables-panel">
|
||
<div class="panel panel-default">
|
||
<div class="panel-heading">
|
||
<h4>Переменные</h4>
|
||
<input type="text" class="form-control" id="variable-search"
|
||
placeholder="🔍 Поиск...">
|
||
</div>
|
||
<div class="panel-body" id="variables-list">
|
||
{* Прямые поля модуля *}
|
||
<div class="variables-group">
|
||
<h5>Прямые поля</h5>
|
||
<ul class="list-unstyled">
|
||
{foreach from=$MODULE_FIELDS key=group item=fields}
|
||
{foreach from=$fields key=varName item=varLabel}
|
||
<li class="variable-item"
|
||
data-variable="{$varName}"
|
||
data-label="{$varLabel}">
|
||
<a href="javascript:void(0)"
|
||
onclick="DocTemplate.insertVariable('{$varName}')">
|
||
<strong>{$varName}</strong><br>
|
||
<small>{$varLabel}</small>
|
||
</a>
|
||
</li>
|
||
{/foreach}
|
||
{/foreach}
|
||
</ul>
|
||
</div>
|
||
|
||
{* Связанные модули *}
|
||
<div class="variables-group">
|
||
<h5>Связанные модули</h5>
|
||
{* Здесь будут связанные модули *}
|
||
</div>
|
||
|
||
{* Функции *}
|
||
<div class="variables-group">
|
||
<h5>Функции</h5>
|
||
<ul class="list-unstyled">
|
||
<li class="variable-item">
|
||
<a href="javascript:void(0)"
|
||
onclick="DocTemplate.insertFunction('formatDate')">
|
||
formatDate()
|
||
</a>
|
||
</li>
|
||
<li class="variable-item">
|
||
<a href="javascript:void(0)"
|
||
onclick="DocTemplate.insertFunction('formatCurrency')">
|
||
formatCurrency()
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Правая панель: OnlyOffice редактор -->
|
||
<div class="col-md-9 editor-panel">
|
||
<div class="panel panel-default">
|
||
<div class="panel-heading">
|
||
<h4>Редактор документа</h4>
|
||
<button class="btn btn-primary" onclick="DocTemplate.saveTemplate()">
|
||
💾 Сохранить
|
||
</button>
|
||
</div>
|
||
<div class="panel-body" style="padding: 0;">
|
||
<iframe id="onlyoffice-editor"
|
||
src="{$ONLYOFFICE_URL}"
|
||
width="100%"
|
||
height="800px"
|
||
frameborder="0">
|
||
</iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.variables-panel {
|
||
max-height: 800px;
|
||
overflow-y: auto;
|
||
}
|
||
.variable-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
cursor: pointer;
|
||
}
|
||
.variable-item:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
.variable-item a {
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
.variable-item strong {
|
||
color: #007bff;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### **3. JavaScript: Интеграция с OnlyOffice**
|
||
|
||
```javascript
|
||
DocTemplate = {
|
||
|
||
onlyOfficeEditor: null,
|
||
onlyOfficeAPI: null,
|
||
|
||
/**
|
||
* Инициализация после загрузки iframe
|
||
*/
|
||
init: function() {
|
||
// Ждем загрузки OnlyOffice
|
||
window.addEventListener('message', function(event) {
|
||
if (event.data && event.data.type === 'onlyoffice-ready') {
|
||
DocTemplate.onlyOfficeAPI = event.data.api;
|
||
DocTemplate.onlyOfficeEditor = event.data.editor;
|
||
}
|
||
});
|
||
|
||
// Поиск по переменным
|
||
jQuery('#variable-search').on('input', function() {
|
||
var search = jQuery(this).val().toLowerCase();
|
||
jQuery('.variable-item').each(function() {
|
||
var text = jQuery(this).text().toLowerCase();
|
||
if (text.indexOf(search) !== -1) {
|
||
jQuery(this).show();
|
||
} else {
|
||
jQuery(this).hide();
|
||
}
|
||
});
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Вставка переменной в OnlyOffice
|
||
*/
|
||
insertVariable: function(variableName) {
|
||
// Формат переменной: {VAR_NAME} или $MODULE_FIELDNAME$
|
||
var variableText = '{' + variableName + '}';
|
||
|
||
// Попытка вставить через OnlyOffice API
|
||
if (DocTemplate.onlyOfficeAPI) {
|
||
try {
|
||
var oDocument = DocTemplate.onlyOfficeAPI.GetDocument();
|
||
var oRange = oDocument.GetRange();
|
||
|
||
// Вставить переменную в текущую позицию курсора
|
||
oRange.InsertText(variableText);
|
||
} catch (e) {
|
||
console.error('OnlyOffice API error:', e);
|
||
// Fallback: копировать в буфер обмена
|
||
DocTemplate.copyToClipboard(variableText);
|
||
}
|
||
} else {
|
||
// Fallback: копировать в буфер обмена
|
||
DocTemplate.copyToClipboard(variableText);
|
||
alert('Переменная скопирована в буфер обмена. Вставьте в документ (Ctrl+V)');
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Вставка функции
|
||
*/
|
||
insertFunction: function(functionName) {
|
||
var functionText = '[FUNCTION|' + functionName + '|FUNCTION]';
|
||
DocTemplate.insertVariable(functionText);
|
||
},
|
||
|
||
/**
|
||
* Копирование в буфер обмена
|
||
*/
|
||
copyToClipboard: function(text) {
|
||
var textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textarea);
|
||
},
|
||
|
||
/**
|
||
* Сохранение шаблона
|
||
*/
|
||
saveTemplate: function() {
|
||
if (!DocTemplate.onlyOfficeAPI) {
|
||
alert('Редактор не готов. Подождите...');
|
||
return;
|
||
}
|
||
|
||
// Получить содержимое документа из OnlyOffice
|
||
var oDocument = DocTemplate.onlyOfficeAPI.GetDocument();
|
||
var content = oDocument.GetContent();
|
||
|
||
// Отправить на сервер для сохранения
|
||
AppConnector.request({
|
||
module: 'DocTemplate',
|
||
action: 'SaveTemplateContent',
|
||
record: jQuery('#recordId').val(),
|
||
content: content
|
||
}).then(function(response) {
|
||
if (response.success) {
|
||
Vtiger_Helper_Js.showPnotify('Шаблон сохранен');
|
||
|
||
// Обновить маппинг (найти переменные в документе)
|
||
DocTemplate.updateMapping();
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Обновление маппинга после сохранения
|
||
*/
|
||
updateMapping: function() {
|
||
AppConnector.request({
|
||
module: 'DocTemplate',
|
||
action: 'ExtractVariables',
|
||
record: jQuery('#recordId').val()
|
||
}).then(function(response) {
|
||
if (response.success && response.result.variables.length > 0) {
|
||
// Показать диалог настройки маппинга
|
||
DocTemplate.showMappingDialog(response.result.variables);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Показать диалог настройки маппинга
|
||
*/
|
||
showMappingDialog: function(variables) {
|
||
// Показать модальное окно с настройкой маппинга
|
||
// (как в предыдущем варианте)
|
||
}
|
||
};
|
||
|
||
// Инициализация при загрузке страницы
|
||
jQuery(document).ready(function() {
|
||
DocTemplate.init();
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 🔗 ИНТЕГРАЦИЯ С ONLYOFFICE DOCUMENT SERVER
|
||
|
||
### **Вариант 1: Через Nextcloud (проще)**
|
||
|
||
```php
|
||
// Использовать встроенный OnlyOffice в Nextcloud
|
||
$onlyOfficeUrl = 'https://office.clientright.ru:8443/apps/onlyoffice/' .
|
||
urlencode($path . $filename);
|
||
```
|
||
|
||
**Преимущества:**
|
||
- ✅ Уже настроен
|
||
- ✅ Автоматическое сохранение в Nextcloud
|
||
- ✅ Не нужна отдельная настройка
|
||
|
||
**Недостатки:**
|
||
- ❌ Сложнее получить доступ к API
|
||
- ❌ Ограниченный контроль
|
||
|
||
---
|
||
|
||
### **Вариант 2: Прямая интеграция с OnlyOffice Document Server**
|
||
|
||
```php
|
||
// Настроить OnlyOffice Document Server отдельно
|
||
$onlyOfficeServer = 'https://onlyoffice.clientright.ru';
|
||
|
||
$config = [
|
||
'document' => [
|
||
'fileType' => 'docx',
|
||
'key' => md5($templateId . time()),
|
||
'title' => $filename,
|
||
'url' => $nextcloudFileUrl // URL файла в Nextcloud
|
||
],
|
||
'documentType' => 'text',
|
||
'editorConfig' => [
|
||
'mode' => 'edit',
|
||
'callbackUrl' => 'https://crm.clientright.ru/index.php?module=DocTemplate&action=OnlyOfficeCallback'
|
||
]
|
||
];
|
||
|
||
$editorUrl = $onlyOfficeServer . '/web-apps/apps/documenteditor/main/index.html';
|
||
$fullUrl = $editorUrl . '?config=' . base64_encode(json_encode($config));
|
||
```
|
||
|
||
**Преимущества:**
|
||
- ✅ Полный контроль над API
|
||
- ✅ Можно вставлять переменные программно
|
||
- ✅ Больше возможностей
|
||
|
||
**Недостатки:**
|
||
- ❌ Нужна настройка OnlyOffice Document Server
|
||
- ❌ Сложнее реализация
|
||
|
||
---
|
||
|
||
## 🎯 РЕКОМЕНДУЕМЫЙ ПОДХОД
|
||
|
||
### **Гибридный вариант:**
|
||
|
||
1. **Использовать OnlyOffice через Nextcloud** (проще)
|
||
2. **Для вставки переменных:** Копировать в буфер обмена + уведомление
|
||
3. **После сохранения:** Автоматически парсить DOCX и настраивать маппинг
|
||
|
||
**Алгоритм:**
|
||
```
|
||
1. Пользователь кликает на переменную слева
|
||
↓
|
||
2. Переменная копируется в буфер обмена
|
||
↓
|
||
3. Показывается уведомление: "Переменная скопирована. Вставьте в документ (Ctrl+V)"
|
||
↓
|
||
4. Пользователь вставляет в OnlyOffice
|
||
↓
|
||
5. Сохраняет документ
|
||
↓
|
||
6. CRM автоматически находит переменные и настраивает маппинг
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 АЛЬТЕРНАТИВА: ПРЯМАЯ ВСТАВКА ЧЕРЕЗ API
|
||
|
||
Если OnlyOffice Document Server настроен отдельно:
|
||
|
||
```javascript
|
||
insertVariable: function(variableName) {
|
||
if (window.AscDesktopEditor) {
|
||
// Desktop версия OnlyOffice
|
||
var oDocument = Api.GetDocument();
|
||
var oRange = oDocument.GetRange();
|
||
oRange.InsertText('{' + variableName + '}');
|
||
} else if (DocTemplate.onlyOfficeAPI) {
|
||
// Web версия OnlyOffice
|
||
var oDocument = DocTemplate.onlyOfficeAPI.GetDocument();
|
||
var oRange = oDocument.GetRange();
|
||
oRange.InsertText('{' + variableName + '}');
|
||
} else {
|
||
// Fallback: копирование в буфер
|
||
DocTemplate.copyToClipboard('{' + variableName + '}');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ ИТОГОВОЕ РЕШЕНИЕ
|
||
|
||
**Интерфейс:**
|
||
- ✅ Двухпанельный (переменные слева, редактор справа)
|
||
- ✅ Клик по переменной → вставка в документ
|
||
- ✅ Автоматическое обнаружение переменных после сохранения
|
||
|
||
**Реализация:**
|
||
1. Начать с копирования в буфер обмена (проще)
|
||
2. Потом добавить прямую вставку через API (если нужно)
|
||
|
||
**Готов реализовать такой интерфейс!** 🚀
|