feat: Добавлен инструмент генерации документов для AI Ассистента
- Создан API create_document_with_text.php для создания DOCX/XLSX/PPTX с текстом от AI - Поддержка Markdown форматирования (заголовки, жирный, курсив, списки, код) - Установлен PHPWord для красивого форматирования документов - Исправлены пути сохранения (crm2/CRM_Active_Files/... без /crm/ в начале) - Замена пробелов на подчеркивания в именах папок - Создана документация для AI и разработчиков - Добавлены API для работы с шаблонами Nextcloud
96
AI_DRAWER_DEBUG.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 🔍 Диагностика проблемы AI Drawer
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
Ошибка: "Ошибка при получении ответа. Попробуйте еще раз."
|
||||||
|
|
||||||
|
## Что проверить
|
||||||
|
|
||||||
|
### 1. Проверить формат сообщения от n8n
|
||||||
|
|
||||||
|
n8n может публиковать сообщение в двух форматах:
|
||||||
|
|
||||||
|
**Формат 1 (просто текст):**
|
||||||
|
```
|
||||||
|
"Текст ответа от AI"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Формат 2 (JSON объект):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task_id": "task-123",
|
||||||
|
"response": "Текст ответа",
|
||||||
|
"status": "completed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Проверить Redis ключ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
|
||||||
|
GET "ai:response:cache:task-691209e225894-1762789858"
|
||||||
|
```
|
||||||
|
|
||||||
|
Если ключ есть → ответ сохранен, но SSE не получил
|
||||||
|
Если ключа нет → n8n не сохраняет в ключ (нужно настроить)
|
||||||
|
|
||||||
|
### 3. Проверить логи SSE
|
||||||
|
|
||||||
|
В консоли браузера должны быть логи:
|
||||||
|
- `AI Drawer: SSE connection opened`
|
||||||
|
- `AI Drawer: Received response via SSE`
|
||||||
|
|
||||||
|
Если их нет → SSE не подключается
|
||||||
|
|
||||||
|
### 4. Проверить публикацию в канал
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
|
||||||
|
MONITOR
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем отправьте сообщение в AI Drawer - должны видеть PUBLISH команду
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
### Если n8n публикует только в канал (без ключа):
|
||||||
|
|
||||||
|
1. **Добавьте Redis SET ноду в n8n** перед PUBLISH:
|
||||||
|
- Operation: `Set`
|
||||||
|
- Key: `ai:response:cache:{{ $json.taskId }}`
|
||||||
|
- Value: JSON с ответом
|
||||||
|
- TTL: 300 секунд
|
||||||
|
|
||||||
|
2. **Или** используйте текущий код - SSE endpoint сам сохранит в ключ когда получит из канала
|
||||||
|
|
||||||
|
### Если SSE не подключается:
|
||||||
|
|
||||||
|
1. Проверьте что `/aiassist/ai_sse.php` доступен
|
||||||
|
2. Проверьте логи PHP на ошибки
|
||||||
|
3. Проверьте консоль браузера на ошибки CORS/сети
|
||||||
|
|
||||||
|
## Текущая архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
n8n → Redis PUBLISH (канал) → SSE endpoint получает → сохраняет в ключ → отправляет браузеру
|
||||||
|
↓
|
||||||
|
Если SSE не получил → fallback проверяет ключ
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что исправлено
|
||||||
|
|
||||||
|
✅ SSE endpoint теперь:
|
||||||
|
- Принимает и JSON и простой текст
|
||||||
|
- Сохраняет ответ в Redis ключ при получении
|
||||||
|
- Проверяет ключ при подключении (на случай если ответ уже есть)
|
||||||
|
|
||||||
|
✅ JavaScript теперь:
|
||||||
|
- Не вызывает fallback если уже получил ответ
|
||||||
|
- Проверяет Redis ключ периодически если SSE не работает
|
||||||
|
- Логирует все действия для отладки
|
||||||
|
|
||||||
|
## Следующие шаги
|
||||||
|
|
||||||
|
1. Проверьте что n8n сохраняет в ключ `ai:response:cache:{taskId}` ПЕРЕД публикацией
|
||||||
|
2. Проверьте логи в консоли браузера
|
||||||
|
3. Проверьте логи PHP (error_log)
|
||||||
|
|
||||||
@@ -87,3 +87,4 @@ tail -f /var/www/fastuser/data/www/crm.clientright.ru/logs/api_attach_documents.
|
|||||||
|
|
||||||
## 🎯 Готово к использованию в n8n!
|
## 🎯 Готово к использованию в n8n!
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -234,3 +234,5 @@ Contact: 396625
|
|||||||
|
|
||||||
**Готово к использованию!** 🎉
|
**Готово к использованию!** 🎉
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
213
LOGS/AI_DOCUMENT_GENERATION_SESSION.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# 📝 Лог сессии: Реализация генерации документов для AI Ассистента
|
||||||
|
|
||||||
|
**Дата:** 2025-01-12
|
||||||
|
**Участники:** Фёдор, AI Assistant
|
||||||
|
**Тема:** Создание инструмента для генерации документов из шаблонов и с текстом от AI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Цель сессии
|
||||||
|
|
||||||
|
Реализовать функционал генерации документов (претензий, исков, жалоб, ходатайств) для AI Ассистента с возможностью использования шаблонов Nextcloud и форматирования Markdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Обсужденные вопросы
|
||||||
|
|
||||||
|
### 1. Шаблонизация документов в Nextcloud
|
||||||
|
|
||||||
|
**Вопрос:** Как настроить шаблоны в Nextcloud? Есть ли понятный механизм шаблонизирования?
|
||||||
|
|
||||||
|
**Анализ:**
|
||||||
|
- Проверен API Nextcloud Direct Editing - endpoint `/templates` не существует
|
||||||
|
- Найдена папка `/Templates/` в корне пользователя admin
|
||||||
|
- ONLYOFFICE хранит "Общие шаблоны" отдельно от обычной папки Templates
|
||||||
|
- Шаблоны доступны через WebDAV PROPFIND
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
- Использовать WebDAV для получения списка шаблонов
|
||||||
|
- Создать API для работы с шаблонами через WebDAV
|
||||||
|
- Поддержать заполнение переменных через PHPWord
|
||||||
|
|
||||||
|
### 2. Процесс создания документов AI Ассистентом
|
||||||
|
|
||||||
|
**Вопрос:** Как AI Ассистент будет создавать документы?
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Пользователь просит создать документ
|
||||||
|
2. AI Drawer отправляет запрос в n8n
|
||||||
|
3. n8n → GPT-4 анализирует запрос и генерирует текст
|
||||||
|
4. n8n вызывает API создания документа
|
||||||
|
5. API создает DOCX с текстом (поддержка Markdown форматирования)
|
||||||
|
6. Документ сохраняется в S3 в папку проекта
|
||||||
|
7. Возвращается ссылка на редактирование в OnlyOffice
|
||||||
|
|
||||||
|
### 3. Форматирование документов
|
||||||
|
|
||||||
|
**Вопрос:** Можно ли сделать красивое форматирование документов?
|
||||||
|
|
||||||
|
**Решение:** ✅ Да! Реализована поддержка Markdown:
|
||||||
|
- Заголовки: `# H1`, `## H2`, `### H3`
|
||||||
|
- Жирный: `**текст**` или `__текст__`
|
||||||
|
- Курсив: `*текст*` или `_текст_`
|
||||||
|
- Код: `` `текст` ``
|
||||||
|
- Маркированные списки: `- пункт` или `* пункт`
|
||||||
|
- Нумерованные списки: `1. пункт`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Реализованные компоненты
|
||||||
|
|
||||||
|
### 1. API создания документов с текстом
|
||||||
|
|
||||||
|
**Файл:** `/crm_extensions/file_storage/api/create_document_with_text.php`
|
||||||
|
|
||||||
|
**Функционал:**
|
||||||
|
- Создает DOCX/XLSX/PPTX с текстом от AI
|
||||||
|
- Поддержка Markdown форматирования
|
||||||
|
- Сохранение в S3 в правильную папку проекта
|
||||||
|
- Возврат ссылки на редактирование в OnlyOffice
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Поддержка JSON POST запросов
|
||||||
|
- Fallback на простой DOCX если PHPWord недоступен
|
||||||
|
- Правильная обработка пробелов (замена на подчеркивания)
|
||||||
|
- Правильный путь: `crm2/CRM_Active_Files/Documents/...` (без `/crm/` в начале)
|
||||||
|
|
||||||
|
### 2. API создания документов из шаблонов
|
||||||
|
|
||||||
|
**Файл:** `/crm_extensions/file_storage/api/create_from_template.php`
|
||||||
|
|
||||||
|
**Функционал:**
|
||||||
|
- Скачивает шаблон из Nextcloud через WebDAV
|
||||||
|
- Заполняет переменные через PHPWord
|
||||||
|
- Сохраняет готовый документ в папку проекта
|
||||||
|
|
||||||
|
### 3. API получения списка шаблонов
|
||||||
|
|
||||||
|
**Файл:** `/crm_extensions/file_storage/api/list_templates.php`
|
||||||
|
|
||||||
|
**Функционал:**
|
||||||
|
- Получает список шаблонов из Nextcloud через WebDAV PROPFIND
|
||||||
|
- Фильтрует только Office файлы
|
||||||
|
- Возвращает JSON с метаданными
|
||||||
|
|
||||||
|
### 4. Установка PHPWord
|
||||||
|
|
||||||
|
**Команда:**
|
||||||
|
```bash
|
||||||
|
composer require phpoffice/phpword
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
- ✅ PHPWord 1.4.0 установлен
|
||||||
|
- ✅ Поддержка форматирования Markdown
|
||||||
|
- ✅ Красивое оформление документов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Исправленные проблемы
|
||||||
|
|
||||||
|
### Проблема 1: PHPWord не установлен
|
||||||
|
- **Решение:** Установлен через composer
|
||||||
|
- **Дополнительно:** Добавлен fallback на простой DOCX через ZIP
|
||||||
|
|
||||||
|
### Проблема 2: JSON POST не обрабатывался
|
||||||
|
- **Решение:** Добавлена проверка Content-Type и парсинг JSON из php://input
|
||||||
|
|
||||||
|
### Проблема 3: Неправильный путь к файлам
|
||||||
|
- **Было:** `/crm/crm2/CRM_Active_Files/...`
|
||||||
|
- **Стало:** `crm2/CRM_Active_Files/...`
|
||||||
|
- **Решение:** Исправлен путь в `create_document_with_text.php`
|
||||||
|
|
||||||
|
### Проблема 4: Пробелы в именах папок
|
||||||
|
- **Было:** `Крылова ГБУ ЖИЛИЩНИК...`
|
||||||
|
- **Стало:** `Крылова_ГБУ_ЖИЛИЩНИК...`
|
||||||
|
- **Решение:** Добавлена замена пробелов на подчеркивания в `recordName`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Созданная документация
|
||||||
|
|
||||||
|
1. **AI_DOCUMENT_TOOL_INSTRUCTION.md** - Инструкция для AI Ассистента
|
||||||
|
2. **AI_DOCUMENT_GENERATION_FLOW.md** - Описание процесса создания документов
|
||||||
|
3. **MARKDOWN_FORMATTING.md** - Справочник по Markdown форматированию
|
||||||
|
4. **NEXTCLOUD_TEMPLATES.md** - Работа с шаблонами Nextcloud
|
||||||
|
5. **NEXTCLOUD_TEMPLATES_API_ANALYSIS.md** - Анализ API шаблонов
|
||||||
|
6. **ONLYOFFICE_TEMPLATES_ANALYSIS.md** - Анализ шаблонов ONLYOFFICE
|
||||||
|
7. **N8N_HTTP_REQUEST_CURL.md** - cURL команды для n8n
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Результаты
|
||||||
|
|
||||||
|
### ✅ Реализовано:
|
||||||
|
|
||||||
|
1. **API создания документов** - работает, протестирован
|
||||||
|
2. **Поддержка Markdown** - заголовки, жирный, курсив, списки, код
|
||||||
|
3. **Правильные пути** - документы сохраняются в правильную структуру
|
||||||
|
4. **Обработка пробелов** - автоматическая замена на подчеркивания
|
||||||
|
5. **PHPWord установлен** - красивое форматирование документов
|
||||||
|
6. **Документация** - полная инструкция для AI и разработчиков
|
||||||
|
|
||||||
|
### 📊 Статистика:
|
||||||
|
|
||||||
|
- **Создано файлов:** 7+ (API, документация)
|
||||||
|
- **Установлено библиотек:** PHPWord 1.4.0
|
||||||
|
- **Исправлено проблем:** 4
|
||||||
|
- **Поддерживаемых форматов:** DOCX, XLSX, PPTX
|
||||||
|
- **Поддерживаемых элементов Markdown:** 6 типов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Следующие шаги
|
||||||
|
|
||||||
|
1. **Настроить в n8n:**
|
||||||
|
- Добавить HTTP Request ноду для создания документов
|
||||||
|
- Подключить к AI workflow
|
||||||
|
- Протестировать создание документов
|
||||||
|
|
||||||
|
2. **Улучшения (опционально):**
|
||||||
|
- Добавить поддержку шаблонов с переменными
|
||||||
|
- Расширенное форматирование (таблицы, изображения)
|
||||||
|
- Автоматическое определение типа документа
|
||||||
|
|
||||||
|
3. **Интеграция с AI:**
|
||||||
|
- Добавить инструмент в список доступных для AI
|
||||||
|
- Протестировать генерацию документов через AI Drawer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Измененные файлы
|
||||||
|
|
||||||
|
### Новые файлы:
|
||||||
|
- `/crm_extensions/file_storage/api/create_document_with_text.php`
|
||||||
|
- `/crm_extensions/file_storage/api/create_from_template.php`
|
||||||
|
- `/crm_extensions/file_storage/api/list_templates.php`
|
||||||
|
- `/crm_extensions/file_storage/docs/AI_DOCUMENT_TOOL_INSTRUCTION.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/AI_DOCUMENT_GENERATION_FLOW.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/MARKDOWN_FORMATTING.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/NEXTCLOUD_TEMPLATES.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/NEXTCLOUD_TEMPLATES_API_ANALYSIS.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/ONLYOFFICE_TEMPLATES_ANALYSIS.md`
|
||||||
|
- `/crm_extensions/file_storage/docs/N8N_HTTP_REQUEST_CURL.md`
|
||||||
|
|
||||||
|
### Обновленные файлы:
|
||||||
|
- `composer.json` - добавлен phpoffice/phpword
|
||||||
|
- `composer.lock` - обновлен после установки PHPWord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Ключевые решения
|
||||||
|
|
||||||
|
1. **Использование Markdown** - стандартный синтаксис, понятный AI
|
||||||
|
2. **WebDAV вместо API** - надежнее, работает всегда
|
||||||
|
3. **Fallback механизм** - работает даже без PHPWord
|
||||||
|
4. **Правильная структура путей** - соответствует существующей системе
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Статус: Готово к использованию
|
||||||
|
|
||||||
|
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.
|
||||||
|
|
||||||
89
NEXTCLOUD_BUTTON_FIX_REDIS.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# 🐛 FIX: Кнопка "Через Nextcloud" → Ошибка подключения к API
|
||||||
|
|
||||||
|
**Дата:** 2 ноября 2025
|
||||||
|
**Проблема:** Кнопка "📚 Через Nextcloud" показывала ошибку "Ошибка подключения к API"
|
||||||
|
|
||||||
|
## 🔍 Диагностика
|
||||||
|
|
||||||
|
### Симптомы:
|
||||||
|
1. ✅ `nextcloud_open.php` **работал в CLI** (возвращал правильный JSON)
|
||||||
|
2. ❌ Через веб (curl/браузер) возвращал **HTTP 500 (пустой ответ)**
|
||||||
|
3. ❌ JavaScript fetch() получал пустой ответ → показывал ошибку
|
||||||
|
|
||||||
|
### Причина:
|
||||||
|
**Redis PHP extension** был установлен только для PHP 7.2, а Apache использовал **PHP 7.3**!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP CLI (работало):
|
||||||
|
php -v # PHP 7.2 (имеет redis extension)
|
||||||
|
|
||||||
|
# Apache (не работало):
|
||||||
|
phpinfo() # PHP 7.3 (НЕТ redis extension!)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Решение
|
||||||
|
|
||||||
|
### 1. Обнаружили модуль .so:
|
||||||
|
```bash
|
||||||
|
find /opt/php73 -name "redis.so"
|
||||||
|
# /opt/php73/lib/php/extensions/no-debug-non-zts-20180731/redis.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Создали конфиг:
|
||||||
|
```bash
|
||||||
|
echo "extension=redis.so" > /opt/php73/mods-available/redis.ini
|
||||||
|
ln -s /opt/php73/mods-available/redis.ini /opt/php73/conf.d/redis.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Перезапустили Apache:
|
||||||
|
```bash
|
||||||
|
systemctl restart apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка:
|
||||||
|
```bash
|
||||||
|
curl https://crm.clientright.ru/crm_extensions/file_storage/api/nextcloud_open.php?recordId=395695
|
||||||
|
|
||||||
|
# Ответ:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"fileId": 115163,
|
||||||
|
"redirectUrl": "https://office.clientright.ru:8443/apps/files/files/115163?...",
|
||||||
|
"source": "redis"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Результат
|
||||||
|
|
||||||
|
✅ Кнопка "📚 Через Nextcloud" **РАБОТАЕТ**
|
||||||
|
✅ FileID получается из **Redis** (быстро!)
|
||||||
|
✅ Файлы открываются в **OnlyOffice через Nextcloud**
|
||||||
|
|
||||||
|
## 📂 Изменённые файлы
|
||||||
|
|
||||||
|
- `/opt/php73/mods-available/redis.ini` (создан)
|
||||||
|
- `/opt/php73/conf.d/redis.ini` (symlink создан)
|
||||||
|
- `nextcloud_open.php` (оптимизирован, убрана PROPFIND fallback логика)
|
||||||
|
|
||||||
|
## 🧪 Проверка других кнопок
|
||||||
|
|
||||||
|
| Кнопка | Статус | Примечание |
|
||||||
|
|--------|--------|------------|
|
||||||
|
| ⚡ **Быстро** | ✅ Работает | S3 → OnlyOffice Standalone |
|
||||||
|
| 📚 **Через Nextcloud** | ✅ Работает | Redis → Nextcloud → OnlyOffice |
|
||||||
|
| 📁 **Папка в Nextcloud** | ✅ Работает | Открывает папку проекта |
|
||||||
|
| 📄 **Скачать** | ✅ Работает | Прямая ссылка S3 |
|
||||||
|
|
||||||
|
## 🔧 Для проверки в будущем:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка модулей PHP 7.3:
|
||||||
|
curl -s "https://crm.clientright.ru/crm_extensions/file_storage/api/test_modules.php"
|
||||||
|
# Должно показать: {"mysqli":true,"redis":true,"json":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Автор:** AI Assistant (Claude Sonnet 4.5)
|
||||||
|
**Время исправления:** ~2 часа (большая часть на диагностику)
|
||||||
|
**Сложность:** ⭐⭐⭐ (3/5) - нетривиальная проблема с разными версиями PHP
|
||||||
138
NEXTCLOUD_FOLDER_BUTTONS_FIX.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 🔧 ИСПРАВЛЕНИЕ: Кнопки "Папка в Nextcloud" не работали
|
||||||
|
|
||||||
|
## ❌ ПРОБЛЕМА:
|
||||||
|
В модулях CRM кнопка **"Папка в Nextcloud"** была неактивна и не реагировала на клики.
|
||||||
|
|
||||||
|
## 🔍 ПРИЧИНА:
|
||||||
|
В JavaScript файле `crm_extensions/nextcloud_editor/js/nextcloud-editor.js` **отсутствовали функции**, которые вызывались из шаблонов:
|
||||||
|
|
||||||
|
**Было в JS:**
|
||||||
|
- ✅ `openProjectFolder()` — ТОЛЬКО ДЛЯ Project
|
||||||
|
|
||||||
|
**Вызывалось из шаблонов, но НЕ СУЩЕСТВОВАЛО:**
|
||||||
|
- ❌ `openRecordFolder()` — для HelpDesk, Invoice, SalesOrder, PurchaseOrder, Leads, Quotes, Potentials
|
||||||
|
- ❌ `openAccountFolder()` — для Accounts
|
||||||
|
- ❌ `openContactFolder()` — для Contacts
|
||||||
|
|
||||||
|
## ✅ РЕШЕНИЕ:
|
||||||
|
|
||||||
|
### 1. Добавлена универсальная функция `openRecordFolder()`
|
||||||
|
```javascript
|
||||||
|
function openRecordFolder(moduleName, recordId, recordName) {
|
||||||
|
// Нормализация имени (убираем кавычки, заменяем пробелы)
|
||||||
|
if (recordName) {
|
||||||
|
recordName = recordName.replace(/"/g, '_').replace(/\s+/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем путь к папке
|
||||||
|
const folderName = recordName ? `${recordName}_${recordId}` : `${moduleName.toLowerCase()}_${recordId}`;
|
||||||
|
const encodedFolderName = encodeURIComponent(folderName);
|
||||||
|
const nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
|
||||||
|
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/${moduleName}/${encodedFolderName}`;
|
||||||
|
|
||||||
|
// Открываем в новом окне
|
||||||
|
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Используется в модулях:**
|
||||||
|
- HelpDesk (тикеты)
|
||||||
|
- Invoice (счета)
|
||||||
|
- SalesOrder (заказы)
|
||||||
|
- PurchaseOrder (закупки)
|
||||||
|
- Leads (лиды)
|
||||||
|
- Quotes (предложения)
|
||||||
|
- Potentials (сделки)
|
||||||
|
|
||||||
|
### 2. Добавлена специализированная функция `openAccountFolder()`
|
||||||
|
```javascript
|
||||||
|
function openAccountFolder(accountId, accountName) {
|
||||||
|
// Нормализация имени контрагента
|
||||||
|
if (accountName) {
|
||||||
|
accountName = accountName.replace(/"/g, '_').replace(/\s+/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderName = accountName ? `${accountName}_${accountId}` : `account_${accountId}`;
|
||||||
|
const encodedFolderName = encodeURIComponent(folderName);
|
||||||
|
|
||||||
|
const folderUrl = `https://office.clientright.ru:8443/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Accounts/${encodedFolderName}`;
|
||||||
|
|
||||||
|
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Используется в модуле:**
|
||||||
|
- Accounts (контрагенты)
|
||||||
|
|
||||||
|
### 3. Добавлена специализированная функция `openContactFolder()`
|
||||||
|
```javascript
|
||||||
|
function openContactFolder(contactId, firstName, lastName) {
|
||||||
|
// Составление имени из firstName и lastName
|
||||||
|
let contactName = '';
|
||||||
|
if (firstName || lastName) {
|
||||||
|
contactName = `${firstName || ''}_${lastName || ''}`.replace(/^_+|_+$/g, '');
|
||||||
|
contactName = contactName.replace(/"/g, '_').replace(/\s+/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderName = contactName ? `${contactName}_${contactId}` : `contact_${contactId}`;
|
||||||
|
const encodedFolderName = encodeURIComponent(folderName);
|
||||||
|
|
||||||
|
const folderUrl = `https://office.clientright.ru:8443/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Contacts/${encodedFolderName}`;
|
||||||
|
|
||||||
|
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Используется в модуле:**
|
||||||
|
- Contacts (контакты)
|
||||||
|
|
||||||
|
### 4. Очищен кеш Smarty
|
||||||
|
```bash
|
||||||
|
rm -rf test/templates_c/v7/*.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 ПРОВЕРКА:
|
||||||
|
|
||||||
|
### Функции в JS:
|
||||||
|
```bash
|
||||||
|
grep -n "^function open.*Folder" crm_extensions/nextcloud_editor/js/nextcloud-editor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
```
|
||||||
|
9:function openRecordFolder(moduleName, recordId, recordName) {
|
||||||
|
37:function openProjectFolder(projectId, projectName) {
|
||||||
|
65:function openAccountFolder(accountId, accountName) {
|
||||||
|
87:function openContactFolder(contactId, firstName, lastName) {
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **ВСЕ 4 ФУНКЦИИ НА МЕСТЕ!**
|
||||||
|
|
||||||
|
### Где используются:
|
||||||
|
|
||||||
|
| Модуль | Функция | Параметры |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| HelpDesk | `openRecordFolder()` | 'HelpDesk', recordId, ticket_no |
|
||||||
|
| Invoice | `openRecordFolder()` | 'Invoice', recordId, invoice_no |
|
||||||
|
| SalesOrder | `openRecordFolder()` | 'SalesOrder', recordId, salesorder_no |
|
||||||
|
| PurchaseOrder | `openRecordFolder()` | 'PurchaseOrder', recordId, purchaseorder_no |
|
||||||
|
| Leads | `openRecordFolder()` | 'Leads', recordId, firstname_lastname |
|
||||||
|
| Quotes | `openRecordFolder()` | 'Quotes', recordId, quote_no |
|
||||||
|
| Potentials | `openRecordFolder()` | 'Potentials', recordId, potentialname |
|
||||||
|
| Accounts | `openAccountFolder()` | accountId, accountname |
|
||||||
|
| Contacts | `openContactFolder()` | contactId, firstname, lastname |
|
||||||
|
| Project | `openProjectFolder()` | projectId, projectname |
|
||||||
|
|
||||||
|
## 🎯 РЕЗУЛЬТАТ:
|
||||||
|
|
||||||
|
✅ **КНОПКИ "Папка в Nextcloud" ТЕПЕРЬ РАБОТАЮТ ВО ВСЕХ МОДУЛЯХ!**
|
||||||
|
|
||||||
|
При клике на кнопку открывается **новое окно** с Nextcloud, где отображается папка соответствующей записи CRM.
|
||||||
|
|
||||||
|
## 📁 ИЗМЕНЕННЫЕ ФАЙЛЫ:
|
||||||
|
- `crm_extensions/nextcloud_editor/js/nextcloud-editor.js` — добавлены 3 недостающие функции
|
||||||
|
- `test/templates_c/v7/*.php` — очищен кеш (автоматически пересоздастся)
|
||||||
|
|
||||||
|
## 📅 ДАТА ИСПРАВЛЕНИЯ:
|
||||||
|
02.11.2025
|
||||||
111
PROJECT_390983_FIXED.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# ✅ ПРОЕКТ 390983 - ВСЁ ИСПРАВЛЕНО!
|
||||||
|
|
||||||
|
## 📊 ИТОГОВАЯ СТАТИСТИКА:
|
||||||
|
|
||||||
|
**БЫЛО:**
|
||||||
|
- ❌ Все файлы недоступны (HTTP 403/404)
|
||||||
|
- ❌ Кнопки не работают
|
||||||
|
- ❌ Пути битые
|
||||||
|
|
||||||
|
**СТАЛО:**
|
||||||
|
- ✅ 8 файлов полностью работают
|
||||||
|
- ✅ Все кнопки работают
|
||||||
|
- ✅ Пути исправлены
|
||||||
|
|
||||||
|
## 🔧 ИСПРАВЛЕННЫЕ ПРОБЛЕМЫ:
|
||||||
|
|
||||||
|
### 1. Кнопка "📁 Папка в Nextcloud" не работала
|
||||||
|
**Проблема:** Отсутствовали JS функции
|
||||||
|
**Решение:** Добавлены:
|
||||||
|
- `openRecordFolder(moduleName, recordId, recordName)`
|
||||||
|
- `openAccountFolder(accountId, accountName)`
|
||||||
|
- `openContactFolder(contactId, firstName, lastName)`
|
||||||
|
- `openProjectFolder(projectId, projectName)`
|
||||||
|
|
||||||
|
### 2. Файлы в неправильной папке
|
||||||
|
**Проблема:** Файлы были в `Documents/Макарова...` вместо `Documents/Project/Макарова...`
|
||||||
|
**Решение:** Скопированы в правильную папку
|
||||||
|
|
||||||
|
### 3. Файлы были приватные
|
||||||
|
**Проблема:** ACL не позволял публичный доступ
|
||||||
|
**Решение:** Установлен `public-read` для всех файлов
|
||||||
|
|
||||||
|
### 4. HTML Entities в путях
|
||||||
|
**Проблема:** В БД были `М...` вместо нормальных букв
|
||||||
|
**Решение:** Обновлены пути с правильным UTF-8
|
||||||
|
|
||||||
|
### 5. Пробелы не закодированы
|
||||||
|
**Проблема:** URL содержали пробелы вместо `%20`
|
||||||
|
**Решение:** Сохранены правильно URL-encoded пути
|
||||||
|
|
||||||
|
### 6. Кнопка "📚 Через Nextcloud" не работала
|
||||||
|
**Проблема:** Двойная кодировка при передаче URL из Smarty в JavaScript
|
||||||
|
**Решение:** Теперь `nextcloud_open.php` получает filename из БД по recordId
|
||||||
|
|
||||||
|
## 🎯 ФАЙЛЫ ПРОЕКТА 390983:
|
||||||
|
|
||||||
|
| ID | Файл | Статус |
|
||||||
|
|----|------|--------|
|
||||||
|
| 390986 | Договор | ✅ HTTP 200 |
|
||||||
|
| 390988 | Подтверждение оплаты | ✅ HTTP 200 |
|
||||||
|
| 390990 | Претензия | ✅ HTTP 200 |
|
||||||
|
| 390992 | Ответ на претензию | ✅ HTTP 200 |
|
||||||
|
| 390994 | Прочие документы | ✅ HTTP 200 |
|
||||||
|
| 390996 | 7 заявление потребителя | ✅ HTTP 200 |
|
||||||
|
| 391199 | 11 Доказательство соблюдения | ✅ HTTP 200 |
|
||||||
|
| 395695 | Исковое заявление (проект) | ✅ HTTP 200 |
|
||||||
|
| 396839 | Счёт и акт Аэрофлот | ❌ Отсутствует |
|
||||||
|
| 396840 | analytical_report | ❌ Отсутствует |
|
||||||
|
|
||||||
|
**ИТОГО: 8/10 (80%) восстановлено**
|
||||||
|
|
||||||
|
## 🚀 ЧТО ТЕПЕРЬ РАБОТАЕТ:
|
||||||
|
|
||||||
|
### ⚡ Кнопка "Быстро" (editInNextcloud)
|
||||||
|
- ✅ Открывает файлы прямо из S3 в OnlyOffice
|
||||||
|
- ✅ Без Nextcloud — быстрее!
|
||||||
|
- ✅ Работает идеально
|
||||||
|
- **РЕКОМЕНДУЕТСЯ ИСПОЛЬЗОВАТЬ**
|
||||||
|
|
||||||
|
### 📚 Кнопка "Через Nextcloud" (openViaNextcloud)
|
||||||
|
- ✅ ИСПРАВЛЕНА! Теперь работает
|
||||||
|
- Получает filename из БД (нет проблем с кодировкой)
|
||||||
|
- Открывает файл в Nextcloud Files UI
|
||||||
|
- Доступно версионирование
|
||||||
|
|
||||||
|
### 📄 Кнопка "Скачать"
|
||||||
|
- ✅ Работает для всех 8 файлов
|
||||||
|
- Прямая ссылка на S3
|
||||||
|
|
||||||
|
### 📁 Кнопка "Папка в Nextcloud"
|
||||||
|
- ✅ ИСПРАВЛЕНА во всех модулях!
|
||||||
|
- Открывает папку записи в Nextcloud
|
||||||
|
|
||||||
|
## 📝 ТЕХНИЧЕСКИЕ ДЕТАЛИ:
|
||||||
|
|
||||||
|
### Файлы в S3:
|
||||||
|
```
|
||||||
|
s3://f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/
|
||||||
|
crm2/CRM_Active_Files/Documents/Project/
|
||||||
|
Макарова_ИП_Большакова_Инна_Борисовна_390983/
|
||||||
|
├─ Договор_390986.pdf
|
||||||
|
├─ Подтверждение_оплаты_390988.pdf
|
||||||
|
├─ Претензия_390990.pdf
|
||||||
|
├─ Ответ_на_претензию_390992.pdf
|
||||||
|
├─ Прочие_документы_390994.pdf
|
||||||
|
├─ 7 заявление потребителя_390996.pdf
|
||||||
|
├─ 11 Доказательство соблюдения претензионного порядк_391199.pdf
|
||||||
|
└─ Исковое заявление (проект)_395695.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
### В БД (vtiger_notes):
|
||||||
|
- `filename` = полный URL-encoded S3 URL
|
||||||
|
- `s3_key` = путь без домена
|
||||||
|
- `s3_bucket` = bucket ID
|
||||||
|
- `filelocationtype` = 'E' (External)
|
||||||
|
|
||||||
|
## 📅 ДАТА ИСПРАВЛЕНИЯ:
|
||||||
|
02.11.2025
|
||||||
|
|
||||||
|
## 🎉 РЕЗУЛЬТАТ:
|
||||||
|
**ВСЁ РАБОТАЕТ! МОЖНО ИСПОЛЬЗОВАТЬ!**
|
||||||
@@ -248,3 +248,4 @@ if ($result && $result['success'] && isset($result['results'])) {
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,53 @@ try {
|
|||||||
|
|
||||||
error_log("Callback: Updated task {$taskId}, affected rows: {$affected}");
|
error_log("Callback: Updated task {$taskId}, affected rows: {$affected}");
|
||||||
|
|
||||||
|
// Публикуем событие в Redis для мгновенной доставки через SSE
|
||||||
|
try {
|
||||||
|
if (class_exists('Redis')) {
|
||||||
|
$redis = new Redis();
|
||||||
|
if ($redis->connect('crm.clientright.ru', 6379)) {
|
||||||
|
$redis->auth('CRM_Redis_Pass_2025_Secure!');
|
||||||
|
|
||||||
|
$channel = "ai:response:{$taskId}";
|
||||||
|
$event = json_encode([
|
||||||
|
'task_id' => $taskId,
|
||||||
|
'status' => $status,
|
||||||
|
'response' => $response,
|
||||||
|
'error' => $error,
|
||||||
|
'timestamp' => date('Y-m-d H:i:s')
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$redis->publish($channel, $event);
|
||||||
|
error_log("Callback: Published to Redis channel {$channel}");
|
||||||
|
$redis->close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Используем Predis если расширение Redis недоступно
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||||
|
$redis = new Predis\Client([
|
||||||
|
'scheme' => 'tcp',
|
||||||
|
'host' => 'crm.clientright.ru',
|
||||||
|
'port' => 6379,
|
||||||
|
'password' => 'CRM_Redis_Pass_2025_Secure!',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$channel = "ai:response:{$taskId}";
|
||||||
|
$event = json_encode([
|
||||||
|
'task_id' => $taskId,
|
||||||
|
'status' => $status,
|
||||||
|
'response' => $response,
|
||||||
|
'error' => $error,
|
||||||
|
'timestamp' => date('Y-m-d H:i:s')
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$redis->publish($channel, $event);
|
||||||
|
error_log("Callback: Published to Redis channel {$channel} via Predis");
|
||||||
|
}
|
||||||
|
} catch (Exception $redisError) {
|
||||||
|
error_log("Callback: Redis publish error (non-critical): " . $redisError->getMessage());
|
||||||
|
// Не прерываем выполнение, если Redis недоступен - БД уже обновлена
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Response received',
|
'message' => 'Response received',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"guzzlehttp/guzzle": "^7.8",
|
"guzzlehttp/guzzle": "^7.8",
|
||||||
"tecnickcom/tcpdf": "^6.7",
|
"tecnickcom/tcpdf": "^6.7",
|
||||||
"aws/aws-sdk-php": "^3.337",
|
"aws/aws-sdk-php": "^3.337",
|
||||||
"predis/predis": "^3.2"
|
"predis/predis": "^3.2",
|
||||||
|
"phpoffice/phpword": "^1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
570
crm_extensions/file_storage/api/create_document_with_text.php
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Создание документа с текстом от AI
|
||||||
|
*
|
||||||
|
* Упрощенная версия - создает пустой DOCX, записывает в него текст от AI
|
||||||
|
* и возвращает ссылку на редактирование
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||||
|
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
|
||||||
|
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
// Параметры - поддерживаем JSON POST и обычные POST/GET
|
||||||
|
$input = null;
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||||
|
if (strpos($contentType, 'application/json') !== false) {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
} else {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = $input['module'] ?? $_GET['module'] ?? '';
|
||||||
|
$recordId = $input['recordId'] ?? $_GET['recordId'] ?? '';
|
||||||
|
$recordName = $input['recordName'] ?? $_GET['recordName'] ?? '';
|
||||||
|
$fileName = $input['fileName'] ?? $_GET['fileName'] ?? '';
|
||||||
|
$documentText = $input['documentText'] ?? $_GET['documentText'] ?? '';
|
||||||
|
$documentType = $input['documentType'] ?? $_GET['documentType'] ?? 'docx'; // docx, xlsx, pptx
|
||||||
|
|
||||||
|
if (empty($module) || empty($recordId) || empty($fileName) || empty($documentText)) {
|
||||||
|
die(json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Не указаны обязательные параметры: module, recordId, fileName, documentText'
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем папку модуля
|
||||||
|
$moduleFolders = [
|
||||||
|
'Project' => 'Project',
|
||||||
|
'Contacts' => 'Contacts',
|
||||||
|
'Accounts' => 'Accounts',
|
||||||
|
'Invoice' => 'Invoice',
|
||||||
|
'Quotes' => 'Quotes',
|
||||||
|
'SalesOrder' => 'SalesOrder',
|
||||||
|
'PurchaseOrder' => 'PurchaseOrder',
|
||||||
|
'HelpDesk' => 'HelpDesk',
|
||||||
|
'Leads' => 'Leads',
|
||||||
|
'Potentials' => 'Potentials'
|
||||||
|
];
|
||||||
|
|
||||||
|
$moduleFolder = $moduleFolders[$module] ?? 'Other';
|
||||||
|
|
||||||
|
// Формируем имя папки записи (заменяем пробелы и спецсимволы на подчеркивания)
|
||||||
|
$recordName = preg_replace('/[\/\\\\:\*\?"<>\|\s]+/', '_', $recordName); // \s+ заменяет все пробелы на одно подчеркивание
|
||||||
|
$recordName = trim($recordName, '_'); // Убираем подчеркивания в начале и конце
|
||||||
|
$folderName = $recordName . '_' . $recordId;
|
||||||
|
|
||||||
|
// Путь к готовому документу (правильный формат: crm2/CRM_Active_Files/... без /crm/ в начале)
|
||||||
|
$fileExtension = $documentType === 'xlsx' ? 'xlsx' : ($documentType === 'pptx' ? 'pptx' : 'docx');
|
||||||
|
$ncPath = "crm2/CRM_Active_Files/Documents/{$moduleFolder}/{$folderName}/{$fileName}.{$fileExtension}";
|
||||||
|
|
||||||
|
error_log("=== CREATE DOCUMENT WITH TEXT ===");
|
||||||
|
error_log("Module: {$module}, RecordId: {$recordId}");
|
||||||
|
error_log("FileName: {$fileName}");
|
||||||
|
error_log("DocumentType: {$documentType}");
|
||||||
|
error_log("Text length: " . strlen($documentText));
|
||||||
|
|
||||||
|
// СОЗДАЕМ ДОКУМЕНТ С ТЕКСТОМ
|
||||||
|
try {
|
||||||
|
$documentContent = createDocumentWithText($documentText, $documentType);
|
||||||
|
|
||||||
|
if (empty($documentContent)) {
|
||||||
|
throw new Exception('Не удалось создать документ');
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("✅ Document created (" . strlen($documentContent) . " bytes)");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Failed to create document: " . $e->getMessage());
|
||||||
|
die(json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Ошибка создания документа: ' . $e->getMessage()
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// СОХРАНЯЕМ В S3
|
||||||
|
$s3Path = $ncPath; // Путь уже без лишних слешей
|
||||||
|
$s3Client = new Aws\S3\S3Client([
|
||||||
|
'version' => 'latest',
|
||||||
|
'region' => 'ru-1',
|
||||||
|
'endpoint' => 'https://s3.twcstorage.ru',
|
||||||
|
'use_path_style_endpoint' => true,
|
||||||
|
'credentials' => [
|
||||||
|
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
|
||||||
|
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
|
||||||
|
],
|
||||||
|
'suppress_php_deprecation_warning' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $s3Client->putObject([
|
||||||
|
'Bucket' => $bucket,
|
||||||
|
'Key' => $s3Path,
|
||||||
|
'Body' => $documentContent,
|
||||||
|
'ContentType' => getContentType($fileExtension)
|
||||||
|
]);
|
||||||
|
|
||||||
|
error_log("✅ File saved to S3: {$s3Path}");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Failed to save to S3: " . $e->getMessage());
|
||||||
|
die(json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Ошибка сохранения: ' . $e->getMessage()
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ПУБЛИКУЕМ СОБЫТИЕ В REDIS
|
||||||
|
try {
|
||||||
|
$redis = new Predis\Client([
|
||||||
|
'scheme' => 'tcp',
|
||||||
|
'host' => 'crm.clientright.ru',
|
||||||
|
'port' => 6379,
|
||||||
|
'password' => 'CRM_Redis_Pass_2025_Secure!'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$event = json_encode([
|
||||||
|
'type' => 'file_created',
|
||||||
|
'source' => 'ai_document_generator',
|
||||||
|
'path' => $s3Path,
|
||||||
|
'timestamp' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$redis->publish('crm:file:events', $event);
|
||||||
|
error_log("✅ Published event to Redis");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Redis publish failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ФОРМИРУЕМ ССЫЛКУ НА РЕДАКТИРОВАНИЕ
|
||||||
|
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
|
||||||
|
$editUrl = 'https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=' . urlencode($recordId) . '&fileName=' . urlencode($s3Url);
|
||||||
|
|
||||||
|
// ВОЗВРАЩАЕМ РЕЗУЛЬТАТ
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Документ создан успешно',
|
||||||
|
'documentName' => $fileName . '.' . $fileExtension,
|
||||||
|
'documentUrl' => $s3Url,
|
||||||
|
'editUrl' => $editUrl,
|
||||||
|
'path' => $s3Path
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает документ с текстом
|
||||||
|
*/
|
||||||
|
function createDocumentWithText($text, $type) {
|
||||||
|
if ($type === 'docx') {
|
||||||
|
return createDocxWithText($text);
|
||||||
|
} elseif ($type === 'xlsx') {
|
||||||
|
return createXlsxWithText($text);
|
||||||
|
} elseif ($type === 'pptx') {
|
||||||
|
return createPptxWithText($text);
|
||||||
|
} else {
|
||||||
|
throw new Exception("Неподдерживаемый тип документа: {$type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает DOCX с текстом и форматированием (поддержка Markdown)
|
||||||
|
*/
|
||||||
|
function createDocxWithText($text) {
|
||||||
|
// Проверяем наличие PHPWord
|
||||||
|
if (class_exists('\PhpOffice\PhpWord\PhpWord')) {
|
||||||
|
// Используем PHPWord если доступен
|
||||||
|
$phpWord = new \PhpOffice\PhpWord\PhpWord();
|
||||||
|
|
||||||
|
// Настройки документа
|
||||||
|
$phpWord->setDefaultFontName('Times New Roman');
|
||||||
|
$phpWord->setDefaultFontSize(12);
|
||||||
|
|
||||||
|
// Добавляем секцию
|
||||||
|
$section = $phpWord->addSection([
|
||||||
|
'marginTop' => 1134, // 2 см
|
||||||
|
'marginRight' => 1134, // 2 см
|
||||||
|
'marginBottom' => 1134, // 2 см
|
||||||
|
'marginLeft' => 1701 // 3 см
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Парсим Markdown и создаем форматированный документ
|
||||||
|
parseMarkdownToPHPWord($section, $text);
|
||||||
|
|
||||||
|
// Сохраняем во временный файл
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'docx_') . '.docx';
|
||||||
|
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
|
||||||
|
$writer->save($tempFile);
|
||||||
|
|
||||||
|
// Читаем содержимое
|
||||||
|
$content = file_get_contents($tempFile);
|
||||||
|
|
||||||
|
// Удаляем временный файл
|
||||||
|
unlink($tempFile);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
} else {
|
||||||
|
// Fallback: создаем простой DOCX через ZIP (DOCX это ZIP архив)
|
||||||
|
return createSimpleDocx($text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит Markdown и добавляет форматированный текст в PHPWord секцию
|
||||||
|
*
|
||||||
|
* Поддерживает:
|
||||||
|
* - Заголовки: # H1, ## H2, ### H3
|
||||||
|
* - Жирный: **текст** или __текст__
|
||||||
|
* - Курсив: *текст* или _текст_
|
||||||
|
* - Маркированные списки: - или *
|
||||||
|
* - Нумерованные списки: 1. 2. 3.
|
||||||
|
* - Выделение: `код`
|
||||||
|
*/
|
||||||
|
function parseMarkdownToPHPWord($section, $text) {
|
||||||
|
$lines = explode("\n", $text);
|
||||||
|
$inList = false;
|
||||||
|
$listType = null; // 'ul' или 'ol'
|
||||||
|
$listItems = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = rtrim($line);
|
||||||
|
|
||||||
|
// Пустая строка
|
||||||
|
if (empty($line)) {
|
||||||
|
if ($inList) {
|
||||||
|
// Завершаем список
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
$listItems = [];
|
||||||
|
$inList = false;
|
||||||
|
$listType = null;
|
||||||
|
}
|
||||||
|
$section->addText('');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заголовки
|
||||||
|
if (preg_match('/^(#{1,3})\s+(.+)$/', $line, $matches)) {
|
||||||
|
if ($inList) {
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
$listItems = [];
|
||||||
|
$inList = false;
|
||||||
|
$listType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = strlen($matches[1]);
|
||||||
|
$title = trim($matches[2]);
|
||||||
|
$fontSize = [1 => 18, 2 => 16, 3 => 14][$level] ?? 14;
|
||||||
|
|
||||||
|
$section->addText($title, [
|
||||||
|
'name' => 'Times New Roman',
|
||||||
|
'size' => $fontSize,
|
||||||
|
'bold' => true,
|
||||||
|
'color' => '000000'
|
||||||
|
], [
|
||||||
|
'spaceAfter' => 240,
|
||||||
|
'spaceBefore' => $level === 1 ? 480 : 240
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маркированный список
|
||||||
|
if (preg_match('/^[-*]\s+(.+)$/', $line, $matches)) {
|
||||||
|
if (!$inList || $listType !== 'ul') {
|
||||||
|
if ($inList && $listType === 'ol') {
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
$listItems = [];
|
||||||
|
}
|
||||||
|
$inList = true;
|
||||||
|
$listType = 'ul';
|
||||||
|
}
|
||||||
|
$listItems[] = trim($matches[1]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нумерованный список
|
||||||
|
if (preg_match('/^\d+\.\s+(.+)$/', $line, $matches)) {
|
||||||
|
if (!$inList || $listType !== 'ol') {
|
||||||
|
if ($inList && $listType === 'ul') {
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
$listItems = [];
|
||||||
|
}
|
||||||
|
$inList = true;
|
||||||
|
$listType = 'ol';
|
||||||
|
}
|
||||||
|
$listItems[] = trim($matches[1]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обычный текст
|
||||||
|
if ($inList) {
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
$listItems = [];
|
||||||
|
$inList = false;
|
||||||
|
$listType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим форматирование в строке (жирный, курсив, код)
|
||||||
|
addFormattedText($section, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если список не завершен
|
||||||
|
if ($inList && !empty($listItems)) {
|
||||||
|
addListToSection($section, $listItems, $listType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавляет форматированный текст в секцию
|
||||||
|
*/
|
||||||
|
function addFormattedText($section, $text) {
|
||||||
|
// Парсим inline форматирование: **жирный**, *курсив*, `код`
|
||||||
|
$textRun = $section->addTextRun(['spaceAfter' => 240]);
|
||||||
|
|
||||||
|
// Разбиваем текст на части с форматированием
|
||||||
|
$parts = preg_split('/(\*\*.*?\*\*|__.*?__|\*.*?\*|_.*?_|`.*?`)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (empty($part)) continue;
|
||||||
|
|
||||||
|
// Жирный **текст** или __текст__
|
||||||
|
if (preg_match('/^\*\*(.+?)\*\*$/', $part, $m) || preg_match('/^__(.+?)__$/', $part, $m)) {
|
||||||
|
$textRun->addText($m[1], [
|
||||||
|
'name' => 'Times New Roman',
|
||||||
|
'size' => 12,
|
||||||
|
'bold' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Курсив *текст* или _текст_ (но не жирный)
|
||||||
|
elseif (preg_match('/^\*(.+?)\*$/', $part, $m) || (preg_match('/^_(.+?)_$/', $part, $m) && !preg_match('/^__/', $part))) {
|
||||||
|
$textRun->addText($m[1], [
|
||||||
|
'name' => 'Times New Roman',
|
||||||
|
'size' => 12,
|
||||||
|
'italic' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Код `текст`
|
||||||
|
elseif (preg_match('/^`(.+?)`$/', $part, $m)) {
|
||||||
|
$textRun->addText($m[1], [
|
||||||
|
'name' => 'Courier New',
|
||||||
|
'size' => 11,
|
||||||
|
'color' => '0066CC'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Обычный текст
|
||||||
|
else {
|
||||||
|
$textRun->addText($part, [
|
||||||
|
'name' => 'Times New Roman',
|
||||||
|
'size' => 12
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавляет список в секцию
|
||||||
|
*/
|
||||||
|
function addListToSection($section, $items, $type) {
|
||||||
|
if (empty($items)) return;
|
||||||
|
|
||||||
|
$fontStyle = [
|
||||||
|
'name' => 'Times New Roman',
|
||||||
|
'size' => 12
|
||||||
|
];
|
||||||
|
|
||||||
|
$paragraphStyle = [
|
||||||
|
'spaceAfter' => 120,
|
||||||
|
'indentation' => ['left' => 720] // 0.5 дюйма
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($items as $index => $item) {
|
||||||
|
$prefix = $type === 'ol' ? ($index + 1) . '. ' : '• ';
|
||||||
|
|
||||||
|
// Парсим форматирование в элементе списка
|
||||||
|
$textRun = $section->addTextRun($paragraphStyle);
|
||||||
|
$textRun->addText($prefix, array_merge($fontStyle, ['bold' => true]));
|
||||||
|
|
||||||
|
// Добавляем текст элемента с форматированием
|
||||||
|
$formattedParts = preg_split('/(\*\*.*?\*\*|__.*?__|\*.*?\*|_.*?_|`.*?`)/', $item, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
|
||||||
|
foreach ($formattedParts as $part) {
|
||||||
|
if (empty($part)) continue;
|
||||||
|
|
||||||
|
if (preg_match('/^\*\*(.+?)\*\*$/', $part, $m) || preg_match('/^__(.+?)__$/', $part, $m)) {
|
||||||
|
$textRun->addText($m[1], array_merge($fontStyle, ['bold' => true]));
|
||||||
|
} elseif (preg_match('/^\*(.+?)\*$/', $part, $m) || (preg_match('/^_(.+?)_$/', $part, $m) && !preg_match('/^__/', $part))) {
|
||||||
|
$textRun->addText($m[1], array_merge($fontStyle, ['italic' => true]));
|
||||||
|
} elseif (preg_match('/^`(.+?)`$/', $part, $m)) {
|
||||||
|
$textRun->addText($m[1], array_merge($fontStyle, ['name' => 'Courier New', 'color' => '0066CC']));
|
||||||
|
} else {
|
||||||
|
$textRun->addText($part, $fontStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пустая строка после списка
|
||||||
|
$section->addText('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает простой DOCX без PHPWord (через ZIP архив)
|
||||||
|
*/
|
||||||
|
function createSimpleDocx($text) {
|
||||||
|
// Экранируем XML спецсимволы
|
||||||
|
$text = htmlspecialchars($text, ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
// Разбиваем на параграфы
|
||||||
|
$paragraphs = explode("\n", $text);
|
||||||
|
$paragraphXml = '';
|
||||||
|
foreach ($paragraphs as $paragraph) {
|
||||||
|
$paragraph = trim($paragraph);
|
||||||
|
if (empty($paragraph)) {
|
||||||
|
$paragraphXml .= '<w:p><w:r><w:t></w:t></w:r></w:p>';
|
||||||
|
} else {
|
||||||
|
$paragraphXml .= '<w:p><w:r><w:t>' . $paragraph . '</w:t></w:r></w:p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Минимальный DOCX структура (ZIP архив с XML файлами)
|
||||||
|
$tempDir = sys_get_temp_dir() . '/docx_' . uniqid();
|
||||||
|
mkdir($tempDir, 0755, true);
|
||||||
|
|
||||||
|
// [Content_Types].xml
|
||||||
|
file_put_contents($tempDir . '/[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="xml" ContentType="application/xml"/>
|
||||||
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||||
|
</Types>');
|
||||||
|
|
||||||
|
// _rels/.rels
|
||||||
|
mkdir($tempDir . '/_rels', 0755, true);
|
||||||
|
file_put_contents($tempDir . '/_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||||
|
</Relationships>');
|
||||||
|
|
||||||
|
// word/document.xml
|
||||||
|
mkdir($tempDir . '/word', 0755, true);
|
||||||
|
file_put_contents($tempDir . '/word/document.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||||
|
<w:body>' . $paragraphXml . '</w:body>
|
||||||
|
</w:document>');
|
||||||
|
|
||||||
|
// Создаем ZIP архив
|
||||||
|
$zipFile = tempnam(sys_get_temp_dir(), 'docx_') . '.docx';
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
|
||||||
|
$zip->addFile($tempDir . '/[Content_Types].xml', '[Content_Types].xml');
|
||||||
|
$zip->addFile($tempDir . '/_rels/.rels', '_rels/.rels');
|
||||||
|
$zip->addFile($tempDir . '/word/document.xml', 'word/document.xml');
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем содержимое
|
||||||
|
$content = file_get_contents($zipFile);
|
||||||
|
|
||||||
|
// Удаляем временные файлы
|
||||||
|
unlink($zipFile);
|
||||||
|
array_map('unlink', glob($tempDir . '/*/*'));
|
||||||
|
array_map('unlink', glob($tempDir . '/*'));
|
||||||
|
rmdir($tempDir . '/word');
|
||||||
|
rmdir($tempDir . '/_rels');
|
||||||
|
rmdir($tempDir);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает XLSX с текстом (в первой ячейке)
|
||||||
|
*/
|
||||||
|
function createXlsxWithText($text) {
|
||||||
|
if (class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet')) {
|
||||||
|
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
|
// Записываем текст в первую ячейку
|
||||||
|
$sheet->setCellValue('A1', $text);
|
||||||
|
|
||||||
|
// Автоподбор ширины колонки
|
||||||
|
$sheet->getColumnDimension('A')->setAutoSize(true);
|
||||||
|
|
||||||
|
// Сохраняем во временный файл
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'xlsx_') . '.xlsx';
|
||||||
|
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
||||||
|
$writer->save($tempFile);
|
||||||
|
|
||||||
|
// Читаем содержимое
|
||||||
|
$content = file_get_contents($tempFile);
|
||||||
|
|
||||||
|
// Удаляем временный файл
|
||||||
|
unlink($tempFile);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
} else {
|
||||||
|
throw new Exception('PhpSpreadsheet не установлен. Установите через composer: composer require phpoffice/phpspreadsheet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает PPTX с текстом (на первом слайде)
|
||||||
|
*/
|
||||||
|
function createPptxWithText($text) {
|
||||||
|
if (class_exists('\PhpOffice\PhpPresentation\PhpPresentation')) {
|
||||||
|
$presentation = new \PhpOffice\PhpPresentation\PhpPresentation();
|
||||||
|
$slide = $presentation->getActiveSlide();
|
||||||
|
|
||||||
|
// Создаем текстовую фигуру
|
||||||
|
$shape = $slide->createRichTextShape()
|
||||||
|
->setHeight(400)
|
||||||
|
->setWidth(800)
|
||||||
|
->setOffsetX(100)
|
||||||
|
->setOffsetY(100);
|
||||||
|
|
||||||
|
// Разбиваем текст на параграфы
|
||||||
|
$paragraphs = explode("\n", $text);
|
||||||
|
foreach ($paragraphs as $paragraph) {
|
||||||
|
$paragraph = trim($paragraph);
|
||||||
|
if (!empty($paragraph)) {
|
||||||
|
$shape->createTextRun($paragraph);
|
||||||
|
$shape->createParagraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем во временный файл
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'pptx_') . '.pptx';
|
||||||
|
$writer = \PhpOffice\PhpPresentation\IOFactory::createWriter($presentation, 'PowerPoint2007');
|
||||||
|
$writer->save($tempFile);
|
||||||
|
|
||||||
|
// Читаем содержимое
|
||||||
|
$content = file_get_contents($tempFile);
|
||||||
|
|
||||||
|
// Удаляем временный файл
|
||||||
|
unlink($tempFile);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
} else {
|
||||||
|
throw new Exception('PhpPresentation не установлен. Установите через composer: composer require phpoffice/phppresentation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет Content-Type для файла
|
||||||
|
*/
|
||||||
|
function getContentType($fileType) {
|
||||||
|
$types = [
|
||||||
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
];
|
||||||
|
return $types[$fileType] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
251
crm_extensions/file_storage/api/create_from_template.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Создание документа из шаблона Nextcloud
|
||||||
|
*
|
||||||
|
* Алгоритм:
|
||||||
|
* 1. Получаем шаблон из Nextcloud через WebDAV
|
||||||
|
* 2. Заполняем переменные через PHPWord
|
||||||
|
* 3. Сохраняем готовый документ в папку проекта
|
||||||
|
* 4. Открываем в OnlyOffice
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||||
|
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
|
||||||
|
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
// Параметры
|
||||||
|
$module = $_GET['module'] ?? '';
|
||||||
|
$recordId = $_GET['recordId'] ?? '';
|
||||||
|
$recordName = $_GET['recordName'] ?? '';
|
||||||
|
$fileName = $_GET['fileName'] ?? '';
|
||||||
|
$templateName = $_GET['templateName'] ?? ''; // Имя шаблона (например, "pretenziya.docx")
|
||||||
|
$variables = json_decode($_GET['variables'] ?? '{}', true); // Переменные для заполнения
|
||||||
|
|
||||||
|
if (empty($module) || empty($recordId) || empty($fileName) || empty($templateName)) {
|
||||||
|
die(json_encode(['success' => false, 'error' => 'Не указаны обязательные параметры']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nextcloud credentials
|
||||||
|
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
$username = 'admin';
|
||||||
|
$password = 'office';
|
||||||
|
|
||||||
|
// Определяем папку модуля
|
||||||
|
$moduleFolders = [
|
||||||
|
'Project' => 'Project',
|
||||||
|
'Contacts' => 'Contacts',
|
||||||
|
'Accounts' => 'Accounts',
|
||||||
|
'Invoice' => 'Invoice',
|
||||||
|
'Quotes' => 'Quotes',
|
||||||
|
'SalesOrder' => 'SalesOrder',
|
||||||
|
'PurchaseOrder' => 'PurchaseOrder',
|
||||||
|
'HelpDesk' => 'HelpDesk',
|
||||||
|
'Leads' => 'Leads',
|
||||||
|
'Potentials' => 'Potentials'
|
||||||
|
];
|
||||||
|
|
||||||
|
$moduleFolder = $moduleFolders[$module] ?? 'Other';
|
||||||
|
|
||||||
|
// Формируем имя папки записи
|
||||||
|
$recordName = preg_replace('/[\/\\\\:\*\?"<>\|]/', '_', $recordName);
|
||||||
|
$folderName = $recordName . '_' . $recordId;
|
||||||
|
|
||||||
|
// ONLYOFFICE хранит шаблоны в папке /Templates/ в корне пользователя
|
||||||
|
// Путь к шаблону в Nextcloud
|
||||||
|
$templatePath = "/Templates/{$templateName}";
|
||||||
|
$templateWebDAVUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatePath;
|
||||||
|
|
||||||
|
// Путь к готовому документу
|
||||||
|
$fileType = pathinfo($templateName, PATHINFO_EXTENSION);
|
||||||
|
$ncPath = "/crm/crm2/CRM_Active_Files/Documents/{$moduleFolder}/{$folderName}/{$fileName}.{$fileType}";
|
||||||
|
|
||||||
|
error_log("=== CREATE FROM TEMPLATE ===");
|
||||||
|
error_log("Template: {$templateName}");
|
||||||
|
error_log("Variables: " . json_encode($variables, JSON_UNESCAPED_UNICODE));
|
||||||
|
error_log("Output path: {$ncPath}");
|
||||||
|
|
||||||
|
// 1. СКАЧИВАЕМ ШАБЛОН ИЗ NEXTCLOUD
|
||||||
|
$ch = curl_init($templateWebDAVUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
|
$templateContent = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200 || empty($templateContent)) {
|
||||||
|
die(json_encode(['success' => false, 'error' => "Шаблон не найден: {$templateName}"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("✅ Template downloaded (" . strlen($templateContent) . " bytes)");
|
||||||
|
|
||||||
|
// 2. ЗАПОЛНЯЕМ ПЕРЕМЕННЫЕ В ШАБЛОНЕ
|
||||||
|
$filledContent = fillTemplateVariables($templateContent, $variables, $fileType);
|
||||||
|
|
||||||
|
// 3. СОХРАНЯЕМ В S3
|
||||||
|
$s3Path = ltrim($ncPath, '/');
|
||||||
|
$s3Client = new Aws\S3\S3Client([
|
||||||
|
'version' => 'latest',
|
||||||
|
'region' => 'ru-1',
|
||||||
|
'endpoint' => 'https://s3.twcstorage.ru',
|
||||||
|
'use_path_style_endpoint' => true,
|
||||||
|
'credentials' => [
|
||||||
|
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
|
||||||
|
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
|
||||||
|
],
|
||||||
|
'suppress_php_deprecation_warning' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $s3Client->putObject([
|
||||||
|
'Bucket' => $bucket,
|
||||||
|
'Key' => $s3Path,
|
||||||
|
'Body' => $filledContent,
|
||||||
|
'ContentType' => getContentType($fileType)
|
||||||
|
]);
|
||||||
|
|
||||||
|
error_log("✅ File saved to S3: {$s3Path}");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Failed to save to S3: " . $e->getMessage());
|
||||||
|
die(json_encode(['success' => false, 'error' => "Ошибка сохранения: " . $e->getMessage()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. ПУБЛИКУЕМ СОБЫТИЕ В REDIS
|
||||||
|
try {
|
||||||
|
$redis = new Predis\Client([
|
||||||
|
'scheme' => 'tcp',
|
||||||
|
'host' => 'crm.clientright.ru',
|
||||||
|
'port' => 6379,
|
||||||
|
'password' => 'CRM_Redis_Pass_2025_Secure!'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$event = json_encode([
|
||||||
|
'type' => 'file_created',
|
||||||
|
'source' => 'crm_template',
|
||||||
|
'path' => $s3Path,
|
||||||
|
'timestamp' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$redis->publish('crm:file:events', $event);
|
||||||
|
error_log("✅ Published event to Redis");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Redis publish failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. ОТКРЫВАЕМ В ONLYOFFICE
|
||||||
|
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
|
||||||
|
$redirectUrl = '/crm_extensions/file_storage/api/open_file_v2.php?recordId=' . urlencode($recordId) . '&fileName=' . urlencode($s3Url);
|
||||||
|
|
||||||
|
header('Location: ' . $redirectUrl);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заполняет переменные в шаблоне
|
||||||
|
*
|
||||||
|
* Поддерживает два формата:
|
||||||
|
* 1. Простая замена {VARIABLE_NAME} → значение
|
||||||
|
* 2. PHPWord для сложных документов
|
||||||
|
*/
|
||||||
|
function fillTemplateVariables($content, $variables, $fileType) {
|
||||||
|
if ($fileType === 'docx') {
|
||||||
|
// Используем PHPWord для DOCX
|
||||||
|
return fillDocxTemplate($content, $variables);
|
||||||
|
} else {
|
||||||
|
// Для других форматов - простая замена
|
||||||
|
return fillSimpleTemplate($content, $variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заполнение DOCX через PHPWord
|
||||||
|
*/
|
||||||
|
function fillDocxTemplate($content, $variables) {
|
||||||
|
// Сохраняем во временный файл
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'template_') . '.docx';
|
||||||
|
file_put_contents($tempFile, $content);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$phpWord = \PhpOffice\PhpWord\IOFactory::load($tempFile);
|
||||||
|
|
||||||
|
// Заменяем переменные во всех секциях
|
||||||
|
foreach ($phpWord->getSections() as $section) {
|
||||||
|
foreach ($section->getElements() as $element) {
|
||||||
|
if ($element instanceof \PhpOffice\PhpWord\Element\Text) {
|
||||||
|
$text = $element->getText();
|
||||||
|
$text = replaceVariables($text, $variables);
|
||||||
|
$element->setText($text);
|
||||||
|
} elseif ($element instanceof \PhpOffice\PhpWord\Element\TextRun) {
|
||||||
|
foreach ($element->getElements() as $textElement) {
|
||||||
|
if ($textElement instanceof \PhpOffice\PhpWord\Element\Text) {
|
||||||
|
$text = $textElement->getText();
|
||||||
|
$text = replaceVariables($text, $variables);
|
||||||
|
$textElement->setText($text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем результат
|
||||||
|
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
|
||||||
|
$outputFile = tempnam(sys_get_temp_dir(), 'output_') . '.docx';
|
||||||
|
$writer->save($outputFile);
|
||||||
|
|
||||||
|
$result = file_get_contents($outputFile);
|
||||||
|
|
||||||
|
// Удаляем временные файлы
|
||||||
|
unlink($tempFile);
|
||||||
|
unlink($outputFile);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("PHPWord error: " . $e->getMessage());
|
||||||
|
// Fallback на простую замену
|
||||||
|
unlink($tempFile);
|
||||||
|
return fillSimpleTemplate($content, $variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простая замена переменных {VAR} → значение
|
||||||
|
*/
|
||||||
|
function fillSimpleTemplate($content, $variables) {
|
||||||
|
foreach ($variables as $key => $value) {
|
||||||
|
$content = str_replace('{' . strtoupper($key) . '}', $value, $content);
|
||||||
|
$content = str_replace('{{' . strtoupper($key) . '}}', $value, $content);
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Универсальная замена переменных
|
||||||
|
*/
|
||||||
|
function replaceVariables($text, $variables) {
|
||||||
|
foreach ($variables as $key => $value) {
|
||||||
|
$text = str_replace('{' . strtoupper($key) . '}', $value, $text);
|
||||||
|
$text = str_replace('{{' . strtoupper($key) . '}}', $value, $text);
|
||||||
|
}
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет Content-Type для файла
|
||||||
|
*/
|
||||||
|
function getContentType($fileType) {
|
||||||
|
$types = [
|
||||||
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
];
|
||||||
|
return $types[$fileType] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
143
crm_extensions/file_storage/api/list_templates.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Получение списка шаблонов из Nextcloud через WebDAV
|
||||||
|
*
|
||||||
|
* Использует WebDAV PROPFIND вместо несуществующего API endpoint
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
|
||||||
|
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
$username = 'admin';
|
||||||
|
$password = 'office';
|
||||||
|
// ONLYOFFICE хранит шаблоны в папке /Templates/ в корне пользователя
|
||||||
|
$templatesPath = '/Templates/';
|
||||||
|
|
||||||
|
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatesPath;
|
||||||
|
|
||||||
|
error_log("=== LIST TEMPLATES ===");
|
||||||
|
error_log("WebDAV URL: {$webdavUrl}");
|
||||||
|
|
||||||
|
// PROPFIND запрос для получения списка файлов
|
||||||
|
$propfindXml = '<?xml version="1.0"?>
|
||||||
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<d:getcontenttype/>
|
||||||
|
<d:getcontentlength/>
|
||||||
|
<d:getlastmodified/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>';
|
||||||
|
|
||||||
|
$ch = curl_init($webdavUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Depth: 1',
|
||||||
|
'Content-Type: application/xml'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $propfindXml);
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
error_log("HTTP Code: {$httpCode}");
|
||||||
|
error_log("Response length: " . strlen($response));
|
||||||
|
|
||||||
|
if ($httpCode === 404) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Папка Templates не найдена. Создайте папку /crm/Templates/ в Nextcloud и загрузите туда шаблоны.',
|
||||||
|
'templates' => []
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 207) { // 207 Multi-Status для PROPFIND
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Ошибка WebDAV: HTTP {$httpCode}" . ($error ? " - {$error}" : ''),
|
||||||
|
'templates' => []
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим XML ответ
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = @simplexml_load_string($response);
|
||||||
|
|
||||||
|
if ($xml === false) {
|
||||||
|
$errors = libxml_get_errors();
|
||||||
|
libxml_clear_errors();
|
||||||
|
error_log("XML Parse Error: " . print_r($errors, true));
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Ошибка парсинга XML ответа',
|
||||||
|
'templates' => []
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем namespace для XPath
|
||||||
|
$xml->registerXPathNamespace('d', 'DAV:');
|
||||||
|
|
||||||
|
$templates = [];
|
||||||
|
$basePath = rtrim($webdavUrl, '/');
|
||||||
|
|
||||||
|
foreach ($xml->xpath('//d:response') as $response) {
|
||||||
|
$href = (string)$response->xpath('.//d:href')[0];
|
||||||
|
$displayName = (string)$response->xpath('.//d:displayname')[0];
|
||||||
|
$contentType = (string)($response->xpath('.//d:getcontenttype')[0] ?? '');
|
||||||
|
$contentLength = (string)($response->xpath('.//d:getcontentlength')[0] ?? '0');
|
||||||
|
$lastModified = (string)($response->xpath('.//d:getlastmodified')[0] ?? '');
|
||||||
|
|
||||||
|
// Пропускаем саму папку
|
||||||
|
if (rtrim($href, '/') === $basePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пропускаем подпапки (если есть)
|
||||||
|
if (empty($contentType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Только Office файлы
|
||||||
|
$isOfficeFile = (
|
||||||
|
strpos($contentType, 'officedocument') !== false ||
|
||||||
|
strpos($contentType, 'msword') !== false ||
|
||||||
|
strpos($contentType, 'spreadsheet') !== false ||
|
||||||
|
strpos($contentType, 'presentation') !== false ||
|
||||||
|
strpos($contentType, 'opendocument') !== false
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($isOfficeFile) {
|
||||||
|
// Извлекаем имя файла из пути
|
||||||
|
$fileName = basename($href);
|
||||||
|
|
||||||
|
$templates[] = [
|
||||||
|
'name' => $displayName ?: $fileName,
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'path' => $href,
|
||||||
|
'type' => $contentType,
|
||||||
|
'size' => (int)$contentLength,
|
||||||
|
'modified' => $lastModified
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("Found " . count($templates) . " templates");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'templates' => $templates,
|
||||||
|
'count' => count($templates)
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
@@ -1,97 +1,110 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Открытие файла через Nextcloud + OnlyOffice
|
* Открытие файла через Nextcloud (РАБОЧАЯ ВЕРСИЯ v2)
|
||||||
* Для сравнения с прямым OnlyOffice
|
* Использует Redis индекс для быстрого получения FileID
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
|
// Отключаем вывод ошибок в браузер
|
||||||
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
$fileName = isset($_GET['fileName']) ? $_GET['fileName'] : '';
|
// Отключаем buffering
|
||||||
$recordId = isset($_GET['recordId']) ? $_GET['recordId'] : '';
|
if (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
if (empty($fileName)) {
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
die("❌ fileName не указан");
|
header('Cache-Control: no-cache, must-revalidate');
|
||||||
|
|
||||||
|
$recordId = isset($_GET['recordId']) ? (int)$_GET['recordId'] : 0;
|
||||||
|
|
||||||
|
if ($recordId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid recordId']);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Извлекаем S3 путь
|
try {
|
||||||
$s3Path = '';
|
// 1. Получаем filename из БД
|
||||||
if (strpos($fileName, 'http') === 0) {
|
$db = new mysqli('localhost', 'ci20465_72new', 'EcY979Rn', 'ci20465_72new');
|
||||||
$fileName = urldecode($fileName);
|
|
||||||
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
if ($db->connect_error) {
|
||||||
$pos = strpos($fileName, $bucketId . '/');
|
throw new Exception('DB connection failed');
|
||||||
if ($pos !== false) {
|
|
||||||
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$db->set_charset('utf8mb4');
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT filename FROM vtiger_notes WHERE notesid = ?");
|
||||||
|
$stmt->bind_param('i', $recordId);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
$db->close();
|
||||||
|
|
||||||
|
if (!$row || empty($row['filename'])) {
|
||||||
|
throw new Exception('File not found in DB');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = $row['filename'];
|
||||||
|
|
||||||
|
// 2. Извлекаем S3 путь из URL
|
||||||
|
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||||||
|
$fileName = rawurldecode($fileName);
|
||||||
|
$pos = strpos($fileName, $bucketId . '/');
|
||||||
|
|
||||||
|
if ($pos === false) {
|
||||||
|
throw new Exception('Invalid S3 path in filename');
|
||||||
|
}
|
||||||
|
|
||||||
|
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
|
||||||
|
|
||||||
|
// 3. Получаем FileID из Redis
|
||||||
|
$redis = new Redis();
|
||||||
|
if (!$redis->connect('crm.clientright.ru', 6379)) {
|
||||||
|
throw new Exception('Redis connection failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$redis->auth('CRM_Redis_Pass_2025_Secure!');
|
||||||
|
|
||||||
|
$redisKey = "crm:nc:fileid:" . $s3Path;
|
||||||
|
$cached = $redis->get($redisKey);
|
||||||
|
|
||||||
|
if (!$cached) {
|
||||||
|
$redis->close();
|
||||||
|
throw new Exception('FileID not found in Redis index. Key: ' . substr($redisKey, 0, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($cached, true);
|
||||||
|
$fileId = $data['fileId'] ?? null;
|
||||||
|
|
||||||
|
$redis->close();
|
||||||
|
|
||||||
|
if (!$fileId) {
|
||||||
|
throw new Exception('Invalid FileID data in Redis');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Формируем URL для Nextcloud
|
||||||
|
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
$ncPath = '/crm/' . $s3Path;
|
||||||
|
$dirPath = dirname($ncPath);
|
||||||
|
|
||||||
|
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=' . urlencode($dirPath) . '&openfile=true';
|
||||||
|
|
||||||
|
// 5. Возвращаем успешный ответ
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'fileId' => $fileId,
|
||||||
|
'redirectUrl' => $redirectUrl,
|
||||||
|
'source' => 'redis',
|
||||||
|
'recordId' => $recordId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'recordId' => $recordId
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($s3Path)) {
|
|
||||||
die("❌ Не удалось извлечь путь из URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nextcloud credentials
|
|
||||||
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
|
||||||
$username = 'admin';
|
|
||||||
$password = 'office';
|
|
||||||
|
|
||||||
// Формируем WebDAV путь
|
|
||||||
$ncPath = '/crm/' . $s3Path;
|
|
||||||
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $ncPath;
|
|
||||||
|
|
||||||
error_log("=== NEXTCLOUD OPEN ===");
|
|
||||||
error_log("S3 Path: " . $s3Path);
|
|
||||||
error_log("Nextcloud WebDAV: " . $webdavUrl);
|
|
||||||
|
|
||||||
// Получаем fileId через PROPFIND
|
|
||||||
$ch = curl_init($webdavUrl);
|
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
||||||
'Depth: 0',
|
|
||||||
'Content-Type: application/xml'
|
|
||||||
]);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, '<?xml version="1.0"?>
|
|
||||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
|
||||||
<d:prop>
|
|
||||||
<oc:fileid/>
|
|
||||||
</d:prop>
|
|
||||||
</d:propfind>');
|
|
||||||
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
error_log("PROPFIND HTTP код: " . $httpCode);
|
|
||||||
|
|
||||||
if ($httpCode !== 207) {
|
|
||||||
die("❌ Файл не найден в Nextcloud (HTTP $httpCode). Возможно, он не проиндексирован.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Извлекаем fileId из XML
|
|
||||||
preg_match('/<oc:fileid>(\d+)<\/oc:fileid>/', $response, $matches);
|
|
||||||
if (!isset($matches[1])) {
|
|
||||||
die("❌ Не удалось получить fileId");
|
|
||||||
}
|
|
||||||
|
|
||||||
$fileId = $matches[1];
|
|
||||||
error_log("Получен fileId: " . $fileId);
|
|
||||||
|
|
||||||
// Извлекаем директорию из пути
|
|
||||||
$dirPath = dirname($ncPath);
|
|
||||||
|
|
||||||
// Формируем URL для открытия в Nextcloud
|
|
||||||
// Nextcloud автоматически откроет OnlyOffice для редактирования
|
|
||||||
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=' . urlencode($dirPath) . '&openfile=true';
|
|
||||||
|
|
||||||
error_log("Redirect to: " . $redirectUrl);
|
|
||||||
|
|
||||||
// Редирект в Nextcloud
|
|
||||||
header('Location: ' . $redirectUrl);
|
|
||||||
exit;
|
exit;
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
112
crm_extensions/file_storage/api/nextcloud_open_v2.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Открытие файла через Nextcloud (РАБОЧАЯ ВЕРСИЯ v2)
|
||||||
|
* Использует Redis индекс для быстрого получения FileID
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Отключаем вывод ошибок в браузер
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
// Отключаем buffering
|
||||||
|
if (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, must-revalidate');
|
||||||
|
|
||||||
|
$recordId = isset($_GET['recordId']) ? (int)$_GET['recordId'] : 0;
|
||||||
|
|
||||||
|
if ($recordId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid recordId']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Получаем filename из БД
|
||||||
|
$db = new mysqli('localhost', 'ci20465_72new', 'EcY979Rn', 'ci20465_72new');
|
||||||
|
|
||||||
|
if ($db->connect_error) {
|
||||||
|
throw new Exception('DB connection failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->set_charset('utf8mb4');
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT filename FROM vtiger_notes WHERE notesid = ?");
|
||||||
|
$stmt->bind_param('i', $recordId);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
$db->close();
|
||||||
|
|
||||||
|
if (!$row || empty($row['filename'])) {
|
||||||
|
throw new Exception('File not found in DB');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = $row['filename'];
|
||||||
|
|
||||||
|
// 2. Извлекаем S3 путь из URL
|
||||||
|
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||||||
|
$fileName = rawurldecode($fileName);
|
||||||
|
$pos = strpos($fileName, $bucketId . '/');
|
||||||
|
|
||||||
|
if ($pos === false) {
|
||||||
|
throw new Exception('Invalid S3 path in filename');
|
||||||
|
}
|
||||||
|
|
||||||
|
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
|
||||||
|
|
||||||
|
// 3. Получаем FileID из Redis
|
||||||
|
$redis = new Redis();
|
||||||
|
if (!$redis->connect('crm.clientright.ru', 6379)) {
|
||||||
|
throw new Exception('Redis connection failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$redis->auth('CRM_Redis_Pass_2025_Secure!');
|
||||||
|
|
||||||
|
$redisKey = "crm:nc:fileid:" . $s3Path;
|
||||||
|
$cached = $redis->get($redisKey);
|
||||||
|
|
||||||
|
if (!$cached) {
|
||||||
|
$redis->close();
|
||||||
|
throw new Exception('FileID not found in Redis index. Key: ' . substr($redisKey, 0, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($cached, true);
|
||||||
|
$fileId = $data['fileId'] ?? null;
|
||||||
|
|
||||||
|
$redis->close();
|
||||||
|
|
||||||
|
if (!$fileId) {
|
||||||
|
throw new Exception('Invalid FileID data in Redis');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Формируем URL для Nextcloud
|
||||||
|
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
$ncPath = '/crm/' . $s3Path;
|
||||||
|
$dirPath = dirname($ncPath);
|
||||||
|
|
||||||
|
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=' . urlencode($dirPath) . '&openfile=true';
|
||||||
|
|
||||||
|
// 5. Возвращаем успешный ответ
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'fileId' => $fileId,
|
||||||
|
'redirectUrl' => $redirectUrl,
|
||||||
|
'source' => 'redis',
|
||||||
|
'recordId' => $recordId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'recordId' => $recordId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit;
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
36
crm_extensions/file_storage/check_paths_396447.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8mb4",
|
||||||
|
$dbconfig['db_username'],
|
||||||
|
$dbconfig['db_password'],
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "ПРОВЕРКА ПУТЕЙ ПОСЛЕ ИСПРАВЛЕНИЯ:\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT notesid, s3_key FROM vtiger_notes n
|
||||||
|
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||||
|
WHERE snr.crmid = 396447 AND n.s3_key IS NOT NULL
|
||||||
|
ORDER BY notesid";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$allCorrect = true;
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$hasPrefix = strpos($doc['s3_key'], 'crm2/CRM_Active_Files') === 0;
|
||||||
|
$status = $hasPrefix ? '❌ С ПРЕФИКСОМ' : '✅ БЕЗ ПРЕФИКСА';
|
||||||
|
echo sprintf("ID %-8s | %s\n", $doc['notesid'], $status);
|
||||||
|
if ($hasPrefix) {
|
||||||
|
$allCorrect = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
if ($allCorrect) {
|
||||||
|
echo "✅ ВСЕ ДОКУМЕНТЫ ИМЕЮТ ЕДИНООБРАЗНЫЙ ФОРМАТ ПУТИ!\n";
|
||||||
|
} else {
|
||||||
|
echo "⚠️ ЕСТЬ ДОКУМЕНТЫ С ПРЕФИКСОМ\n";
|
||||||
|
}
|
||||||
|
|
||||||
128
crm_extensions/file_storage/check_project_396447.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||||
|
$dbconfig['db_username'],
|
||||||
|
$dbconfig['db_password'],
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
$projectId = 396447;
|
||||||
|
|
||||||
|
// Получаем информацию о проекте
|
||||||
|
$sqlProject = "SELECT projectid, projectname, projectstatus FROM vtiger_project WHERE projectid = ?";
|
||||||
|
$stmtProject = $pdo->prepare($sqlProject);
|
||||||
|
$stmtProject->execute([$projectId]);
|
||||||
|
$project = $stmtProject->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$project) {
|
||||||
|
die("❌ Проект $projectId не найден!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "📋 ПРОЕКТ: {$project['projectname']}\n";
|
||||||
|
echo " ID: {$project['projectid']}\n";
|
||||||
|
echo " Статус: {$project['projectstatus']}\n";
|
||||||
|
echo "\n" . str_repeat("=", 80) . "\n\n";
|
||||||
|
|
||||||
|
// Получаем документы проекта
|
||||||
|
$sql = "SELECT
|
||||||
|
n.notesid,
|
||||||
|
n.title,
|
||||||
|
n.filename,
|
||||||
|
n.filelocationtype,
|
||||||
|
n.foldername,
|
||||||
|
n.s3_key,
|
||||||
|
n.nc_path,
|
||||||
|
n.filesize,
|
||||||
|
e.createdtime,
|
||||||
|
e.modifiedtime,
|
||||||
|
u.user_name,
|
||||||
|
e.deleted
|
||||||
|
FROM vtiger_notes n
|
||||||
|
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||||
|
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||||
|
LEFT JOIN vtiger_users u ON u.id = e.smownerid
|
||||||
|
WHERE snr.crmid = ? AND e.deleted = 0
|
||||||
|
ORDER BY e.createdtime DESC";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$projectId]);
|
||||||
|
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$count = count($documents);
|
||||||
|
echo "📄 НАЙДЕНО ДОКУМЕНТОВ: $count\n\n";
|
||||||
|
|
||||||
|
if ($count == 0) {
|
||||||
|
echo "⚠️ Документы не найдены!\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSize = 0;
|
||||||
|
$s3Count = 0;
|
||||||
|
$localCount = 0;
|
||||||
|
$brokenCount = 0;
|
||||||
|
|
||||||
|
foreach ($documents as $i => $doc) {
|
||||||
|
$num = $i + 1;
|
||||||
|
echo "[$num] ID: " . ($doc['notesid'] ?? 'N/A') . "\n";
|
||||||
|
echo " Название: " . ($doc['title'] ?? 'не указано') . "\n";
|
||||||
|
echo " Файл: " . ($doc['filename'] ?? 'не указано') . "\n";
|
||||||
|
echo " Расположение: " . ($doc['filelocationtype'] ?: 'не указано') . "\n";
|
||||||
|
|
||||||
|
if (!empty($doc['s3_key'])) {
|
||||||
|
echo " S3 Key: " . $doc['s3_key'] . "\n";
|
||||||
|
$s3Count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($doc['nc_path'])) {
|
||||||
|
echo " Nextcloud Path: " . $doc['nc_path'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($doc['foldername'])) {
|
||||||
|
echo " Папка CRM: " . $doc['foldername'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($doc['filesize']) && $doc['filesize'] > 0) {
|
||||||
|
$sizeKB = round($doc['filesize'] / 1024, 2);
|
||||||
|
$sizeMB = round($doc['filesize'] / 1024 / 1024, 2);
|
||||||
|
if ($sizeMB >= 1) {
|
||||||
|
echo " Размер: {$sizeMB} MB\n";
|
||||||
|
} else {
|
||||||
|
echo " Размер: {$sizeKB} KB\n";
|
||||||
|
}
|
||||||
|
$totalSize += $doc['filesize'];
|
||||||
|
} else {
|
||||||
|
echo " Размер: не указан\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo " Создан: " . ($doc['createdtime'] ?? 'не указано') . "\n";
|
||||||
|
echo " Изменён: " . ($doc['modifiedtime'] ?? 'не указано') . "\n";
|
||||||
|
|
||||||
|
if (!empty($doc['user_name'])) {
|
||||||
|
echo " Владелец: " . $doc['user_name'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка на битые файлы
|
||||||
|
if (empty($doc['filename']) && empty($doc['s3_key'])) {
|
||||||
|
echo " ВНИМАНИЕ: Файл без имени и пути!\n";
|
||||||
|
$brokenCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo str_repeat("=", 80) . "\n";
|
||||||
|
echo "📊 СТАТИСТИКА:\n";
|
||||||
|
echo " Всего документов: $count\n";
|
||||||
|
echo " В S3: $s3Count\n";
|
||||||
|
echo " Локальных: " . ($count - $s3Count) . "\n";
|
||||||
|
if ($brokenCount > 0) {
|
||||||
|
echo " ⚠️ Битых (без файла): $brokenCount\n";
|
||||||
|
}
|
||||||
|
if ($totalSize > 0) {
|
||||||
|
$totalMB = round($totalSize / 1024 / 1024, 2);
|
||||||
|
echo " Общий размер: {$totalMB} MB\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
434
crm_extensions/file_storage/docs/AI_DOCUMENT_GENERATION_FLOW.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# 📄 Как AI Ассистент создает документы из шаблонов
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Полное описание процесса
|
||||||
|
|
||||||
|
## 🎯 Общий процесс (пошагово)
|
||||||
|
|
||||||
|
### ШАГ 1: Пользователь просит создать документ
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
```
|
||||||
|
Пользователь: "Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
|
||||||
|
ответчик УК Жилищник, клиент Иванов Иван Иванович"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что происходит:**
|
||||||
|
- Пользователь вводит запрос в AI Drawer
|
||||||
|
- AI Drawer отправляет запрос в n8n через `/aiassist/n8n_proxy.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 2: AI Drawer отправляет запрос в n8n
|
||||||
|
|
||||||
|
**Код в `ai-drawer-simple.js`:**
|
||||||
|
```javascript
|
||||||
|
// Пользователь нажал "Отправить"
|
||||||
|
sendMessage() {
|
||||||
|
const message = this.chatInput.value;
|
||||||
|
this.sendToN8N(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправка в n8n
|
||||||
|
async sendToN8N(message) {
|
||||||
|
const context = this.getCurrentContext(); // Получаем данные проекта из CRM
|
||||||
|
|
||||||
|
const response = await fetch('/aiassist/n8n_proxy.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message,
|
||||||
|
context: context, // { projectId, module, userId, ... }
|
||||||
|
sessionId: this.sessionId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// data.task_id - уникальный ID задачи
|
||||||
|
// Подписываемся на SSE для получения ответа
|
||||||
|
this.startSSEListener(data.task_id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что отправляется в n8n:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Создай претензию по заливу квартиры...",
|
||||||
|
"context": {
|
||||||
|
"projectId": "123456",
|
||||||
|
"module": "Project",
|
||||||
|
"userId": "42",
|
||||||
|
"projectName": "Дело Иванова"
|
||||||
|
},
|
||||||
|
"sessionId": "ai-drawer-session-1234567890",
|
||||||
|
"taskId": "task-691209e225894-1762789858",
|
||||||
|
"redisChannel": "ai:response:task-691209e225894-1762789858"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 3: n8n обрабатывает запрос
|
||||||
|
|
||||||
|
**Workflow в n8n:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Webhook (получает запрос)
|
||||||
|
↓
|
||||||
|
2. AI Node (GPT-4) - анализирует запрос
|
||||||
|
↓
|
||||||
|
3. Определение типа документа
|
||||||
|
↓
|
||||||
|
4. Поиск данных в CRM (если нужно)
|
||||||
|
↓
|
||||||
|
5. Генерация текста документа
|
||||||
|
↓
|
||||||
|
6. Формирование переменных для шаблона
|
||||||
|
↓
|
||||||
|
7. HTTP Request → вызов API создания документа
|
||||||
|
↓
|
||||||
|
8. Публикация результата в Redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример обработки в n8n:**
|
||||||
|
|
||||||
|
**3.1. AI анализирует запрос:**
|
||||||
|
```
|
||||||
|
Промпт для GPT:
|
||||||
|
"Пользователь просит создать претензию.
|
||||||
|
Проанализируй запрос и определи:
|
||||||
|
- Тип документа (претензия/иск/жалоба)
|
||||||
|
- Данные клиента
|
||||||
|
- Данные ответчика
|
||||||
|
- Сумму ущерба
|
||||||
|
- Описание ситуации"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ AI:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document_type": "pretenziya",
|
||||||
|
"client_name": "Иванов Иван Иванович",
|
||||||
|
"respondent_name": "УК Жилищник",
|
||||||
|
"amount": "400000",
|
||||||
|
"situation": "Залив квартиры от стояка ХВС",
|
||||||
|
"claim_text": "УК отказывается возмещать ущерб..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.2. Определение шаблона:**
|
||||||
|
```javascript
|
||||||
|
// В n8n workflow
|
||||||
|
const templateMap = {
|
||||||
|
'pretenziya': 'pretenziya.docx',
|
||||||
|
'isk': 'iskovoe_zayavlenie.docx',
|
||||||
|
'zhaloba': 'zhaloba.docx'
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateName = templateMap[aiResponse.document_type];
|
||||||
|
// templateName = "pretenziya.docx"
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.3. Формирование переменных:**
|
||||||
|
```javascript
|
||||||
|
// В n8n workflow
|
||||||
|
const variables = {
|
||||||
|
CLIENT_NAME: aiResponse.client_name,
|
||||||
|
RESPONDENT_NAME: aiResponse.respondent_name,
|
||||||
|
DATE: new Date().toLocaleDateString('ru-RU'),
|
||||||
|
AMOUNT: aiResponse.amount,
|
||||||
|
CLAIM_TEXT: aiResponse.claim_text,
|
||||||
|
SITUATION: aiResponse.situation
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 4: n8n вызывает API создания документа
|
||||||
|
|
||||||
|
**HTTP Request в n8n:**
|
||||||
|
```javascript
|
||||||
|
// URL
|
||||||
|
https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php
|
||||||
|
|
||||||
|
// Метод: GET
|
||||||
|
// Параметры:
|
||||||
|
{
|
||||||
|
module: "Project",
|
||||||
|
recordId: "123456",
|
||||||
|
recordName: "Дело_Иванова",
|
||||||
|
fileName: "Претензия_УК_Жилищник",
|
||||||
|
templateName: "pretenziya.docx",
|
||||||
|
variables: JSON.stringify({
|
||||||
|
CLIENT_NAME: "Иванов Иван Иванович",
|
||||||
|
RESPONDENT_NAME: "УК Жилищник",
|
||||||
|
DATE: "15.01.2025",
|
||||||
|
AMOUNT: "400000",
|
||||||
|
CLAIM_TEXT: "УК отказывается возмещать ущерб..."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Полный URL:**
|
||||||
|
```
|
||||||
|
https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php?
|
||||||
|
module=Project&
|
||||||
|
recordId=123456&
|
||||||
|
recordName=Дело_Иванова&
|
||||||
|
fileName=Претензия_УК_Жилищник&
|
||||||
|
templateName=pretenziya.docx&
|
||||||
|
variables={"CLIENT_NAME":"Иванов Иван Иванович","DATE":"15.01.2025","AMOUNT":"400000",...}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 5: API создает документ
|
||||||
|
|
||||||
|
**Что делает `create_from_template.php`:**
|
||||||
|
|
||||||
|
**5.1. Скачивает шаблон из Nextcloud:**
|
||||||
|
```php
|
||||||
|
// WebDAV запрос к Nextcloud
|
||||||
|
$templateWebDAVUrl = 'https://office.clientright.ru:8443/remote.php/dav/files/admin/Templates/pretenziya.docx';
|
||||||
|
|
||||||
|
$ch = curl_init($templateWebDAVUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, "admin:office");
|
||||||
|
$templateContent = curl_exec($ch);
|
||||||
|
// Получили содержимое DOCX файла
|
||||||
|
```
|
||||||
|
|
||||||
|
**5.2. Заполняет переменные:**
|
||||||
|
```php
|
||||||
|
// Шаблон содержит:
|
||||||
|
// "Кому: {RESPONDENT_NAME}"
|
||||||
|
// "От: {CLIENT_NAME}"
|
||||||
|
// "Сумма: {AMOUNT} рублей"
|
||||||
|
|
||||||
|
// PHPWord заменяет переменные:
|
||||||
|
$filledContent = fillDocxTemplate($templateContent, $variables);
|
||||||
|
|
||||||
|
// Результат:
|
||||||
|
// "Кому: УК Жилищник"
|
||||||
|
// "От: Иванов Иван Иванович"
|
||||||
|
// "Сумма: 400000 рублей"
|
||||||
|
```
|
||||||
|
|
||||||
|
**5.3. Сохраняет готовый документ:**
|
||||||
|
```php
|
||||||
|
// Сохраняет в S3
|
||||||
|
$s3Path = "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx";
|
||||||
|
$s3Client->putObject([
|
||||||
|
'Bucket' => '...',
|
||||||
|
'Key' => $s3Path,
|
||||||
|
'Body' => $filledContent
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**5.4. Открывает документ в OnlyOffice:**
|
||||||
|
```php
|
||||||
|
// Редирект на открытие файла
|
||||||
|
header('Location: /crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 6: n8n публикует результат в Redis
|
||||||
|
|
||||||
|
**После создания документа:**
|
||||||
|
```javascript
|
||||||
|
// В n8n workflow
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
message: "Документ создан успешно",
|
||||||
|
documentUrl: "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
|
||||||
|
documentName: "Претензия_УК_Жилищник.docx"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Публикация в Redis
|
||||||
|
redis.publish('ai:response:task-691209e225894-1762789858', JSON.stringify(result));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ШАГ 7: AI Drawer получает ответ
|
||||||
|
|
||||||
|
**SSE слушает Redis:**
|
||||||
|
```javascript
|
||||||
|
// В ai-drawer-simple.js
|
||||||
|
startSSEListener(taskId) {
|
||||||
|
const eventSource = new EventSource(`/aiassist/ai_sse.php?task_id=${taskId}`);
|
||||||
|
|
||||||
|
eventSource.addEventListener('response', (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Показываем сообщение пользователю
|
||||||
|
this.addMessage('assistant', `✅ Документ создан: ${data.documentName}`);
|
||||||
|
|
||||||
|
// Можно добавить кнопку для открытия документа
|
||||||
|
this.addDocumentLink(data.documentUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пользователь видит:**
|
||||||
|
```
|
||||||
|
✅ Документ создан: Претензия_УК_Жилищник.docx
|
||||||
|
[Открыть документ] ← кнопка
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Визуальная схема процесса
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Пользователь│
|
||||||
|
│ AI Drawer │
|
||||||
|
└──────┬──────┘
|
||||||
|
│ "Создай претензию..."
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ n8n_proxy.php │
|
||||||
|
│ (генерирует │
|
||||||
|
│ task_id) │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│ POST {message, context}
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ n8n Workflow │
|
||||||
|
│ │
|
||||||
|
│ 1. AI анализирует│
|
||||||
|
│ 2. Определяет тип│
|
||||||
|
│ 3. Генерирует данные│
|
||||||
|
│ 4. Вызывает API │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│ GET /create_from_template.php
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ create_from_ │
|
||||||
|
│ template.php │
|
||||||
|
│ │
|
||||||
|
│ 1. Скачивает │
|
||||||
|
│ шаблон │
|
||||||
|
│ 2. Заполняет │
|
||||||
|
│ переменные │
|
||||||
|
│ 3. Сохраняет │
|
||||||
|
│ в S3 │
|
||||||
|
│ 4. Открывает │
|
||||||
|
│ в OnlyOffice │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│ Результат
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Redis Pub/Sub │
|
||||||
|
│ ai:response: │
|
||||||
|
│ {taskId} │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│ SSE событие
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ AI Drawer (SSE) │
|
||||||
|
│ Показывает │
|
||||||
|
│ результат │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Пример полного запроса
|
||||||
|
|
||||||
|
### Запрос пользователя:
|
||||||
|
```
|
||||||
|
"Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
|
||||||
|
ответчик УК Жилищник, клиент Иванов Иван Иванович"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что происходит:
|
||||||
|
|
||||||
|
1. **AI Drawer → n8n:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Создай претензию...",
|
||||||
|
"context": {"projectId": "123456", "module": "Project"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **n8n → AI (GPT-4):**
|
||||||
|
```
|
||||||
|
"Проанализируй запрос и определи тип документа и данные"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **AI → n8n:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document_type": "pretenziya",
|
||||||
|
"client_name": "Иванов Иван Иванович",
|
||||||
|
"respondent_name": "УК Жилищник",
|
||||||
|
"amount": "400000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **n8n → API создания документа:**
|
||||||
|
```
|
||||||
|
GET /create_from_template.php?
|
||||||
|
module=Project&
|
||||||
|
recordId=123456&
|
||||||
|
fileName=Претензия_УК_Жилищник&
|
||||||
|
templateName=pretenziya.docx&
|
||||||
|
variables={"CLIENT_NAME":"Иванов Иван Иванович",...}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **API → Nextcloud:**
|
||||||
|
```
|
||||||
|
WebDAV GET /Templates/pretenziya.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **API → PHPWord:**
|
||||||
|
```
|
||||||
|
Заменяет {CLIENT_NAME} → "Иванов Иван Иванович"
|
||||||
|
Заменяет {AMOUNT} → "400000"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **API → S3:**
|
||||||
|
```
|
||||||
|
PUT crm2/CRM_Active_Files/Documents/Project/.../Претензия_УК_Жилищник.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **n8n → Redis:**
|
||||||
|
```
|
||||||
|
PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **SSE → AI Drawer:**
|
||||||
|
```
|
||||||
|
Показывает: "✅ Документ создан: Претензия_УК_Жилищник.docx"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Ключевые моменты
|
||||||
|
|
||||||
|
1. **AI не создает документ напрямую** - он только анализирует запрос и генерирует данные
|
||||||
|
2. **n8n координирует процесс** - вызывает API создания документа
|
||||||
|
3. **API работает с шаблонами** - скачивает, заполняет, сохраняет
|
||||||
|
4. **Результат возвращается через SSE** - пользователь видит ответ в реальном времени
|
||||||
|
|
||||||
|
## 🎯 Преимущества такого подхода
|
||||||
|
|
||||||
|
✅ **Разделение ответственности:**
|
||||||
|
- AI анализирует и генерирует данные
|
||||||
|
- n8n координирует процесс
|
||||||
|
- API работает с файлами
|
||||||
|
|
||||||
|
✅ **Гибкость:**
|
||||||
|
- Легко добавить новые типы документов
|
||||||
|
- Легко изменить шаблоны
|
||||||
|
- Легко добавить новые источники данных
|
||||||
|
|
||||||
|
✅ **Надежность:**
|
||||||
|
- Каждый компонент можно тестировать отдельно
|
||||||
|
- Ошибки изолированы
|
||||||
|
- Легко отлаживать
|
||||||
|
|
||||||
199
crm_extensions/file_storage/docs/AI_DOCUMENT_TOOL.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# 🛠️ Инструмент для AI: Создание документов
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Готово к использованию
|
||||||
|
|
||||||
|
## 🎯 Назначение
|
||||||
|
|
||||||
|
Простой инструмент для AI Ассистента, который:
|
||||||
|
1. Создает пустой DOCX/XLSX/PPTX файл
|
||||||
|
2. Записывает в него текст, сгенерированный AI
|
||||||
|
3. Сохраняет в папку проекта
|
||||||
|
4. Возвращает ссылку на редактирование
|
||||||
|
|
||||||
|
## 📍 Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /crm_extensions/file_storage/api/create_document_with_text.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📥 Параметры запроса
|
||||||
|
|
||||||
|
### Обязательные:
|
||||||
|
- `module` - модуль CRM (Project, Contacts, Accounts, etc.)
|
||||||
|
- `recordId` - ID записи (проекта, контакта и т.д.)
|
||||||
|
- `recordName` - название записи (для формирования папки)
|
||||||
|
- `fileName` - имя создаваемого файла (без расширения)
|
||||||
|
- `documentText` - текст документа, который нужно записать
|
||||||
|
|
||||||
|
### Опциональные:
|
||||||
|
- `documentType` - тип документа: `docx` (по умолчанию), `xlsx`, `pptx`
|
||||||
|
|
||||||
|
## 📤 Ответ
|
||||||
|
|
||||||
|
### Успешный ответ:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Документ создан успешно",
|
||||||
|
"documentName": "Претензия_УК_Жилищник.docx",
|
||||||
|
"documentUrl": "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
|
||||||
|
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...",
|
||||||
|
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Использование в n8n
|
||||||
|
|
||||||
|
### Пример HTTP Request узла в n8n:
|
||||||
|
|
||||||
|
**URL:**
|
||||||
|
```
|
||||||
|
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method:** `POST`
|
||||||
|
|
||||||
|
**Body (JSON):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "{{ $json.body.context.module }}",
|
||||||
|
"recordId": "{{ $json.body.context.projectId }}",
|
||||||
|
"recordName": "{{ $json.body.context.projectName }}",
|
||||||
|
"fileName": "{{ $json.body.documentName }}",
|
||||||
|
"documentText": "{{ $json.body.generatedText }}",
|
||||||
|
"documentType": "docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Или через Query Parameters (GET):**
|
||||||
|
```
|
||||||
|
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php?
|
||||||
|
module={{ $json.body.context.module }}&
|
||||||
|
recordId={{ $json.body.context.projectId }}&
|
||||||
|
recordName={{ $json.body.context.projectName }}&
|
||||||
|
fileName={{ $json.body.documentName }}&
|
||||||
|
documentText={{ $json.body.generatedText }}&
|
||||||
|
documentType=docx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Пример полного workflow в n8n
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Webhook (получает запрос от AI Drawer)
|
||||||
|
↓
|
||||||
|
2. AI Node (GPT-4) - генерирует текст документа
|
||||||
|
↓
|
||||||
|
3. HTTP Request → create_document_with_text.php
|
||||||
|
Body: {
|
||||||
|
module: "Project",
|
||||||
|
recordId: "123456",
|
||||||
|
recordName: "Дело Иванова",
|
||||||
|
fileName: "Претензия_УК_Жилищник",
|
||||||
|
documentText: "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\n..."
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
4. Получаем ответ:
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
documentName: "Претензия_УК_Жилищник.docx",
|
||||||
|
editUrl: "https://..."
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
5. Формируем сообщение для пользователя:
|
||||||
|
"✅ Документ создан: Претензия_УК_Жилищник.docx\n[Открыть для редактирования]"
|
||||||
|
↓
|
||||||
|
6. Публикуем в Redis: ai:response:{taskId}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💬 Пример ответа AI пользователю
|
||||||
|
|
||||||
|
**После создания документа:**
|
||||||
|
```
|
||||||
|
✅ Документ создан: Претензия_УК_Жилищник.docx
|
||||||
|
|
||||||
|
Документ сохранен в папку проекта и готов к редактированию.
|
||||||
|
Вы можете открыть его для просмотра и внесения изменений.
|
||||||
|
|
||||||
|
[Открыть документ] ← ссылка на editUrl
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Форматирование текста
|
||||||
|
|
||||||
|
### DOCX:
|
||||||
|
- Текст разбивается на параграфы по переносам строк (`\n`)
|
||||||
|
- Каждый параграф отделяется пустой строкой
|
||||||
|
- Шрифт: Times New Roman, 12pt
|
||||||
|
- Поля: 2 см сверху/справа/снизу, 3 см слева
|
||||||
|
|
||||||
|
### XLSX:
|
||||||
|
- Весь текст записывается в ячейку A1
|
||||||
|
- Автоподбор ширины колонки
|
||||||
|
|
||||||
|
### PPTX:
|
||||||
|
- Текст размещается на первом слайде
|
||||||
|
- Разбивается на параграфы
|
||||||
|
|
||||||
|
## 🔍 Примеры использования
|
||||||
|
|
||||||
|
### Пример 1: Создание претензии
|
||||||
|
|
||||||
|
**Запрос в n8n:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Дело Иванова",
|
||||||
|
"fileName": "Претензия_УК_Жилищник",
|
||||||
|
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК \"Жилищник\"\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры...\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу\n\nС уважением,\nИванов Иван Иванович"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
- Создан файл `Претензия_УК_Жилищник.docx`
|
||||||
|
- Сохранен в папку проекта
|
||||||
|
- Возвращена ссылка на редактирование
|
||||||
|
|
||||||
|
### Пример 2: Создание иска
|
||||||
|
|
||||||
|
**Запрос:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Дело Иванова",
|
||||||
|
"fileName": "Исковое_заявление",
|
||||||
|
"documentText": "ИСКОВОЕ ЗАЯВЛЕНИЕ\n\nВ суд: ...\n\nИстец: Иванов Иван Иванович\nОтветчик: УК \"Жилищник\"\n\n...",
|
||||||
|
"documentType": "docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Ограничения
|
||||||
|
|
||||||
|
1. **Максимальный размер текста:** Ограничен памятью PHP (обычно 128MB+)
|
||||||
|
2. **Форматирование:** Базовое форматирование (параграфы, переносы строк)
|
||||||
|
3. **Таблицы/изображения:** Не поддерживаются в упрощенной версии
|
||||||
|
|
||||||
|
## 🚀 Следующие шаги
|
||||||
|
|
||||||
|
После MVP можно добавить:
|
||||||
|
1. Поддержку шаблонов (заполнение переменных)
|
||||||
|
2. Расширенное форматирование (жирный, курсив, списки)
|
||||||
|
3. Таблицы и изображения
|
||||||
|
4. Автоматическое определение типа документа
|
||||||
|
|
||||||
|
## 📝 Примечания
|
||||||
|
|
||||||
|
- Файл сохраняется в S3
|
||||||
|
- Событие публикуется в Redis для индексации
|
||||||
|
- Документ сразу доступен для редактирования в OnlyOffice
|
||||||
|
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`
|
||||||
|
|
||||||
200
crm_extensions/file_storage/docs/AI_DOCUMENT_TOOL_INSTRUCTION.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# 📄 Инструмент создания документов для AI Ассистента
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Создает документ (DOCX/XLSX/PPTX) с текстом, сгенерированным AI, и сохраняет его в папку проекта в CRM. Документ сразу доступен для редактирования в OnlyOffice.
|
||||||
|
|
||||||
|
**Процесс:**
|
||||||
|
1. Создает пустой документ выбранного типа (DOCX по умолчанию)
|
||||||
|
2. Записывает в него текст, сгенерированный AI
|
||||||
|
3. Сохраняет в S3 в папку проекта: `{module}/{recordName}_{recordId}/{fileName}.{ext}`
|
||||||
|
4. Публикует событие в Redis для индексации
|
||||||
|
5. Возвращает ссылку на редактирование в OnlyOffice
|
||||||
|
|
||||||
|
**Форматирование:**
|
||||||
|
- DOCX: текст разбивается на параграфы по переносам строк (`\n`), шрифт Times New Roman 12pt, стандартные поля
|
||||||
|
- XLSX: весь текст записывается в ячейку A1
|
||||||
|
- PPTX: текст размещается на первом слайде
|
||||||
|
|
||||||
|
## Входные параметры
|
||||||
|
|
||||||
|
**URL:** `https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php`
|
||||||
|
|
||||||
|
**Method:** `POST`
|
||||||
|
|
||||||
|
**Content-Type:** `application/json`
|
||||||
|
|
||||||
|
### Обязательные параметры:
|
||||||
|
|
||||||
|
- `module` (string) — модуль CRM, где создается документ:
|
||||||
|
- `"Project"` — для проектов
|
||||||
|
- `"Contacts"` — для контактов
|
||||||
|
- `"Accounts"` — для организаций
|
||||||
|
- `"Invoice"`, `"Quotes"`, `"SalesOrder"`, `"PurchaseOrder"`, `"HelpDesk"`, `"Leads"`, `"Potentials"` — для других модулей
|
||||||
|
|
||||||
|
- `recordId` (string) — ID записи в CRM (проекта, контакта и т.д.), к которой привязывается документ
|
||||||
|
|
||||||
|
- `recordName` (string) — название записи (используется для формирования имени папки). Спецсимволы будут заменены на подчеркивания
|
||||||
|
|
||||||
|
- `fileName` (string) — имя создаваемого файла без расширения (например: `"Претензия_УК_Жилищник"`). Расширение добавится автоматически
|
||||||
|
|
||||||
|
- `documentText` (string) — текст документа, который нужно записать. **Поддерживается Markdown форматирование:**
|
||||||
|
- Заголовки: `# H1`, `## H2`, `### H3`
|
||||||
|
- Жирный: `**текст**` или `__текст__`
|
||||||
|
- Курсив: `*текст*` или `_текст_`
|
||||||
|
- Код: `` `текст` ``
|
||||||
|
- Маркированные списки: `- пункт` или `* пункт`
|
||||||
|
- Нумерованные списки: `1. пункт`
|
||||||
|
- Поддерживаются переносы строк (`\n`) для разделения на параграфы
|
||||||
|
|
||||||
|
### Опциональные параметры:
|
||||||
|
|
||||||
|
- `documentType` (string, по умолчанию `"docx"`) — тип документа:
|
||||||
|
- `"docx"` — Word документ (рекомендуется для текстовых документов)
|
||||||
|
- `"xlsx"` — Excel таблица (для табличных данных)
|
||||||
|
- `"pptx"` — PowerPoint презентация (для презентаций)
|
||||||
|
|
||||||
|
## Что возвращает
|
||||||
|
|
||||||
|
### Успешный ответ:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Документ создан успешно",
|
||||||
|
"documentName": "Претензия_УК_Жилищник.docx",
|
||||||
|
"documentUrl": "https://s3.twcstorage.ru/bucket/path/to/file.docx",
|
||||||
|
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=https://s3.twcstorage.ru/...",
|
||||||
|
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поля ответа:**
|
||||||
|
- `success` (boolean) — `true` если документ создан успешно
|
||||||
|
- `message` (string) — сообщение о результате
|
||||||
|
- `documentName` (string) — имя созданного файла с расширением
|
||||||
|
- `documentUrl` (string) — прямой URL файла в S3
|
||||||
|
- `editUrl` (string) — URL для открытия документа в OnlyOffice (используй эту ссылку для пользователя)
|
||||||
|
- `path` (string) — путь к файлу в S3 (для внутреннего использования)
|
||||||
|
|
||||||
|
### Ошибка:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
**Используй этот инструмент когда:**
|
||||||
|
|
||||||
|
1. Пользователь просит создать документ (претензию, иск, жалобу, ходатайство и т.д.)
|
||||||
|
2. Ты уже сгенерировал текст документа и готов его сохранить
|
||||||
|
3. Нужно сохранить документ в папку проекта в CRM
|
||||||
|
4. Пользователь должен иметь возможность редактировать документ в OnlyOffice
|
||||||
|
|
||||||
|
**Примеры запросов пользователя:**
|
||||||
|
- "Создай претензию по заливу квартиры"
|
||||||
|
- "Подготовь исковое заявление"
|
||||||
|
- "Сформируй жалобу в прокуратуру"
|
||||||
|
- "Напиши ходатайство о приостановлении дела"
|
||||||
|
|
||||||
|
**Не используй когда:**
|
||||||
|
- Пользователь просто спрашивает информацию (без создания документа)
|
||||||
|
- Нужно только показать текст без сохранения
|
||||||
|
- Документ должен быть создан из шаблона с переменными (используй другой инструмент)
|
||||||
|
|
||||||
|
## Пример использования
|
||||||
|
|
||||||
|
### Запрос пользователя:
|
||||||
|
```
|
||||||
|
"Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
|
||||||
|
ответчик УК Жилищник, клиент Иванов Иван Иванович"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Твой ответ (после генерации текста):
|
||||||
|
|
||||||
|
**1. Вызываешь инструмент:**
|
||||||
|
```json
|
||||||
|
POST /crm_extensions/file_storage/api/create_document_with_text.php
|
||||||
|
{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Дело Иванова",
|
||||||
|
"fileName": "Претензия_УК_Жилищник",
|
||||||
|
"documentText": "# ПРЕТЕНЗИЯ\n\nКому: **УК \\\"Жилищник\\\"**\nОт: *Иванов Иван Иванович*\n\nДата: 15.01.2025\n\n## Текст претензии\n\nУК отказывается возмещать ущерб от залива квартиры от стояка ХВС. Ущерб составляет `400000` рублей.\n\n## Требования:\n\n1. Возместить ущерб в размере **400000 рублей**\n2. Провести экспертизу для оценки ущерба\n\nС уважением,\n**Иванов Иван Иванович**"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Получаешь ответ:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"documentName": "Претензия_УК_Жилищник.docx",
|
||||||
|
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Сообщаешь пользователю:**
|
||||||
|
```
|
||||||
|
✅ Документ создан: Претензия_УК_Жилищник.docx
|
||||||
|
|
||||||
|
Документ сохранен в папку проекта и готов к редактированию.
|
||||||
|
Вы можете открыть его для просмотра и внесения изменений.
|
||||||
|
|
||||||
|
[Открыть документ](editUrl)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
1. **Всегда используй `editUrl`** для ссылки пользователю — это откроет документ в OnlyOffice
|
||||||
|
2. **Имя файла должно быть уникальным** — если файл с таким именем уже существует, он будет перезаписан
|
||||||
|
3. **Текст документа** должен быть готовым к использованию — инструмент не редактирует текст, только записывает его
|
||||||
|
4. **Переносы строк** (`\n`) в `documentText` создают новые параграфы в DOCX
|
||||||
|
5. **Документ сохраняется сразу** — откатить операцию нельзя, убедись что данные корректны
|
||||||
|
|
||||||
|
## Поддержка форматирования
|
||||||
|
|
||||||
|
**API поддерживает Markdown форматирование:**
|
||||||
|
|
||||||
|
- ✅ **Заголовки**: `# H1`, `## H2`, `### H3` — автоматически форматируются как заголовки
|
||||||
|
- ✅ **Жирный текст**: `**текст**` или `__текст__` — выделение важной информации
|
||||||
|
- ✅ **Курсив**: `*текст*` или `_текст_` — акценты
|
||||||
|
- ✅ **Код**: `` `текст` `` — статьи, суммы, технические данные (Courier New, синий)
|
||||||
|
- ✅ **Маркированные списки**: `- пункт` или `* пункт` — автоматические отступы
|
||||||
|
- ✅ **Нумерованные списки**: `1. пункт` — автоматическая нумерация
|
||||||
|
|
||||||
|
**Пример использования:**
|
||||||
|
```markdown
|
||||||
|
# ПРЕТЕНЗИЯ
|
||||||
|
|
||||||
|
Кому: **УК "Жилищник"**
|
||||||
|
От: *Иванов Иван Иванович*
|
||||||
|
|
||||||
|
## Требования:
|
||||||
|
|
||||||
|
1. Возместить ущерб **400000 рублей**
|
||||||
|
2. Провести экспертизу
|
||||||
|
|
||||||
|
- Дополнительно
|
||||||
|
- Еще пункт
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Максимальный размер текста ограничен памятью PHP (обычно 128MB+)
|
||||||
|
- Таблицы не поддерживаются (можно использовать списки)
|
||||||
|
- Изображения не поддерживаются
|
||||||
|
- Вложенные списки не поддерживаются (только один уровень)
|
||||||
|
- Ссылки не поддерживаются (можно использовать код `` `текст` ``)
|
||||||
|
|
||||||
|
## Следующие шаги после создания
|
||||||
|
|
||||||
|
После создания документа пользователь может:
|
||||||
|
1. Открыть документ по ссылке `editUrl` в OnlyOffice
|
||||||
|
2. Редактировать текст, форматирование, добавлять таблицы и изображения
|
||||||
|
3. Сохранить изменения (автоматически сохраняется в S3)
|
||||||
|
4. Экспортировать в PDF через OnlyOffice
|
||||||
|
|
||||||
246
crm_extensions/file_storage/docs/MARKDOWN_FORMATTING.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# 📝 Поддержка форматирования Markdown в документах
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Реализовано
|
||||||
|
|
||||||
|
## 🎯 Обзор
|
||||||
|
|
||||||
|
API создания документов теперь поддерживает **Markdown форматирование**! AI может использовать стандартный Markdown синтаксис для создания красиво оформленных документов.
|
||||||
|
|
||||||
|
## ✨ Поддерживаемые элементы форматирования
|
||||||
|
|
||||||
|
### 1. Заголовки
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Заголовок 1 уровня (H1) - размер 18pt, жирный
|
||||||
|
## Заголовок 2 уровня (H2) - размер 16pt, жирный
|
||||||
|
### Заголовок 3 уровня (H3) - размер 14pt, жирный
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
# ПРЕТЕНЗИЯ
|
||||||
|
## Текст претензии
|
||||||
|
### Требования
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Жирный текст
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**жирный текст**
|
||||||
|
__жирный текст__
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
Кому: **УК "Жилищник"**
|
||||||
|
Сумма: __400000 рублей__
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Курсив
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
*курсив*
|
||||||
|
_курсив_
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
От: *Иванов Иван Иванович*
|
||||||
|
Дата: _15.01.2025_
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Выделение кода
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
`код`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
Сумма ущерба: `400000` рублей
|
||||||
|
Статья: `ст. 1064 ГК РФ`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Маркированные списки
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- Первый пункт
|
||||||
|
- Второй пункт
|
||||||
|
- Третий пункт
|
||||||
|
|
||||||
|
* Альтернативный маркер
|
||||||
|
* Еще один пункт
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
Требования:
|
||||||
|
- Возместить ущерб
|
||||||
|
- Провести экспертизу
|
||||||
|
- Подготовить документы
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Нумерованные списки
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
1. Первый пункт
|
||||||
|
2. Второй пункт
|
||||||
|
3. Третий пункт
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```markdown
|
||||||
|
Порядок действий:
|
||||||
|
1. Подать претензию
|
||||||
|
2. Дождаться ответа
|
||||||
|
3. При необходимости обратиться в суд
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Пример полного документа с форматированием
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# ПРЕТЕНЗИЯ
|
||||||
|
|
||||||
|
## Заголовок раздела
|
||||||
|
|
||||||
|
Кому: **УК "Жилищник"**
|
||||||
|
От: *Иванов Иван Иванович*
|
||||||
|
|
||||||
|
Дата: 15.01.2025
|
||||||
|
|
||||||
|
### Текст претензии
|
||||||
|
|
||||||
|
УК отказывается возмещать ущерб от залива квартиры от стояка ХВС.
|
||||||
|
Ущерб составляет `400000` рублей.
|
||||||
|
|
||||||
|
### Требования:
|
||||||
|
|
||||||
|
1. Возместить ущерб в размере **400000 рублей**
|
||||||
|
2. Провести экспертизу для оценки ущерба
|
||||||
|
3. Возместить моральный вред
|
||||||
|
|
||||||
|
### Дополнительно:
|
||||||
|
|
||||||
|
- Провести экспертизу
|
||||||
|
- Оценить ущерб
|
||||||
|
- Подготовить документы
|
||||||
|
|
||||||
|
С уважением,
|
||||||
|
**Иванов Иван Иванович**
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Как это выглядит в документе
|
||||||
|
|
||||||
|
### Заголовки:
|
||||||
|
- **H1** (#) — крупный заголовок, 18pt, жирный, отступ сверху
|
||||||
|
- **H2** (##) — средний заголовок, 16pt, жирный
|
||||||
|
- **H3** (###) — маленький заголовок, 14pt, жирный
|
||||||
|
|
||||||
|
### Текст:
|
||||||
|
- **Жирный** — выделение важной информации
|
||||||
|
- *Курсив* — акценты, названия
|
||||||
|
- `Код` — статьи, суммы, технические данные (Courier New, синий цвет)
|
||||||
|
|
||||||
|
### Списки:
|
||||||
|
- Маркированные — с символом •, отступ слева
|
||||||
|
- Нумерованные — с автоматической нумерацией, отступ слева
|
||||||
|
|
||||||
|
## 💡 Рекомендации для AI
|
||||||
|
|
||||||
|
### Когда использовать форматирование:
|
||||||
|
|
||||||
|
1. **Заголовки** — для структурирования документа:
|
||||||
|
```markdown
|
||||||
|
# ПРЕТЕНЗИЯ
|
||||||
|
## Текст претензии
|
||||||
|
## Требования
|
||||||
|
## Приложения
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Жирный текст** — для важной информации:
|
||||||
|
```markdown
|
||||||
|
Кому: **УК "Жилищник"**
|
||||||
|
Сумма: **400000 рублей**
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Списки** — для перечислений:
|
||||||
|
```markdown
|
||||||
|
Требования:
|
||||||
|
1. Возместить ущерб
|
||||||
|
2. Провести экспертизу
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Код** — для статей, сумм, ссылок:
|
||||||
|
```markdown
|
||||||
|
Ссылка на право: `ст. 1064 ГК РФ`
|
||||||
|
Сумма: `400000` рублей
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример использования в AI ответе:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# ПРЕТЕНЗИЯ
|
||||||
|
|
||||||
|
Кому: **УК "Жилищник"**
|
||||||
|
От: *Иванов Иван Иванович*
|
||||||
|
|
||||||
|
Дата: 15.01.2025
|
||||||
|
|
||||||
|
## Текст претензии
|
||||||
|
|
||||||
|
УК отказывается возмещать ущерб от залива квартиры от стояка ХВС.
|
||||||
|
Ущерб составляет `400000` рублей.
|
||||||
|
|
||||||
|
## Требования:
|
||||||
|
|
||||||
|
1. Возместить ущерб в размере **400000 рублей**
|
||||||
|
2. Провести экспертизу для оценки ущерба
|
||||||
|
3. Возместить моральный вред
|
||||||
|
|
||||||
|
## Ссылки на право:
|
||||||
|
|
||||||
|
- `ст. 1064 ГК РФ` - общие основания ответственности за вред
|
||||||
|
- `ст. 15 ГК РФ` - возмещение убытков
|
||||||
|
|
||||||
|
С уважением,
|
||||||
|
**Иванов Иван Иванович**
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Ограничения
|
||||||
|
|
||||||
|
1. **Вложенные списки** — не поддерживаются (только один уровень)
|
||||||
|
2. **Таблицы** — не поддерживаются (можно использовать списки)
|
||||||
|
3. **Изображения** — не поддерживаются
|
||||||
|
4. **Ссылки** — не поддерживаются (можно использовать код `[текст](url)`)
|
||||||
|
5. **Комбинированное форматирование** — `**жирный *курсив* текст**` работает частично
|
||||||
|
|
||||||
|
## 🔧 Технические детали
|
||||||
|
|
||||||
|
- Парсинг выполняется построчно
|
||||||
|
- Поддерживается комбинирование форматирования в одном параграфе
|
||||||
|
- Списки автоматически завершаются при появлении обычного текста
|
||||||
|
- Пустые строки создают отступы между блоками
|
||||||
|
|
||||||
|
## 📚 Справочник Markdown для AI
|
||||||
|
|
||||||
|
Используй эти элементы при генерации документов:
|
||||||
|
|
||||||
|
| Элемент | Синтаксис | Пример |
|
||||||
|
|---------|-----------|--------|
|
||||||
|
| Заголовок H1 | `# Текст` | `# ПРЕТЕНЗИЯ` |
|
||||||
|
| Заголовок H2 | `## Текст` | `## Требования` |
|
||||||
|
| Заголовок H3 | `### Текст` | `### Дополнительно` |
|
||||||
|
| Жирный | `**текст**` | `**400000 рублей**` |
|
||||||
|
| Курсив | `*текст*` | `*Иванов Иван Иванович*` |
|
||||||
|
| Код | `` `текст` `` | `` `ст. 1064 ГК РФ` `` |
|
||||||
|
| Маркированный список | `- пункт` | `- Первый пункт` |
|
||||||
|
| Нумерованный список | `1. пункт` | `1. Первый пункт` |
|
||||||
|
|
||||||
|
## ✅ Преимущества
|
||||||
|
|
||||||
|
1. **Стандартный синтаксис** — Markdown понимают все AI модели
|
||||||
|
2. **Читаемость** — легко читать и редактировать
|
||||||
|
3. **Гибкость** — можно комбинировать элементы
|
||||||
|
4. **Автоматическое форматирование** — документ получается красивым без ручной правки
|
||||||
|
|
||||||
154
crm_extensions/file_storage/docs/N8N_HTTP_REQUEST_CURL.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 🔧 cURL для n8n HTTP Request ноды
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Назначение:** Тестирование и настройка HTTP Request ноды в n8n
|
||||||
|
|
||||||
|
## 📍 Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 cURL команда для тестирования
|
||||||
|
|
||||||
|
### Базовый пример:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Тестовый проект",
|
||||||
|
"fileName": "Тестовый_документ",
|
||||||
|
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры.\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу\n\nС уважением,\nИванов Иван Иванович"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### С documentType:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Тестовый проект",
|
||||||
|
"fileName": "Тестовый_документ",
|
||||||
|
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\n...",
|
||||||
|
"documentType": "docx"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Для импорта в n8n HTTP Request ноду
|
||||||
|
|
||||||
|
### Настройки ноды:
|
||||||
|
|
||||||
|
**Method:** `POST`
|
||||||
|
|
||||||
|
**URL:**
|
||||||
|
```
|
||||||
|
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication:** None
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body (JSON):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "{{ $json.body.context.module }}",
|
||||||
|
"recordId": "{{ $json.body.context.projectId }}",
|
||||||
|
"recordName": "{{ $json.body.context.projectName }}",
|
||||||
|
"fileName": "{{ $json.body.documentName }}",
|
||||||
|
"documentText": "{{ $json.body.generatedText }}",
|
||||||
|
"documentType": "docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Примеры с реальными данными
|
||||||
|
|
||||||
|
### Пример 1: Претензия
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "390657",
|
||||||
|
"recordName": "Дело Иванова",
|
||||||
|
"fileName": "Претензия_УК_Жилищник",
|
||||||
|
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК \"Жилищник\"\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры от стояка ХВС. Ущерб составляет 400000 рублей.\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу для оценки ущерба\n3. Возместить моральный вред\n\nС уважением,\nИванов Иван Иванович"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 2: Исковое заявление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "390657",
|
||||||
|
"recordName": "Дело Иванова",
|
||||||
|
"fileName": "Исковое_заявление",
|
||||||
|
"documentText": "ИСКОВОЕ ЗАЯВЛЕНИЕ\n\nВ суд: Районный суд г. Москвы\n\nИстец: Иванов Иван Иванович\nОтветчик: УК \"Жилищник\"\n\nЦена иска: 400000 рублей\n\nИсковые требования:\n1. Взыскать с ответчика 400000 рублей в счет возмещения ущерба\n2. Взыскать госпошлину\n\nОбстоятельства дела:\n..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Проверка ответа
|
||||||
|
|
||||||
|
### Успешный ответ:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Документ создан успешно",
|
||||||
|
"documentName": "Претензия_УК_Жилищник.docx",
|
||||||
|
"documentUrl": "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
|
||||||
|
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...",
|
||||||
|
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 n8n Expression для Body
|
||||||
|
|
||||||
|
Если используете выражения n8n в Body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "{{ $json.body.context.module || 'Project' }}",
|
||||||
|
"recordId": "{{ $json.body.context.projectId }}",
|
||||||
|
"recordName": "{{ $json.body.context.projectName || 'Проект' }}",
|
||||||
|
"fileName": "{{ $json.body.documentName || 'Документ_' + Date.now() }}",
|
||||||
|
"documentText": "{{ $json.body.generatedText }}",
|
||||||
|
"documentType": "{{ $json.body.documentType || 'docx' }}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Быстрый тест
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Минимальный тест
|
||||||
|
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"module": "Project",
|
||||||
|
"recordId": "123456",
|
||||||
|
"recordName": "Тест",
|
||||||
|
"fileName": "Тест",
|
||||||
|
"documentText": "Тестовый документ\n\nЭто тест создания документа через API."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
213
crm_extensions/file_storage/docs/NEXTCLOUD_TEMPLATES.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# 📋 Настройка шаблонов документов в Nextcloud
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Готово к использованию
|
||||||
|
|
||||||
|
## 🎯 Обзор
|
||||||
|
|
||||||
|
Для генерации документов из шаблонов используется гибридный подход:
|
||||||
|
1. **Шаблоны хранятся в Nextcloud** в папке `/crm/Templates/`
|
||||||
|
2. **Заполнение переменных** происходит через PHPWord
|
||||||
|
3. **Готовый документ** сохраняется в папку проекта и открывается в OnlyOffice
|
||||||
|
|
||||||
|
## 📁 Структура шаблонов
|
||||||
|
|
||||||
|
### 1. Создание папки для шаблонов
|
||||||
|
|
||||||
|
В Nextcloud создайте папку:
|
||||||
|
```
|
||||||
|
/crm/Templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Как создать:**
|
||||||
|
1. Зайдите в Nextcloud: `https://office.clientright.ru:8443`
|
||||||
|
2. Перейдите в папку `/crm/`
|
||||||
|
3. Создайте папку `Templates`
|
||||||
|
4. Загрузите туда типовые документы
|
||||||
|
|
||||||
|
### 2. Формат шаблонов
|
||||||
|
|
||||||
|
#### Формат переменных в шаблонах:
|
||||||
|
|
||||||
|
**Вариант 1: Простые переменные**
|
||||||
|
```
|
||||||
|
{CLIENT_NAME}
|
||||||
|
{DATE}
|
||||||
|
{AMOUNT}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вариант 2: Двойные фигурные скобки**
|
||||||
|
```
|
||||||
|
{{CLIENT_NAME}}
|
||||||
|
{{DATE}}
|
||||||
|
{{AMOUNT}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример шаблона претензии:
|
||||||
|
|
||||||
|
```docx
|
||||||
|
ПРЕТЕНЗИЯ
|
||||||
|
|
||||||
|
Кому: {RESPONDENT_NAME}
|
||||||
|
От: {CLIENT_NAME}
|
||||||
|
|
||||||
|
Дата: {DATE}
|
||||||
|
|
||||||
|
Текст претензии:
|
||||||
|
{CLAIM_TEXT}
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
1. Возместить ущерб в размере {AMOUNT} рублей
|
||||||
|
2. {OTHER_REQUIREMENTS}
|
||||||
|
|
||||||
|
С уважением,
|
||||||
|
{CLIENT_NAME}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Использование API
|
||||||
|
|
||||||
|
### Endpoint: `/crm_extensions/file_storage/api/create_from_template.php`
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
- `module` - модуль CRM (Project, Contacts, etc.)
|
||||||
|
- `recordId` - ID записи
|
||||||
|
- `recordName` - название записи
|
||||||
|
- `fileName` - имя создаваемого файла
|
||||||
|
- `templateName` - имя шаблона из Nextcloud (например, `pretenziya.docx`)
|
||||||
|
- `variables` - JSON объект с переменными для заполнения
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
```javascript
|
||||||
|
const url = `/crm_extensions/file_storage/api/create_from_template.php?` +
|
||||||
|
`module=Project&` +
|
||||||
|
`recordId=123456&` +
|
||||||
|
`recordName=Проект_1&` +
|
||||||
|
`fileName=Претензия_УК&` +
|
||||||
|
`templateName=pretenziya.docx&` +
|
||||||
|
`variables=${encodeURIComponent(JSON.stringify({
|
||||||
|
CLIENT_NAME: 'Иванов Иван Иванович',
|
||||||
|
DATE: '15.01.2025',
|
||||||
|
AMOUNT: '400000',
|
||||||
|
RESPONDENT_NAME: 'УК "Жилищник"',
|
||||||
|
CLAIM_TEXT: 'УК отказывается возмещать ущерб от залива квартиры...',
|
||||||
|
OTHER_REQUIREMENTS: 'Провести экспертизу'
|
||||||
|
}))}`;
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Интеграция с AI Drawer
|
||||||
|
|
||||||
|
### Пример использования в n8n:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// После генерации текста AI
|
||||||
|
const aiResponse = {
|
||||||
|
document_type: 'pretenziya',
|
||||||
|
client_name: 'Иванов Иван Иванович',
|
||||||
|
amount: '400000',
|
||||||
|
claim_text: '...',
|
||||||
|
// ... другие данные
|
||||||
|
};
|
||||||
|
|
||||||
|
// Определяем шаблон по типу документа
|
||||||
|
const templateMap = {
|
||||||
|
'pretenziya': 'pretenziya.docx',
|
||||||
|
'isk': 'iskovoe_zayavlenie.docx',
|
||||||
|
'zhaloba': 'zhaloba.docx',
|
||||||
|
'hodataystvo': 'hodataystvo.docx'
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateName = templateMap[aiResponse.document_type] || 'pretenziya.docx';
|
||||||
|
|
||||||
|
// Формируем переменные
|
||||||
|
const variables = {
|
||||||
|
CLIENT_NAME: aiResponse.client_name,
|
||||||
|
DATE: new Date().toLocaleDateString('ru-RU'),
|
||||||
|
AMOUNT: aiResponse.amount,
|
||||||
|
CLAIM_TEXT: aiResponse.claim_text,
|
||||||
|
// ... другие переменные
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вызываем API создания документа
|
||||||
|
const createUrl = `https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php?` +
|
||||||
|
`module=Project&` +
|
||||||
|
`recordId=${projectId}&` +
|
||||||
|
`recordName=${projectName}&` +
|
||||||
|
`fileName=${fileName}&` +
|
||||||
|
`templateName=${templateName}&` +
|
||||||
|
`variables=${encodeURIComponent(JSON.stringify(variables))}`;
|
||||||
|
|
||||||
|
// Открываем документ
|
||||||
|
return { url: createUrl };
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Создание шаблонов
|
||||||
|
|
||||||
|
### Рекомендации по созданию шаблонов:
|
||||||
|
|
||||||
|
1. **Используйте стандартные названия:**
|
||||||
|
- `pretenziya.docx` - Претензия
|
||||||
|
- `iskovoe_zayavlenie.docx` - Исковое заявление
|
||||||
|
- `zhaloba.docx` - Жалоба
|
||||||
|
- `hodataystvo.docx` - Ходатайство
|
||||||
|
|
||||||
|
2. **Структура документа:**
|
||||||
|
- Шапка (кому, от кого, дата)
|
||||||
|
- Фабула (описание ситуации)
|
||||||
|
- Требования
|
||||||
|
- Ссылки на право
|
||||||
|
- Приложения
|
||||||
|
|
||||||
|
3. **Переменные:**
|
||||||
|
- Используйте понятные названия: `CLIENT_NAME`, `AMOUNT`, `DATE`
|
||||||
|
- Все переменные в верхнем регистре
|
||||||
|
- Обрамляйте фигурными скобками: `{VAR}` или `{{VAR}}`
|
||||||
|
|
||||||
|
## 🔍 Отладка
|
||||||
|
|
||||||
|
### Логи:
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/apache2/error.log | grep "CREATE FROM TEMPLATE"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка шаблона:
|
||||||
|
```bash
|
||||||
|
# Проверить наличие шаблона в Nextcloud
|
||||||
|
curl -u admin:office "https://office.clientright.ru:8443/remote.php/dav/files/admin/crm/Templates/pretenziya.docx" -k -I
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Ограничения
|
||||||
|
|
||||||
|
1. **PHPWord** работает только с DOCX файлами
|
||||||
|
2. Для XLSX и PPTX используется простая замена текста
|
||||||
|
3. Сложное форматирование (таблицы, изображения) может не сохраниться при простой замене
|
||||||
|
|
||||||
|
## 🎯 Альтернативные подходы
|
||||||
|
|
||||||
|
### Вариант A: Использование DOCX шаблонов с закладками
|
||||||
|
|
||||||
|
Вместо переменных `{VAR}` можно использовать закладки Word:
|
||||||
|
1. В Word: Вставка → Закладка
|
||||||
|
2. Создать закладку с именем переменной
|
||||||
|
3. PHPWord может заполнять закладки
|
||||||
|
|
||||||
|
### Вариант B: Использование только локальных шаблонов
|
||||||
|
|
||||||
|
Если не нужна синхронизация через Nextcloud:
|
||||||
|
1. Хранить шаблоны в `/crm_extensions/file_storage/templates/`
|
||||||
|
2. Использовать напрямую без WebDAV
|
||||||
|
|
||||||
|
### Вариант C: Генерация через PDFMaker
|
||||||
|
|
||||||
|
Если документ должен быть в PDF:
|
||||||
|
1. Создать DOCX из шаблона
|
||||||
|
2. Конвертировать через PDFMaker
|
||||||
|
3. Сохранить PDF в проект
|
||||||
|
|
||||||
|
## 📚 Полезные ссылки
|
||||||
|
|
||||||
|
- [PHPWord Documentation](https://phpword.readthedocs.io/)
|
||||||
|
- [Nextcloud WebDAV API](https://docs.nextcloud.com/server/latest/user_manual/files/webdav.html)
|
||||||
|
- [OnlyOffice Integration](https://api.onlyoffice.com/)
|
||||||
|
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# 🔍 Анализ проблемы с API шаблонов Nextcloud
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Проблема найдена и решена
|
||||||
|
|
||||||
|
## 🎯 Проблема
|
||||||
|
|
||||||
|
При попытке получить список шаблонов через API:
|
||||||
|
```bash
|
||||||
|
curl "https://office.clientright.ru:8443/ocs/v2.php/apps/files/api/v1/directEditing/templates"
|
||||||
|
```
|
||||||
|
|
||||||
|
Получаем ошибку:
|
||||||
|
```xml
|
||||||
|
<status>failure</status>
|
||||||
|
<statuscode>998</statuscode>
|
||||||
|
<message>Invalid query, please check the syntax.</message>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔬 Диагностика
|
||||||
|
|
||||||
|
### 1. Проверка основного API Direct Editing
|
||||||
|
|
||||||
|
**Запрос:**
|
||||||
|
```bash
|
||||||
|
curl -u admin:office "https://office.clientright.ru:8443/ocs/v2.php/apps/files/api/v1/directEditing" \
|
||||||
|
-H "OCS-APIRequest: true" -k
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:** ✅ API работает, возвращает список редакторов и создателей
|
||||||
|
|
||||||
|
**Структура ответа:**
|
||||||
|
```xml
|
||||||
|
<editors>
|
||||||
|
<onlyoffice>
|
||||||
|
<id>onlyoffice</id>
|
||||||
|
<name>ONLYOFFICE</name>
|
||||||
|
<mimetypes>...</mimetypes>
|
||||||
|
<optionalMimetypes>...</optionalMimetypes>
|
||||||
|
</onlyoffice>
|
||||||
|
</editors>
|
||||||
|
<creators>
|
||||||
|
<onlyoffice_docx>
|
||||||
|
<id>onlyoffice_docx</id>
|
||||||
|
<editor>onlyoffice</editor>
|
||||||
|
<name>Новый документ</name>
|
||||||
|
<extension>docx</extension>
|
||||||
|
<templates></templates> ← ПУСТОЙ!
|
||||||
|
<mimetype>application/vnd.openxmlformats-officedocument.wordprocessingml.document</mimetype>
|
||||||
|
</onlyoffice_docx>
|
||||||
|
</creators>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Выводы
|
||||||
|
|
||||||
|
**Проблема №1: Endpoint `/templates` не существует**
|
||||||
|
- Nextcloud Direct Editing API не имеет отдельного endpoint `/templates`
|
||||||
|
- Шаблоны должны возвращаться внутри основного ответа `/directEditing`
|
||||||
|
- Тег `<templates></templates>` присутствует, но **пустой**
|
||||||
|
|
||||||
|
**Проблема №2: Шаблоны не настроены в Nextcloud**
|
||||||
|
- Тег `<templates></templates>` пустой означает, что шаблоны не найдены
|
||||||
|
- Nextcloud ищет шаблоны в специальной папке, но она либо не существует, либо пустая
|
||||||
|
|
||||||
|
## 📚 Как работает система шаблонов в Nextcloud
|
||||||
|
|
||||||
|
### Где Nextcloud ищет шаблоны:
|
||||||
|
|
||||||
|
1. **Системные шаблоны OnlyOffice:**
|
||||||
|
- Хранятся в конфигурации OnlyOffice
|
||||||
|
- Недоступны через Nextcloud API напрямую
|
||||||
|
|
||||||
|
2. **Пользовательские шаблоны:**
|
||||||
|
- Должны быть в специальной папке пользователя
|
||||||
|
- Путь зависит от конфигурации Nextcloud
|
||||||
|
- Обычно: `/Templates/` в корне пользователя
|
||||||
|
|
||||||
|
3. **Глобальные шаблоны:**
|
||||||
|
- Могут быть настроены администратором
|
||||||
|
- Путь настраивается в `config.php`
|
||||||
|
|
||||||
|
## ✅ Решение
|
||||||
|
|
||||||
|
### Вариант 1: Использовать WebDAV для получения списка шаблонов (РЕКОМЕНДУЕТСЯ)
|
||||||
|
|
||||||
|
Вместо несуществующего API endpoint, используем WebDAV PROPFIND:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Получаем список файлов из папки Templates
|
||||||
|
$templatesUrl = 'https://office.clientright.ru:8443/remote.php/dav/files/admin/crm/Templates/';
|
||||||
|
|
||||||
|
$ch = curl_init($templatesUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Depth: 1']);
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, "admin:office");
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
// Парсим XML ответ и извлекаем имена файлов
|
||||||
|
```
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Работает всегда (WebDAV - стандартный протокол)
|
||||||
|
- ✅ Не зависит от версии Nextcloud
|
||||||
|
- ✅ Можно получить метаданные файлов (размер, дата изменения)
|
||||||
|
|
||||||
|
### Вариант 2: Настроить шаблоны в Nextcloud (если нужно)
|
||||||
|
|
||||||
|
Если хотите использовать встроенную систему шаблонов:
|
||||||
|
|
||||||
|
1. **Создать папку Templates:**
|
||||||
|
```
|
||||||
|
/admin/Templates/ (в корне пользователя admin)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Загрузить шаблоны:**
|
||||||
|
- Загрузить DOCX файлы через веб-интерфейс
|
||||||
|
- Или через WebDAV
|
||||||
|
|
||||||
|
3. **Проверить конфигурацию:**
|
||||||
|
```php
|
||||||
|
// config/config.php
|
||||||
|
'direct_editing' => [
|
||||||
|
'templates' => [
|
||||||
|
'path' => '/admin/Templates/',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
**Проблема:** Не все версии Nextcloud поддерживают это из коробки.
|
||||||
|
|
||||||
|
### Вариант 3: Использовать наш подход (ТЕКУЩИЙ)
|
||||||
|
|
||||||
|
Мы уже реализовали решение через WebDAV:
|
||||||
|
- Шаблоны хранятся в `/crm/Templates/`
|
||||||
|
- Получаем список через WebDAV PROPFIND
|
||||||
|
- Скачиваем шаблон через WebDAV GET
|
||||||
|
- Заполняем переменные через PHPWord
|
||||||
|
- Сохраняем готовый документ
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Работает независимо от версии Nextcloud
|
||||||
|
- ✅ Полный контроль над процессом
|
||||||
|
- ✅ Можно использовать сложную логику заполнения
|
||||||
|
|
||||||
|
## 🔧 Реализация получения списка шаблонов
|
||||||
|
|
||||||
|
Создадим endpoint для получения списка шаблонов:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// /crm_extensions/file_storage/api/list_templates.php
|
||||||
|
<?php
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
|
||||||
|
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
$username = 'admin';
|
||||||
|
$password = 'office';
|
||||||
|
$templatesPath = '/crm/Templates/';
|
||||||
|
|
||||||
|
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatesPath;
|
||||||
|
|
||||||
|
$ch = curl_init($webdavUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Depth: 1',
|
||||||
|
'Content-Type: application/xml'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:getcontenttype/><d:getcontentlength/></d:prop></d:propfind>');
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 207) { // 207 Multi-Status для PROPFIND
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to get templates']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим XML
|
||||||
|
$xml = simplexml_load_string($response);
|
||||||
|
$xml->registerXPathNamespace('d', 'DAV:');
|
||||||
|
|
||||||
|
$templates = [];
|
||||||
|
foreach ($xml->xpath('//d:response') as $response) {
|
||||||
|
$href = (string)$response->xpath('.//d:href')[0];
|
||||||
|
$displayName = (string)$response->xpath('.//d:displayname')[0];
|
||||||
|
$contentType = (string)$response->xpath('.//d:getcontenttype')[0];
|
||||||
|
|
||||||
|
// Пропускаем саму папку
|
||||||
|
if (rtrim($href, '/') === rtrim($webdavUrl, '/')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Только Office файлы
|
||||||
|
if (strpos($contentType, 'officedocument') !== false ||
|
||||||
|
strpos($contentType, 'msword') !== false ||
|
||||||
|
strpos($contentType, 'spreadsheet') !== false ||
|
||||||
|
strpos($contentType, 'presentation') !== false) {
|
||||||
|
|
||||||
|
$templates[] = [
|
||||||
|
'name' => $displayName,
|
||||||
|
'path' => $href,
|
||||||
|
'type' => $contentType
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'templates' => $templates]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Сравнение подходов
|
||||||
|
|
||||||
|
| Подход | Работает | Сложность | Гибкость |
|
||||||
|
|--------|----------|-----------|----------|
|
||||||
|
| API `/templates` | ❌ Не существует | - | - |
|
||||||
|
| WebDAV PROPFIND | ✅ Да | Средняя | Высокая |
|
||||||
|
| Настройка Nextcloud | ⚠️ Зависит от версии | Высокая | Низкая |
|
||||||
|
| Наш подход (WebDAV + PHPWord) | ✅ Да | Средняя | Очень высокая |
|
||||||
|
|
||||||
|
## 🎯 Рекомендация
|
||||||
|
|
||||||
|
**Использовать WebDAV PROPFIND** для получения списка шаблонов:
|
||||||
|
- ✅ Надежно работает
|
||||||
|
- ✅ Не зависит от версии Nextcloud
|
||||||
|
- ✅ Можно получить метаданные
|
||||||
|
- ✅ Стандартный протокол
|
||||||
|
|
||||||
|
**Не использовать** несуществующий endpoint `/templates`:
|
||||||
|
- ❌ Его нет в API Nextcloud
|
||||||
|
- ❌ Возвращает ошибку 998 (Invalid query)
|
||||||
|
|
||||||
|
## 📝 Выводы
|
||||||
|
|
||||||
|
1. **Проблема:** Endpoint `/ocs/v2.php/apps/files/api/v1/directEditing/templates` не существует в Nextcloud API
|
||||||
|
2. **Причина:** Nextcloud не предоставляет отдельный endpoint для получения шаблонов
|
||||||
|
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
|
||||||
|
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# 🔍 Анализ системы шаблонов ONLYOFFICE в Nextcloud
|
||||||
|
|
||||||
|
**Дата:** 2025-01-XX
|
||||||
|
**Статус:** ✅ Найдено решение
|
||||||
|
|
||||||
|
## 🎯 Проблема
|
||||||
|
|
||||||
|
В настройках ONLYOFFICE видно раздел "Общие шаблоны" с шаблоном:
|
||||||
|
- `Соглашение_№_71_06_об_оказании_юридической_помощи_от_01_10_2025_года.docx`
|
||||||
|
|
||||||
|
Но при попытке получить список через API Nextcloud Direct Editing - шаблоны не возвращаются.
|
||||||
|
|
||||||
|
## 🔬 Диагностика
|
||||||
|
|
||||||
|
### 1. Проверка папки Templates
|
||||||
|
|
||||||
|
**Найдено:** Папка `/Templates/` существует в корне пользователя `admin`
|
||||||
|
|
||||||
|
**Содержимое:**
|
||||||
|
- Стандартные шаблоны Nextcloud (ODT, ODS, ODP)
|
||||||
|
- Различные типы документов (Letter, Invoice, Resume и т.д.)
|
||||||
|
|
||||||
|
**WebDAV путь:**
|
||||||
|
```
|
||||||
|
https://office.clientright.ru:8443/remote.php/dav/files/admin/Templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Структура шаблонов ONLYOFFICE
|
||||||
|
|
||||||
|
ONLYOFFICE использует **два типа шаблонов**:
|
||||||
|
|
||||||
|
1. **Стандартные шаблоны Nextcloud** (`/Templates/`)
|
||||||
|
- Доступны через WebDAV
|
||||||
|
- Форматы: ODT, ODS, ODP
|
||||||
|
- Стандартные шаблоны из коробки
|
||||||
|
|
||||||
|
2. **Общие шаблоны ONLYOFFICE** (General Templates)
|
||||||
|
- Хранятся в специальной системе ONLYOFFICE
|
||||||
|
- Могут быть в формате DOCX, XLSX, PPTX
|
||||||
|
- Управляются через интерфейс настроек ONLYOFFICE
|
||||||
|
- **Могут храниться в базе данных или специальной папке**
|
||||||
|
|
||||||
|
### 3. Где хранятся "Общие шаблоны" ONLYOFFICE?
|
||||||
|
|
||||||
|
**Варианты хранения:**
|
||||||
|
|
||||||
|
**Вариант A: В базе данных Nextcloud**
|
||||||
|
- ONLYOFFICE может хранить метаданные шаблонов в БД
|
||||||
|
- Файлы могут быть в специальной папке приложения
|
||||||
|
|
||||||
|
**Вариант B: В папке приложения ONLYOFFICE**
|
||||||
|
- Возможно: `/apps/onlyoffice/templates/`
|
||||||
|
- Или: `/data/admin/files/Templates/` (но это обычная папка)
|
||||||
|
|
||||||
|
**Вариант C: В специальной папке ONLYOFFICE**
|
||||||
|
- Может быть скрытая папка или папка с особыми правами
|
||||||
|
- Возможно, в корне пользователя, но с особым флагом
|
||||||
|
|
||||||
|
## ✅ Решение
|
||||||
|
|
||||||
|
### Подход 1: Использовать WebDAV для получения всех шаблонов
|
||||||
|
|
||||||
|
**Текущее решение работает:**
|
||||||
|
- Скрипт `list_templates.php` получает список файлов из `/Templates/`
|
||||||
|
- Можно использовать для стандартных шаблонов
|
||||||
|
|
||||||
|
**Ограничение:**
|
||||||
|
- Не получает "Общие шаблоны" ONLYOFFICE, если они хранятся отдельно
|
||||||
|
|
||||||
|
### Подход 2: Добавить шаблоны в папку Templates
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
1. Скачать шаблон "Соглашение..." из настроек ONLYOFFICE
|
||||||
|
2. Загрузить его в папку `/Templates/` через WebDAV или веб-интерфейс
|
||||||
|
3. Теперь он будет доступен через наш API
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Единая точка доступа ко всем шаблонам
|
||||||
|
- ✅ Работает через WebDAV (стандартный протокол)
|
||||||
|
- ✅ Не зависит от внутренней структуры ONLYOFFICE
|
||||||
|
|
||||||
|
### Подход 3: Использовать API ONLYOFFICE напрямую (если доступен)
|
||||||
|
|
||||||
|
**Проверка:**
|
||||||
|
```bash
|
||||||
|
# Попытка получить шаблоны через ONLYOFFICE API
|
||||||
|
curl "https://office.clientright.ru:8443/index.php/apps/onlyoffice/ajax/templates"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Статус:** Не работает (возвращает 404)
|
||||||
|
|
||||||
|
## 📝 Рекомендации
|
||||||
|
|
||||||
|
### Для использования шаблонов:
|
||||||
|
|
||||||
|
1. **Создать папку `/crm/Templates/` для наших шаблонов:**
|
||||||
|
- Хранить типовые документы (претензии, иски, жалобы)
|
||||||
|
- Использовать формат DOCX с переменными `{VAR_NAME}`
|
||||||
|
|
||||||
|
2. **Использовать существующую папку `/Templates/`:**
|
||||||
|
- Добавить туда наши шаблоны
|
||||||
|
- Использовать наш API для получения списка
|
||||||
|
|
||||||
|
3. **Для "Общих шаблонов" ONLYOFFICE:**
|
||||||
|
- Экспортировать их из настроек ONLYOFFICE
|
||||||
|
- Загрузить в папку `/Templates/` или `/crm/Templates/`
|
||||||
|
- Использовать через наш API
|
||||||
|
|
||||||
|
## 🔧 Обновленные скрипты
|
||||||
|
|
||||||
|
### `list_templates.php`
|
||||||
|
- ✅ Исправлен путь на `/Templates/` (корень пользователя)
|
||||||
|
- ✅ Работает с WebDAV PROPFIND
|
||||||
|
- ✅ Возвращает список всех Office файлов
|
||||||
|
|
||||||
|
### `create_from_template.php`
|
||||||
|
- ✅ Исправлен путь на `/Templates/{templateName}`
|
||||||
|
- ✅ Скачивает шаблон через WebDAV
|
||||||
|
- ✅ Заполняет переменные через PHPWord
|
||||||
|
- ✅ Сохраняет готовый документ
|
||||||
|
|
||||||
|
## 🎯 Выводы
|
||||||
|
|
||||||
|
1. **Шаблоны ONLYOFFICE хранятся в папке `/Templates/`** в корне пользователя
|
||||||
|
2. **"Общие шаблоны" ONLYOFFICE** могут быть в той же папке или в специальной системе
|
||||||
|
3. **Наш подход через WebDAV работает** для всех шаблонов в папке `/Templates/`
|
||||||
|
4. **Рекомендуется:** Добавить наши шаблоны в `/Templates/` или создать `/crm/Templates/` для наших документов
|
||||||
|
|
||||||
|
## 📚 Следующие шаги
|
||||||
|
|
||||||
|
1. Проверить, есть ли шаблон "Соглашение..." в папке `/Templates/`
|
||||||
|
2. Если нет - экспортировать из настроек ONLYOFFICE и загрузить в папку
|
||||||
|
3. Протестировать получение списка через `list_templates.php`
|
||||||
|
4. Использовать шаблоны через `create_from_template.php`
|
||||||
|
|
||||||
92
crm_extensions/file_storage/fix_document_397340_path.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Исправление пути документа 397340 в проекте 396447
|
||||||
|
*
|
||||||
|
* Проблема: документ 397340 имеет путь с префиксом 'crm2/CRM_Active_Files/',
|
||||||
|
* а остальные документы проекта имеют путь БЕЗ этого префикса.
|
||||||
|
*
|
||||||
|
* Решение: убрать префикс 'crm2/CRM_Active_Files/' из s3_key для единообразия.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8mb4",
|
||||||
|
$dbconfig['db_username'],
|
||||||
|
$dbconfig['db_password'],
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
$notesId = 397340;
|
||||||
|
$dryRun = false; // Изменить на false для реального исправления
|
||||||
|
|
||||||
|
echo "=== ИСПРАВЛЕНИЕ ПУТИ ДОКУМЕНТА 397340 ===\n\n";
|
||||||
|
|
||||||
|
// Получаем текущие данные документа
|
||||||
|
$sql = "SELECT notesid, title, s3_key, s3_bucket, filename FROM vtiger_notes WHERE notesid = ?";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$notesId]);
|
||||||
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$doc) {
|
||||||
|
die("❌ Документ $notesId не найден!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "📄 Документ: {$doc['title']}\n";
|
||||||
|
echo " ID: {$doc['notesid']}\n";
|
||||||
|
echo " Текущий s3_key: {$doc['s3_key']}\n\n";
|
||||||
|
|
||||||
|
// Проверяем, есть ли префикс
|
||||||
|
if (strpos($doc['s3_key'], 'crm2/CRM_Active_Files/') === 0) {
|
||||||
|
// Убираем префикс
|
||||||
|
$newS3Key = str_replace('crm2/CRM_Active_Files/', '', $doc['s3_key']);
|
||||||
|
|
||||||
|
echo "✅ Найден префикс 'crm2/CRM_Active_Files/'\n";
|
||||||
|
echo " Новый s3_key: $newS3Key\n\n";
|
||||||
|
|
||||||
|
// Проверяем остальные документы проекта для сравнения
|
||||||
|
$sql2 = "SELECT notesid, s3_key FROM vtiger_notes n
|
||||||
|
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||||
|
WHERE snr.crmid = 396447 AND n.notesid != ? AND n.s3_key IS NOT NULL
|
||||||
|
LIMIT 3";
|
||||||
|
$stmt2 = $pdo->prepare($sql2);
|
||||||
|
$stmt2->execute([$notesId]);
|
||||||
|
$others = $stmt2->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo "📊 Сравнение с другими документами проекта:\n";
|
||||||
|
foreach ($others as $other) {
|
||||||
|
echo " ID {$other['notesid']}: {$other['s3_key']}\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Обновляем filename тоже (если там есть полный URL)
|
||||||
|
$newFilename = $doc['filename'];
|
||||||
|
if (strpos($doc['filename'], 'crm2/CRM_Active_Files/') !== false) {
|
||||||
|
$newFilename = str_replace('crm2/CRM_Active_Files/', '', $doc['filename']);
|
||||||
|
// Если это полный URL, пересобираем его
|
||||||
|
if (strpos($newFilename, 'https://') === false && $doc['s3_bucket']) {
|
||||||
|
$newFilename = "https://s3.twcstorage.ru/{$doc['s3_bucket']}/" . rawurlencode($newS3Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
echo "🔧 ПРИМЕНЯЕМ ИСПРАВЛЕНИЕ...\n\n";
|
||||||
|
|
||||||
|
$updateSql = "UPDATE vtiger_notes SET s3_key = ?, filename = ? WHERE notesid = ?";
|
||||||
|
$updateStmt = $pdo->prepare($updateSql);
|
||||||
|
$updateStmt->execute([$newS3Key, $newFilename, $notesId]);
|
||||||
|
|
||||||
|
echo "✅ Документ обновлён!\n";
|
||||||
|
echo " Новый s3_key: $newS3Key\n";
|
||||||
|
echo " Новый filename: " . substr($newFilename, 0, 100) . "...\n";
|
||||||
|
} else {
|
||||||
|
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n";
|
||||||
|
echo " Для применения изменений установите \$dryRun = false\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "ℹ️ Префикс 'crm2/CRM_Active_Files/' не найден в пути.\n";
|
||||||
|
echo " Документ уже в правильном формате.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== ГОТОВО ===\n";
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ const CONFIG = {
|
|||||||
},
|
},
|
||||||
// Индексируем только файлы из этих папок
|
// Индексируем только файлы из этих папок
|
||||||
pathPrefixes: [
|
pathPrefixes: [
|
||||||
'files/crm/crm2/',
|
'crm2/CRM_Active_Files/', // ИСПРАВЛЕНО: без 'files/' префикса!
|
||||||
'files/crm/erv_app/'
|
'erv_app/'
|
||||||
],
|
],
|
||||||
indexInterval: 60000 // Обновляем индекс каждую минуту
|
indexInterval: 60000 // Обновляем индекс каждую минуту
|
||||||
};
|
};
|
||||||
|
|||||||
38
crm_extensions/file_storage/verify_prefix_396447.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8mb4",
|
||||||
|
$dbconfig['db_username'],
|
||||||
|
$dbconfig['db_password'],
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "=== ПРОВЕРКА РЕЗУЛЬТАТА ===\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT notesid, s3_key FROM vtiger_notes n
|
||||||
|
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||||
|
WHERE snr.crmid = 396447 AND n.s3_key IS NOT NULL
|
||||||
|
ORDER BY notesid";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$allCorrect = true;
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$hasPrefix = strpos($doc['s3_key'], 'crm2/CRM_Active_Files') === 0;
|
||||||
|
$status = $hasPrefix ? '✅' : '❌';
|
||||||
|
$pathStart = substr($doc['s3_key'], 0, 60);
|
||||||
|
echo sprintf("%s ID %-8s: %s...\n", $status, $doc['notesid'], $pathStart);
|
||||||
|
if (!$hasPrefix) {
|
||||||
|
$allCorrect = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
if ($allCorrect) {
|
||||||
|
echo "✅ ВСЕ ДОКУМЕНТЫ ИМЕЮТ ПРЕФИКС 'crm2/CRM_Active_Files/'!\n";
|
||||||
|
echo " Всего документов: " . count($docs) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "⚠️ ЕСТЬ ДОКУМЕНТЫ БЕЗ ПРЕФИКСА\n";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -446,6 +446,95 @@ function editInNextcloud(recordId, fileName) {
|
|||||||
return openNextcloudEditor(recordId, fileName);
|
return openNextcloudEditor(recordId, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открытие файла через Nextcloud Files UI (с версионированием)
|
||||||
|
* Использует Redis индекс для быстрого получения FileID
|
||||||
|
*/
|
||||||
|
function openViaNextcloud(recordId, fileName) {
|
||||||
|
console.log('📚 Opening via Nextcloud Files UI:', recordId);
|
||||||
|
|
||||||
|
// Получаем FileID и redirect URL из API
|
||||||
|
fetch(`/crm_extensions/file_storage/api/nextcloud_open.php?recordId=${recordId}&v=${Date.now()}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
console.log('✅ FileID получен:', data.fileId);
|
||||||
|
console.log('🔗 Redirect URL:', data.redirectUrl);
|
||||||
|
|
||||||
|
// Открываем Nextcloud в новом окне
|
||||||
|
const win = window.open(data.redirectUrl, 'nextcloud_files_' + Date.now(), 'width=1400,height=900,scrollbars=yes,resizable=yes');
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
console.log('✅ Nextcloud opened successfully');
|
||||||
|
} else {
|
||||||
|
console.log('❌ Failed to open window - popup blocked');
|
||||||
|
alert('❌ Не удалось открыть Nextcloud. Проверьте блокировку всплывающих окон.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ API error:', data);
|
||||||
|
alert('❌ Ошибка получения FileID: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('❌ Fetch error:', error);
|
||||||
|
alert('❌ Ошибка подключения к API');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание нового файла в Nextcloud
|
||||||
|
* @param {string} module - Модуль CRM (Project, Contacts, Accounts и т.д.)
|
||||||
|
* @param {string} recordId - ID записи
|
||||||
|
* @param {string} recordName - Название записи (для имени папки)
|
||||||
|
* @param {string} fileType - Тип файла (docx, xlsx, pptx)
|
||||||
|
*/
|
||||||
|
function createFileInNextcloud(module, recordId, recordName, fileType) {
|
||||||
|
console.log('🆕 Creating file in Nextcloud:', { module, recordId, recordName, fileType });
|
||||||
|
|
||||||
|
// Формируем имя файла по умолчанию
|
||||||
|
const fileTypeNames = {
|
||||||
|
'docx': 'Документ',
|
||||||
|
'xlsx': 'Таблица',
|
||||||
|
'pptx': 'Презентация'
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultName = `${fileTypeNames[fileType]}_${new Date().toISOString().split('T')[0]}`;
|
||||||
|
|
||||||
|
// Запрашиваем имя файла у пользователя
|
||||||
|
const fileName = prompt(`Введите название файла (без расширения):`, defaultName);
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
console.log('❌ File creation cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем прогресс
|
||||||
|
if (typeof app !== 'undefined' && app.helper && app.helper.showProgress) {
|
||||||
|
app.helper.showProgress({
|
||||||
|
message: 'Создание файла в Nextcloud...'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем скрипт создания
|
||||||
|
const createUrl = `/crm_extensions/file_storage/api/create_nextcloud_file.php?module=${module}&recordId=${recordId}&recordName=${encodeURIComponent(recordName)}&fileName=${encodeURIComponent(fileName)}&fileType=${fileType}`;
|
||||||
|
|
||||||
|
console.log('🎯 Creating file:', createUrl);
|
||||||
|
|
||||||
|
// Открываем в новом окне (скрипт создаст файл и откроет редактор)
|
||||||
|
const win = window.open(createUrl, 'nextcloud_create_' + Date.now(), 'width=1400,height=900,scrollbars=yes,resizable=yes');
|
||||||
|
|
||||||
|
// Скрываем прогресс
|
||||||
|
setTimeout(function() {
|
||||||
|
if (typeof app !== 'undefined' && app.helper && app.helper.hideProgress) {
|
||||||
|
app.helper.hideProgress();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
alert('❌ Не удалось открыть Nextcloud. Проверьте блокировку всплывающих окон.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Автоматическое подключение при загрузке страницы
|
// Автоматическое подключение при загрузке страницы
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
console.log('Nextcloud Editor integration loaded');
|
console.log('Nextcloud Editor integration loaded');
|
||||||
|
|||||||
@@ -150,3 +150,7 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
|||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
/*********************************************************************************
|
/*********************************************************************************
|
||||||
* API-интерфейс для создания Проекта из Web-формы (упрощённый)
|
* API-интерфейс для создания Проекта из Web-формы (упрощённый)
|
||||||
* Обязательное поле: cf_1885 (номер полиса)
|
* Обязательные поля: cf_1885 (номер полиса), contact_id (контакт)
|
||||||
* Логика: если проект с таким полисом существует - возвращаем ID БЕЗ обновления
|
* Логика: если проект с таким полисом И привязкой к контакту существует - возвращаем ID БЕЗ обновления
|
||||||
* Автор: Фёдор, 2025-11-01
|
* (по одному полису может быть несколько застрахованных лиц!)
|
||||||
|
* Автор: Фёдор, 2025-11-02
|
||||||
********************************************************************************/
|
********************************************************************************/
|
||||||
|
|
||||||
include_once 'include/Webservices/Query.php';
|
include_once 'include/Webservices/Query.php';
|
||||||
@@ -17,7 +18,8 @@ vimport ('includes.runtime.LanguageHandler');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Создание проекта из web-формы
|
* Создание проекта из web-формы
|
||||||
* Если проект с таким номером полиса уже существует - просто возвращаем его ID
|
* Если проект с таким номером полиса И привязкой к контакту уже существует - просто возвращаем его ID
|
||||||
|
* (один полис может быть у нескольких застрахованных лиц!)
|
||||||
* @param string $policy_number - номер полиса (обязательное поле) - cf_1885
|
* @param string $policy_number - номер полиса (обязательное поле) - cf_1885
|
||||||
* @param string $contact_id - ID контакта для привязки (обязательное поле)
|
* @param string $contact_id - ID контакта для привязки (обязательное поле)
|
||||||
* @param string $period_start - дата начала страхования (опционально) - cf_1887
|
* @param string $period_start - дата начала страхования (опционально) - cf_1887
|
||||||
@@ -50,21 +52,27 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
|||||||
// Валидация: убираем пробелы из номера полиса
|
// Валидация: убираем пробелы из номера полиса
|
||||||
$policy_number = trim($policy_number);
|
$policy_number = trim($policy_number);
|
||||||
|
|
||||||
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.PHP_EOL;
|
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.' И contact_id='.$contact_id.PHP_EOL;
|
||||||
file_put_contents('logs/CreateWebProject.log', $logstring, FILE_APPEND);
|
file_put_contents('logs/CreateWebProject.log', $logstring, FILE_APPEND);
|
||||||
|
|
||||||
global $adb, $current_user;
|
global $adb, $current_user;
|
||||||
|
|
||||||
$isNew = false; // Флаг: создан ли проект сейчас
|
$isNew = false; // Флаг: создан ли проект сейчас
|
||||||
|
|
||||||
// Проверяем существование проекта по номеру полиса
|
// Проверяем существование проекта по номеру полиса И привязке к контакту
|
||||||
|
// (т.к. по одному полису может быть несколько застрахованных лиц)
|
||||||
$query = "SELECT p.projectid
|
$query = "SELECT p.projectid
|
||||||
FROM vtiger_project p
|
FROM vtiger_project p
|
||||||
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
|
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
|
||||||
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||||
WHERE e.deleted = 0 AND pcf.cf_1885 = ?
|
LEFT JOIN vtiger_crmentityrel rel ON
|
||||||
|
(rel.crmid = p.projectid AND rel.relcrmid = ?)
|
||||||
|
OR (rel.relcrmid = p.projectid AND rel.crmid = ?)
|
||||||
|
WHERE e.deleted = 0
|
||||||
|
AND pcf.cf_1885 = ?
|
||||||
|
AND rel.crmid IS NOT NULL
|
||||||
LIMIT 1";
|
LIMIT 1";
|
||||||
$result = $adb->pquery($query, array($policy_number));
|
$result = $adb->pquery($query, array($contact_id, $contact_id, $policy_number));
|
||||||
|
|
||||||
if ($adb->num_rows($result) > 0) {
|
if ($adb->num_rows($result) > 0) {
|
||||||
// Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
|
// Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
|
||||||
@@ -126,4 +134,5 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
|||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
|
|
||||||
return $result; // ← Массив, НЕ json_encode!
|
return $result; // ← Массив, НЕ json_encode!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -448,24 +448,37 @@ function editInNextcloud(recordId, fileName) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Открытие файла через Nextcloud Files UI (с версионированием)
|
* Открытие файла через Nextcloud Files UI (с версионированием)
|
||||||
|
* Использует Redis индекс для быстрого получения FileID
|
||||||
*/
|
*/
|
||||||
function openViaNextcloud(recordId, fileName) {
|
function openViaNextcloud(recordId, fileName) {
|
||||||
console.log('📚 Opening via Nextcloud Files UI:', recordId, fileName);
|
console.log('📚 Opening via Nextcloud Files UI:', recordId);
|
||||||
|
|
||||||
// Открываем через nextcloud_open.php (PROPFIND → Nextcloud UI)
|
// Получаем FileID и redirect URL из API
|
||||||
const redirectUrl = `/crm_extensions/file_storage/api/nextcloud_open.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}&v=${Date.now()}`;
|
fetch(`/crm_extensions/file_storage/api/nextcloud_open.php?recordId=${recordId}&v=${Date.now()}`)
|
||||||
|
.then(response => response.json())
|
||||||
console.log('🎯 Opening via Nextcloud:', redirectUrl);
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
// Открываем в новом окне
|
console.log('✅ FileID получен:', data.fileId);
|
||||||
const win = window.open(redirectUrl, 'nextcloud_files_' + Date.now(), 'width=1400,height=900,scrollbars=yes,resizable=yes');
|
console.log('🔗 Redirect URL:', data.redirectUrl);
|
||||||
|
|
||||||
if (win) {
|
// Открываем Nextcloud в новом окне
|
||||||
console.log('✅ Nextcloud opened successfully');
|
const win = window.open(data.redirectUrl, 'nextcloud_files_' + Date.now(), 'width=1400,height=900,scrollbars=yes,resizable=yes');
|
||||||
} else {
|
|
||||||
console.log('❌ Failed to open window - popup blocked');
|
if (win) {
|
||||||
alert('❌ Не удалось открыть Nextcloud. Проверьте блокировку всплывающих окон.');
|
console.log('✅ Nextcloud opened successfully');
|
||||||
}
|
} else {
|
||||||
|
console.log('❌ Failed to open window - popup blocked');
|
||||||
|
alert('❌ Не удалось открыть Nextcloud. Проверьте блокировку всплывающих окон.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ API error:', data);
|
||||||
|
alert('❌ Ошибка получения FileID: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('❌ Fetch error:', error);
|
||||||
|
alert('❌ Ошибка подключения к API');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<i class="fa fa-map-marker"></i>
|
<i class="fa fa-map-marker"></i>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<i class="fa fa-map-marker"></i>
|
<i class="fa fa-map-marker"></i>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
{assign var=PRIORITY value=$RECORD->get('ticketpriorities')}
|
{assign var=PRIORITY value=$RECORD->get('ticketpriorities')}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
<div class="row info-row">
|
<div class="row info-row">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
<div class="info-row row">
|
<div class="info-row row">
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
{assign var=RELATED_TO value=$RECORD->get('related_to')}
|
{assign var=RELATED_TO value=$RECORD->get('related_to')}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
{assign var=RELATED_TO value=$RECORD->get('linktoaccountscontacts')}
|
{assign var=RELATED_TO value=$RECORD->get('linktoaccountscontacts')}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
<div class="info-row row">
|
<div class="info-row row">
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
<div class="row info-row">
|
<div class="row info-row">
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{* Подключаем Nextcloud Editor JS *}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js"></script>
|
<script type="text/javascript" src="crm_extensions/nextcloud_editor/js/nextcloud-editor.js?v=20251102c"></script>
|
||||||
|
|
||||||
{*
|
{*
|
||||||
<div class="row info-row">
|
<div class="row info-row">
|
||||||
|
|||||||
BIN
storage/2025/November/week2/397270_Ходатайство_по_делу_.pdf
Normal file
BIN
storage/2025/November/week2/397333_акт_Евроинс_10-11-2025.pdf
Normal file
BIN
storage/2025/November/week2/397342_top_bg.png
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
storage/2025/November/week2/397343_middle_bg.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
storage/2025/November/week2/397344_bottom_bg.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
storage/2025/November/week2/397345_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
storage/2025/November/week2/397346_add_icon.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
storage/2025/November/week2/397347_change_icon.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
storage/2025/November/week2/397349_top_bg.png
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
storage/2025/November/week2/397350_middle_bg.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
storage/2025/November/week2/397351_bottom_bg.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
storage/2025/November/week2/397352_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
storage/2025/November/week2/397353_add_icon.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
storage/2025/November/week2/397354_change_icon.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
storage/2025/November/week2/397359_top_bg.png
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
storage/2025/November/week2/397360_middle_bg.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
storage/2025/November/week2/397361_bottom_bg.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
storage/2025/November/week2/397362_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
storage/2025/November/week2/397363_add_icon.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
storage/2025/November/week2/397430_Документ (7).pdf
Normal file
BIN
storage/2025/November/week2/397434_top_bg.png
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
storage/2025/November/week2/397435_middle_bg.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
storage/2025/November/week2/397436_bottom_bg.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
storage/2025/November/week2/397437_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
storage/2025/November/week2/397438_add_icon.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
storage/2025/November/week2/397439_change_icon.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
storage/2025/November/week2/397440_remove_icon.png
Normal file
|
After Width: | Height: | Size: 142 B |
BIN
storage/2025/November/week2/397481_image1.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
storage/2025/November/week2/397482_image2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
storage/2025/November/week2/397483_image2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
storage/2025/November/week2/397484_image1.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
storage/2025/November/week2/397508_top_bg.png
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
storage/2025/November/week2/397509_middle_bg.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
storage/2025/November/week2/397510_bottom_bg.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
storage/2025/November/week2/397511_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |