.
This commit is contained in:
@@ -0,0 +1,752 @@
|
|||||||
|
# API для запуска стратегий продвижения
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Данный документ описывает API для управления credentials социальных сетей и запуска маркетинговых стратегий с автоматической публикацией постов.
|
||||||
|
|
||||||
|
## Базовый URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://your-server:port/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
Все endpoints требуют JWT токен в заголовке `Authorization`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your-jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Управление Credentials социальных сетей
|
||||||
|
|
||||||
|
### 1. Сохранение/обновление credentials
|
||||||
|
|
||||||
|
Сохраняет или обновляет credentials для указанной платформы. Credentials автоматически шифруются перед сохранением.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/social-media/credentials`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"credentials": "your-facebook-access-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
- `platform` (string, required) - Название платформы (например: "facebook", "instagram")
|
||||||
|
- `credentials` (string, required) - Access token или другие credentials для платформы
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Credentials успешно сохранены для платформы facebook",
|
||||||
|
"data": {
|
||||||
|
"platform": "facebook",
|
||||||
|
"hasCredentials": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00",
|
||||||
|
"updatedAt": "2024-01-15T10:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 400 Bad Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Ошибка валидации",
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Platform is required",
|
||||||
|
"details": {
|
||||||
|
"platform": "Platform is required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 401 Unauthorized:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Не авторизован",
|
||||||
|
"error": {
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса (JavaScript):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const saveCredentials = async (platform, accessToken) => {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: platform,
|
||||||
|
credentials: accessToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
await saveCredentials('facebook', 'EAABwzLix...');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Получение информации о credentials
|
||||||
|
|
||||||
|
Проверяет наличие credentials для указанной платформы.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/social-media/credentials/{platform}`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
|
||||||
|
- `platform` (string) - Название платформы
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"platform": "facebook",
|
||||||
|
"hasCredentials": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const checkCredentials = async (platform) => {
|
||||||
|
const response = await fetch(`/api/social-media/credentials/${platform}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data.hasCredentials;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Получение списка всех credentials
|
||||||
|
|
||||||
|
Возвращает список всех платформ, для которых у пользователя настроены credentials.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/social-media/credentials`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"hasCredentials": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00",
|
||||||
|
"updatedAt": "2024-01-15T10:30:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const getAllCredentials = async () => {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Удаление credentials
|
||||||
|
|
||||||
|
Удаляет credentials для указанной платформы.
|
||||||
|
|
||||||
|
**Endpoint:** `DELETE /api/social-media/credentials/{platform}`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
|
||||||
|
- `platform` (string) - Название платформы
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Credentials для платформы facebook успешно удалены",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 404 Not Found:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Credentials не найдены",
|
||||||
|
"error": {
|
||||||
|
"code": "NOT_FOUND",
|
||||||
|
"message": "Credentials для платформы facebook не найдены"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const deleteCredentials = async (platform) => {
|
||||||
|
const response = await fetch(`/api/social-media/credentials/${platform}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск стратегии продвижения
|
||||||
|
|
||||||
|
### Запуск стратегии
|
||||||
|
|
||||||
|
Запускает выполнение маркетинговой стратегии. Система автоматически создает задачи публикации из календаря постов стратегии и добавляет их в очередь для выполнения.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/marketing/analysis/strategy/{strategyId}/start`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
|
||||||
|
- `strategyId` (string) - ID стратегии для запуска
|
||||||
|
|
||||||
|
**Request Body (опционально):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Стратегия успешно запущена",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "67890abcdef",
|
||||||
|
"tasksCreated": 12,
|
||||||
|
"platforms": ["facebook", "instagram"],
|
||||||
|
"message": "Стратегия успешно запущена. Создано задач: 12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 400 Bad Request (стратегия не завершена):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Стратегия не готова к запуску",
|
||||||
|
"error": {
|
||||||
|
"code": "INVALID_STATUS",
|
||||||
|
"message": "Стратегия еще не завершена. Статус: processing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 400 Bad Request (нет credentials):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Не удалось запустить стратегию",
|
||||||
|
"error": {
|
||||||
|
"code": "MISSING_CREDENTIALS",
|
||||||
|
"message": "Credentials not found for platform: facebook. Please configure credentials first."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 404 Not Found:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Стратегия не найдена",
|
||||||
|
"error": {
|
||||||
|
"code": "NOT_FOUND",
|
||||||
|
"message": "Стратегия с указанным ID не найдена"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 403 Forbidden:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Доступ запрещен",
|
||||||
|
"error": {
|
||||||
|
"code": "FORBIDDEN",
|
||||||
|
"message": "У вас нет доступа к этой стратегии"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const startStrategy = async (strategyId) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/marketing/analysis/strategy/${strategyId}/start`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log(`Создано задач: ${data.data.tasksCreated}`);
|
||||||
|
console.log(`Платформы: ${data.data.platforms.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.error('Ошибка:', data.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полный пример использования
|
||||||
|
|
||||||
|
### Шаг 1: Настройка credentials для Facebook
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Сохраняем Facebook Access Token
|
||||||
|
const facebookToken = 'EAABwzLix...'; // Получить из Facebook Developer Console
|
||||||
|
|
||||||
|
const result = await saveCredentials('facebook', facebookToken);
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Facebook credentials сохранены');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Получение стратегии
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Получаем список стратегий пользователя
|
||||||
|
const response = await fetch('/api/marketing/analysis/strategy/my', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const strategies = data.data;
|
||||||
|
|
||||||
|
// Выбираем завершенную стратегию
|
||||||
|
const completedStrategy = strategies.find((s) => s.status === 'completed');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Запуск стратегии
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (completedStrategy) {
|
||||||
|
// Проверяем наличие credentials для платформ стратегии
|
||||||
|
const platforms = completedStrategy.priorityPlatforms || [];
|
||||||
|
|
||||||
|
for (const platform of platforms) {
|
||||||
|
const hasCreds = await checkCredentials(platform);
|
||||||
|
if (!hasCreds) {
|
||||||
|
console.warn(`Необходимо настроить credentials для ${platform}`);
|
||||||
|
// Показать пользователю форму для ввода credentials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем стратегию
|
||||||
|
const startResult = await startStrategy(completedStrategy.strategyId);
|
||||||
|
|
||||||
|
if (startResult.success) {
|
||||||
|
console.log(
|
||||||
|
`Стратегия запущена! Создано ${startResult.data.tasksCreated} задач`
|
||||||
|
);
|
||||||
|
// Показать уведомление пользователю
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
### Типичные ошибки и их обработка
|
||||||
|
|
||||||
|
1. **UNAUTHORIZED (401)**
|
||||||
|
|
||||||
|
- Причина: Невалидный или отсутствующий JWT токен
|
||||||
|
- Решение: Обновить токен или перенаправить на страницу входа
|
||||||
|
|
||||||
|
2. **VALIDATION_ERROR (400)**
|
||||||
|
|
||||||
|
- Причина: Невалидные данные в запросе
|
||||||
|
- Решение: Проверить обязательные поля и их формат
|
||||||
|
|
||||||
|
3. **MISSING_CREDENTIALS (400)**
|
||||||
|
|
||||||
|
- Причина: Не настроены credentials для платформы
|
||||||
|
- Решение: Предложить пользователю настроить credentials
|
||||||
|
|
||||||
|
4. **INVALID_STATUS (400)**
|
||||||
|
|
||||||
|
- Причина: Стратегия еще не завершена
|
||||||
|
- Решение: Дождаться завершения генерации стратегии
|
||||||
|
|
||||||
|
5. **NOT_FOUND (404)**
|
||||||
|
|
||||||
|
- Причина: Стратегия или credentials не найдены
|
||||||
|
- Решение: Проверить правильность ID
|
||||||
|
|
||||||
|
6. **FORBIDDEN (403)**
|
||||||
|
- Причина: Пользователь не имеет доступа к ресурсу
|
||||||
|
- Решение: Проверить права доступа
|
||||||
|
|
||||||
|
### Пример обработки ошибок
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const handleApiError = (error) => {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'UNAUTHORIZED':
|
||||||
|
// Перенаправить на страницу входа
|
||||||
|
window.location.href = '/login';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MISSING_CREDENTIALS':
|
||||||
|
// Показать модальное окно для настройки credentials
|
||||||
|
showCredentialsModal(error.message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'INVALID_STATUS':
|
||||||
|
// Показать сообщение о том, что стратегия еще не готова
|
||||||
|
showNotification(
|
||||||
|
'Стратегия еще не завершена. Пожалуйста, подождите.',
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'VALIDATION_ERROR':
|
||||||
|
// Показать ошибки валидации
|
||||||
|
showValidationErrors(error.details);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
showNotification('Произошла ошибка. Попробуйте позже.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
try {
|
||||||
|
const result = await startStrategy(strategyId);
|
||||||
|
if (!result.success) {
|
||||||
|
handleApiError(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
showNotification('Ошибка сети. Проверьте подключение.', 'error');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Статусы задач публикации
|
||||||
|
|
||||||
|
После запуска стратегии создаются задачи со следующими статусами:
|
||||||
|
|
||||||
|
- `pending` - Задача ожидает выполнения (дата публикации еще не наступила)
|
||||||
|
- `processing` - Задача выполняется в данный момент
|
||||||
|
- `completed` - Задача успешно выполнена
|
||||||
|
- `failed` - Задача не выполнена из-за ошибки
|
||||||
|
|
||||||
|
**Примечание:** Задачи с датой публикации в прошлом выполняются сразу после создания.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Планировщик задач
|
||||||
|
|
||||||
|
Система автоматически проверяет очередь задач каждую минуту и выполняет задачи, у которых наступило время публикации.
|
||||||
|
|
||||||
|
- Планировщик включен по умолчанию
|
||||||
|
- Можно отключить через настройку `posting.scheduler.enabled=false`
|
||||||
|
- Задачи выполняются асинхронно
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые платформы
|
||||||
|
|
||||||
|
На данный момент поддерживается:
|
||||||
|
|
||||||
|
- **Facebook** - через Facebook Graph API v18.0
|
||||||
|
|
||||||
|
В будущем планируется поддержка:
|
||||||
|
|
||||||
|
- Instagram
|
||||||
|
- LinkedIn
|
||||||
|
- Telegram
|
||||||
|
- TikTok
|
||||||
|
- YouTube
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Получение Facebook Access Token
|
||||||
|
|
||||||
|
Для получения Facebook Access Token:
|
||||||
|
|
||||||
|
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||||
|
2. Создайте приложение
|
||||||
|
3. Добавьте продукт "Facebook Login"
|
||||||
|
4. Настройте OAuth и получите Access Token
|
||||||
|
5. Используйте полученный токен в API
|
||||||
|
|
||||||
|
**Важно:**
|
||||||
|
|
||||||
|
- Access Token имеет срок действия
|
||||||
|
- Для долгосрочного использования рекомендуется использовать Long-Lived Token
|
||||||
|
- Токен должен иметь разрешения `pages_manage_posts` для публикации
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры React компонентов
|
||||||
|
|
||||||
|
### Компонент для настройки credentials
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const CredentialsForm = ({ platform, onSave }) => {
|
||||||
|
const [token, setToken] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: platform,
|
||||||
|
credentials: token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
onSave();
|
||||||
|
setToken('');
|
||||||
|
} else {
|
||||||
|
setError(data.error.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Ошибка сети');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<label>Access Token для {platform}:</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => setToken(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <div className='error'>{error}</div>}
|
||||||
|
<button type='submit' disabled={loading}>
|
||||||
|
{loading ? 'Сохранение...' : 'Сохранить'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CredentialsForm;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Компонент для запуска стратегии
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const StartStrategyButton = ({ strategyId, onStart }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/marketing/analysis/strategy/${strategyId}/start`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
onStart(data.data);
|
||||||
|
} else {
|
||||||
|
setError(data.error.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Ошибка сети');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={handleStart} disabled={loading}>
|
||||||
|
{loading ? 'Запуск...' : 'Запустить стратегию'}
|
||||||
|
</button>
|
||||||
|
{error && <div className='error'>{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartStrategyButton;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часто задаваемые вопросы
|
||||||
|
|
||||||
|
### Q: Как часто проверяются задачи на выполнение?
|
||||||
|
|
||||||
|
A: Планировщик проверяет очередь каждую минуту.
|
||||||
|
|
||||||
|
### Q: Что происходит, если credentials истекли?
|
||||||
|
|
||||||
|
A: Задача получит статус `failed` с сообщением об ошибке. Необходимо обновить credentials и перезапустить стратегию.
|
||||||
|
|
||||||
|
### Q: Можно ли отменить выполнение стратегии?
|
||||||
|
|
||||||
|
A: На данный момент нет, но можно удалить credentials для платформы, что предотвратит выполнение будущих задач.
|
||||||
|
|
||||||
|
### Q: Как узнать статус выполнения задач?
|
||||||
|
|
||||||
|
A: Статусы задач можно получить через API (будет добавлено в будущих версиях).
|
||||||
|
|
||||||
|
### Q: Поддерживается ли публикация с изображениями?
|
||||||
|
|
||||||
|
A: На данный момент поддерживается только текстовая публикация. Поддержка изображений планируется в будущем.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Версионирование API
|
||||||
|
|
||||||
|
Текущая версия: **v1**
|
||||||
|
|
||||||
|
Все endpoints могут изменяться в будущих версиях. При изменении API будет указана новая версия.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем:
|
||||||
|
|
||||||
|
1. Проверьте логи в консоли браузера
|
||||||
|
2. Убедитесь, что JWT токен валиден
|
||||||
|
3. Проверьте формат запросов согласно документации
|
||||||
|
4. Обратитесь к разработчикам с описанием проблемы
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
|||||||
|
# Обновление API: Виды анализа для маркетингового анализа (Frontend/AI Agent)
|
||||||
|
|
||||||
|
## Обзор изменений
|
||||||
|
|
||||||
|
В API маркетингового анализа добавлена поддержка **опциональных видов анализа**, которые могут быть переданы от фронтенда и будут включены в промпт для AI-генерации отчета.
|
||||||
|
|
||||||
|
**Дата обновления**: 2025-01-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что изменилось
|
||||||
|
|
||||||
|
### Новые опциональные поля в запросе
|
||||||
|
|
||||||
|
В эндпоинт `POST /api/marketing/analysis/start` добавлены 5 новых опциональных полей для видов анализа:
|
||||||
|
|
||||||
|
1. **`market`** - Анализ рынка
|
||||||
|
2. **`competitors`** - Анализ конкурентов
|
||||||
|
3. **`targetAudienceAnalysis`** - Анализ целевой аудитории
|
||||||
|
4. **`channels`** - Анализ каналов
|
||||||
|
5. **`swot`** - SWOT анализ
|
||||||
|
|
||||||
|
Эти данные будут:
|
||||||
|
|
||||||
|
- ✅ Включены в контекст промпта для AI-генерации
|
||||||
|
- ✅ Сохранены в отчете
|
||||||
|
- ✅ Отображены в PDF-отчете в разделе "Виды анализа"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обновленная структура запроса
|
||||||
|
|
||||||
|
### Эндпоинт
|
||||||
|
|
||||||
|
**POST** `/api/marketing/analysis/start`
|
||||||
|
|
||||||
|
### Тело запроса (JSON)
|
||||||
|
|
||||||
|
#### Обязательные поля (без изменений)
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||||
|
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||||
|
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||||
|
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||||
|
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||||
|
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||||
|
|
||||||
|
#### Новые опциональные поля
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательный | Описание | Максимальная длина | Пример значения |
|
||||||
|
| ------------------------ | ------ | ------------ | ----------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------- |
|
||||||
|
| `market` | string | ❌ | Анализ рынка (размер, тренды, возможности) | 2000 символов | "Рынок веб-разработки в Казахстане растет на 15% ежегодно..." |
|
||||||
|
| `competitors` | string | ❌ | Анализ конкурентов (основные игроки, их преимущества) | 2000 символов | "Основные конкуренты: Компания X, Компания Y..." |
|
||||||
|
| `targetAudienceAnalysis` | string | ❌ | Детальный анализ целевой аудитории | 2000 символов | "Целевая аудитория: IT-директора средних компаний..." |
|
||||||
|
| `channels` | string | ❌ | Анализ маркетинговых каналов | 2000 символов | "Рекомендуемые каналы: LinkedIn для B2B, Instagram для визуального контента..." |
|
||||||
|
| `swot` | string | ❌ | SWOT анализ (Strengths, Weaknesses, Opportunities, Threats) | 2000 символов | "Strengths: Быстрая разработка... Weaknesses: Ограниченный бюджет..." |
|
||||||
|
|
||||||
|
### Валидация новых полей
|
||||||
|
|
||||||
|
Все новые поля имеют одинаковые правила валидации:
|
||||||
|
|
||||||
|
- **Тип**: `string`
|
||||||
|
- **Обязательность**: ❌ Опциональное (можно не передавать)
|
||||||
|
- **Максимальная длина**: 2000 символов
|
||||||
|
- **Минимальная длина**: Нет ограничений (может быть пустой строкой)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Пример 1: Запрос только с обязательными полями (как раньше)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
product: 'Разработка мобильных приложений',
|
||||||
|
location: 'Нур-Султан, Казахстан',
|
||||||
|
client: 'B2B клиенты',
|
||||||
|
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат**: Работает как раньше, без изменений в функциональности.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Пример 2: Запрос с одним видом анализа
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
product: 'Разработка мобильных приложений',
|
||||||
|
location: 'Нур-Султан, Казахстан',
|
||||||
|
client: 'B2B клиенты',
|
||||||
|
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||||
|
market:
|
||||||
|
'Рынок мобильной разработки в Казахстане показывает стабильный рост на 20% в год. Основные драйверы: цифровизация бизнеса, рост числа стартапов, увеличение инвестиций в IT-сектор.',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат**: Данные об анализе рынка будут включены в промпт и отображены в отчете.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Пример 3: Запрос со всеми видами анализа
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
product: 'Разработка мобильных приложений',
|
||||||
|
location: 'Нур-Султан, Казахстан',
|
||||||
|
client: 'B2B клиенты',
|
||||||
|
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||||
|
|
||||||
|
// Виды анализа
|
||||||
|
market:
|
||||||
|
'Рынок мобильной разработки в Казахстане растет на 20% ежегодно. Основные сегменты: корпоративные приложения, e-commerce, финтех.',
|
||||||
|
|
||||||
|
competitors:
|
||||||
|
'Основные конкуренты: TechCorp (лидер рынка, 30% доля), DevStudio (средний сегмент, 15% доля), StartupDev (нишевый игрок, 5% доля). Их преимущества: большая команда, долгосрочные контракты.',
|
||||||
|
|
||||||
|
targetAudienceAnalysis:
|
||||||
|
'Целевая аудитория: IT-директора средних и крупных компаний (50-500 сотрудников), возраст 35-50 лет, техническое образование. Боли: долгие сроки разработки, высокие цены, отсутствие гибкости.',
|
||||||
|
|
||||||
|
channels:
|
||||||
|
'Рекомендуемые каналы: LinkedIn (основной для B2B), Telegram-каналы для IT-сообщества, отраслевые конференции, контент-маркетинг через блог.',
|
||||||
|
|
||||||
|
swot: 'Strengths: Быстрая разработка MVP, опытная команда, гибкие методологии. Weaknesses: Ограниченный бюджет на маркетинг, небольшая команда. Opportunities: Рост спроса на мобильные решения, цифровизация госсектора. Threats: Конкуренция с крупными игроками, экономическая нестабильность.',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат**: Все виды анализа будут включены в промпт для более точной генерации отчета и отображены в PDF.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как это влияет на генерацию отчета
|
||||||
|
|
||||||
|
### Включение в промпт
|
||||||
|
|
||||||
|
Когда вы передаете виды анализа, они автоматически добавляются в контекст промпта для AI:
|
||||||
|
|
||||||
|
```
|
||||||
|
Продукт/услуга: Разработка мобильных приложений
|
||||||
|
Локация: Нур-Султан, Казахстан
|
||||||
|
Тип клиентов: B2B клиенты
|
||||||
|
Уникальные особенности: Специализируемся на быстрой разработке MVP за 4 недели
|
||||||
|
|
||||||
|
Анализ рынка:
|
||||||
|
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
|
||||||
|
|
||||||
|
Анализ конкурентов:
|
||||||
|
Основные конкуренты: TechCorp...
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Это позволяет AI генерировать более точные и релевантные рекомендации, учитывая предоставленную аналитику.
|
||||||
|
|
||||||
|
### Отображение в PDF-отчете
|
||||||
|
|
||||||
|
Если виды анализа были переданы, они будут отображены в PDF-отчете в отдельном разделе:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Виды анализа
|
||||||
|
|
||||||
|
### Анализ рынка
|
||||||
|
|
||||||
|
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
|
||||||
|
|
||||||
|
### Анализ конкурентов
|
||||||
|
|
||||||
|
Основные конкуренты: TechCorp...
|
||||||
|
|
||||||
|
### Анализ целевой аудитории
|
||||||
|
|
||||||
|
Целевая аудитория: IT-директора средних и крупных компаний...
|
||||||
|
|
||||||
|
### Анализ каналов
|
||||||
|
|
||||||
|
Рекомендуемые каналы: LinkedIn (основной для B2B)...
|
||||||
|
|
||||||
|
### SWOT анализ
|
||||||
|
|
||||||
|
Strengths: Быстрая разработка MVP...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обратная совместимость
|
||||||
|
|
||||||
|
✅ **Полная обратная совместимость**: Старые запросы без новых полей продолжают работать без изменений.
|
||||||
|
|
||||||
|
✅ **Опциональные поля**: Все новые поля опциональны, их можно не передавать.
|
||||||
|
|
||||||
|
✅ **Валидация**: Если поля переданы, они валидируются (максимум 2000 символов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендации для фронтенда
|
||||||
|
|
||||||
|
### 1. UI/UX
|
||||||
|
|
||||||
|
Рекомендуется добавить в форму создания анализа:
|
||||||
|
|
||||||
|
- **Опциональные секции** для каждого вида анализа
|
||||||
|
- **Текстовые поля** (textarea) с ограничением в 2000 символов
|
||||||
|
- **Подсказки** о том, какую информацию следует включить в каждый вид анализа
|
||||||
|
- **Индикатор прогресса** заполнения (опционально)
|
||||||
|
|
||||||
|
### 2. Валидация на фронтенде
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Пример валидации на фронтенде
|
||||||
|
const validateAnalysisTypes = (data) => {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
const analysisTypes = [
|
||||||
|
'market',
|
||||||
|
'competitors',
|
||||||
|
'targetAudienceAnalysis',
|
||||||
|
'channels',
|
||||||
|
'swot',
|
||||||
|
];
|
||||||
|
|
||||||
|
analysisTypes.forEach((field) => {
|
||||||
|
if (data[field] && data[field].length > 2000) {
|
||||||
|
errors[field] = `Поле "${field}" не должно превышать 2000 символов`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Структура данных
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript интерфейс для запроса
|
||||||
|
interface MarketingAnalysisRequest {
|
||||||
|
// Обязательные поля
|
||||||
|
product: string; // 3-200 символов
|
||||||
|
location: string; // 2-150 символов
|
||||||
|
client: string; // Одно из допустимых значений
|
||||||
|
differentiator: string; // 10-500 символов
|
||||||
|
|
||||||
|
// Опциональные виды анализа
|
||||||
|
market?: string; // до 2000 символов
|
||||||
|
competitors?: string; // до 2000 символов
|
||||||
|
targetAudienceAnalysis?: string; // до 2000 символов
|
||||||
|
channels?: string; // до 2000 символов
|
||||||
|
swot?: string; // до 2000 символов
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры ответов API
|
||||||
|
|
||||||
|
### Успешный ответ (без изменений)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Анализ запущен успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "processing",
|
||||||
|
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка валидации (если поле превышает лимит)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Ошибка валидации",
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Ошибка валидации входных данных",
|
||||||
|
"details": {
|
||||||
|
"market": "Поле 'market' должно содержать не более 2000 символов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP статус**: `400 Bad Request`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часто задаваемые вопросы (FAQ)
|
||||||
|
|
||||||
|
### Q: Обязательно ли передавать все виды анализа?
|
||||||
|
|
||||||
|
**A**: Нет, все поля опциональны. Вы можете передать только те, которые у вас есть.
|
||||||
|
|
||||||
|
### Q: Что произойдет, если я передам пустую строку?
|
||||||
|
|
||||||
|
**A**: Пустые строки игнорируются, они не будут включены в промпт и отчет.
|
||||||
|
|
||||||
|
### Q: Можно ли передать только часть видов анализа?
|
||||||
|
|
||||||
|
**A**: Да, вы можете передать любое количество видов анализа (от 0 до 5).
|
||||||
|
|
||||||
|
### Q: Влияет ли это на время генерации отчета?
|
||||||
|
|
||||||
|
**A**: Нет, время генерации остается прежним (5-10 минут). Дополнительные данные могут улучшить качество отчета.
|
||||||
|
|
||||||
|
### Q: Будут ли виды анализа отображаться в JSON-ответе?
|
||||||
|
|
||||||
|
**A**: Виды анализа сохраняются в отчете и могут быть доступны через структуру `reportData`, но в текущей версии API они не возвращаются в стандартном JSON-ответе. Они включены в PDF-отчет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Миграция для существующих клиентов
|
||||||
|
|
||||||
|
### Для существующих интеграций
|
||||||
|
|
||||||
|
**Никаких изменений не требуется!** Старые запросы продолжают работать без изменений.
|
||||||
|
|
||||||
|
### Для новых интеграций
|
||||||
|
|
||||||
|
Если вы хотите использовать новые возможности:
|
||||||
|
|
||||||
|
1. Добавьте новые поля в форму запроса (опционально)
|
||||||
|
2. Соблюдайте лимит в 2000 символов для каждого поля
|
||||||
|
3. Передавайте только те виды анализа, которые у вас есть
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
### Структура хранения
|
||||||
|
|
||||||
|
Виды анализа сохраняются в MongoDB в поле `reportData.analysisTypes`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reportData": {
|
||||||
|
"summary": "...",
|
||||||
|
"targetAudience": {...},
|
||||||
|
"recommendations": [...],
|
||||||
|
"strategy": {...},
|
||||||
|
"analysisTypes": {
|
||||||
|
"market": "Рынок...",
|
||||||
|
"competitors": "Конкуренты...",
|
||||||
|
"targetAudienceAnalysis": "ЦА...",
|
||||||
|
"channels": "Каналы...",
|
||||||
|
"swot": "SWOT..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Включение в промпт
|
||||||
|
|
||||||
|
Виды анализа добавляются в контекст промпта в следующем формате:
|
||||||
|
|
||||||
|
```
|
||||||
|
Анализ рынка:
|
||||||
|
[содержимое поля market]
|
||||||
|
|
||||||
|
Анализ конкурентов:
|
||||||
|
[содержимое поля competitors]
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Это позволяет AI учитывать предоставленную аналитику при генерации резюме, рекомендаций и стратегии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
Если у вас возникли вопросы или проблемы с использованием новых полей, обратитесь к документации API или свяжитесь с командой разработки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия документа**: 1.0
|
||||||
|
**Дата обновления**: 2025-01-20
|
||||||
|
**Статус**: ✅ Актуально
|
||||||
+6
-1
@@ -62,7 +62,12 @@ export const API_CONFIG = {
|
|||||||
MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy',
|
MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy',
|
||||||
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis',
|
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis',
|
||||||
MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my',
|
MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my',
|
||||||
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy'
|
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy',
|
||||||
|
MARKETING_STRATEGY_START: '/api/marketing/analysis/strategy',
|
||||||
|
|
||||||
|
// SocialMediaCredentialsController - Управление credentials социальных сетей
|
||||||
|
SOCIAL_MEDIA_CREDENTIALS: '/api/social-media/credentials',
|
||||||
|
SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM: '/api/social-media/credentials'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -248,11 +248,16 @@ const router = createRouter({
|
|||||||
name: 'marketing-analyses-list',
|
name: 'marketing-analyses-list',
|
||||||
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/marketing-analysis/strategies',
|
path: '/marketing-analysis/strategies',
|
||||||
name: 'marketing-strategies-list',
|
name: 'marketing-strategies-list',
|
||||||
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/marketing-analysis/credentials',
|
||||||
|
name: 'marketing-credentials',
|
||||||
|
component: () => import('@/views/pages/marketing/MarketingCredentials.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/pages/notfound',
|
path: '/pages/notfound',
|
||||||
name: 'notfound',
|
name: 'notfound',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class MarketingService {
|
|||||||
* @param {string} data.location - Географическая локация работы
|
* @param {string} data.location - Географическая локация работы
|
||||||
* @param {string} data.client - Тип целевой аудитории
|
* @param {string} data.client - Тип целевой аудитории
|
||||||
* @param {string} data.differentiator - Уникальные особенности бизнеса
|
* @param {string} data.differentiator - Уникальные особенности бизнеса
|
||||||
|
* @param {string} data.analysisType - Тип анализа (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
|
||||||
* @returns {Promise<Object>} Объект с analysisId и статусом
|
* @returns {Promise<Object>} Объект с analysisId и статусом
|
||||||
*/
|
*/
|
||||||
async startAnalysis(data) {
|
async startAnalysis(data) {
|
||||||
@@ -23,7 +24,8 @@ class MarketingService {
|
|||||||
product: data.product,
|
product: data.product,
|
||||||
location: data.location,
|
location: data.location,
|
||||||
client: data.client,
|
client: data.client,
|
||||||
differentiator: data.differentiator
|
differentiator: data.differentiator,
|
||||||
|
analysisType: data.analysisType
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +320,184 @@ class MarketingService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск стратегии продвижения
|
||||||
|
* POST /api/marketing/analysis/strategy/{strategyId}/start
|
||||||
|
* @param {string} strategyId - Идентификатор стратегии для запуска
|
||||||
|
* @returns {Promise<Object>} Объект с информацией о запуске (tasksCreated, platforms)
|
||||||
|
*/
|
||||||
|
async startStrategy(strategyId) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_START}/${strategyId}/start`, {
|
||||||
|
method: 'POST',
|
||||||
|
...DEFAULT_REQUEST_CONFIG,
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || result.error?.message || 'Ошибка при запуске стратегии',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || 'Ошибка при запуске стратегии',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при запуске стратегии:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранение/обновление credentials для социальной сети
|
||||||
|
* POST /api/social-media/credentials
|
||||||
|
* @param {string} platform - Название платформы (facebook, instagram, etc.)
|
||||||
|
* @param {string} credentials - Access token или другие credentials
|
||||||
|
* @returns {Promise<Object>} Объект с информацией о сохраненных credentials
|
||||||
|
*/
|
||||||
|
async saveCredentials(platform, credentials) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`, {
|
||||||
|
method: 'POST',
|
||||||
|
...DEFAULT_REQUEST_CONFIG,
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: platform,
|
||||||
|
credentials: credentials
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || result.error?.message || 'Ошибка при сохранении credentials',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || 'Ошибка при сохранении credentials',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении credentials:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение информации о credentials для платформы
|
||||||
|
* GET /api/social-media/credentials/{platform}
|
||||||
|
* @param {string} platform - Название платформы
|
||||||
|
* @returns {Promise<Object>} Объект с информацией о наличии credentials
|
||||||
|
*/
|
||||||
|
async getCredentials(platform) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении credentials:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка всех credentials
|
||||||
|
* GET /api/social-media/credentials
|
||||||
|
* @returns {Promise<Array>} Массив объектов с информацией о credentials для каждой платформы
|
||||||
|
*/
|
||||||
|
async getAllCredentials() {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении списка credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении списка credentials:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаление credentials для платформы
|
||||||
|
* DELETE /api/social-media/credentials/{platform}
|
||||||
|
* @param {string} platform - Название платформы
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async deleteCredentials(platform) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || result.error?.message || 'Ошибка при удалении credentials',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const error = {
|
||||||
|
message: result.message || 'Ошибка при удалении credentials',
|
||||||
|
code: result.error?.code,
|
||||||
|
details: result.error?.details
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при удалении credentials:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MarketingService();
|
export default new MarketingService();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:sortField="'createdAt'"
|
:sortField="'createdAt'"
|
||||||
:sortOrder="-1"
|
:sortOrder="-1"
|
||||||
filterDisplay="row"
|
filterDisplay="row"
|
||||||
:globalFilterFields="['product', 'location', 'clientType', 'status']"
|
:globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
dataKey="analysisId"
|
dataKey="analysisId"
|
||||||
@@ -54,6 +54,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
<Column field="analysisType" header="Тип анализа" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag v-if="slotProps.data.analysisType" :value="getAnalysisTypeLabel(slotProps.data.analysisType)" severity="secondary" />
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||||
@@ -309,6 +316,18 @@ const getStatusIcon = (status) => {
|
|||||||
return icons[status] || 'pi pi-circle';
|
return icons[status] || 'pi pi-circle';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get analysis type label
|
||||||
|
const getAnalysisTypeLabel = (type) => {
|
||||||
|
const labels = {
|
||||||
|
РЫНОК: 'Анализ рынка',
|
||||||
|
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||||
|
ЦА: 'Анализ целевой аудитории',
|
||||||
|
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||||
|
SWOT: 'SWOT-анализ'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
// Load on mount
|
// Load on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAnalyses();
|
loadAnalyses();
|
||||||
|
|||||||
@@ -33,6 +33,22 @@
|
|||||||
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
|
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
|
||||||
|
<Dropdown
|
||||||
|
id="analysisType"
|
||||||
|
v-model="formData.analysisType"
|
||||||
|
:options="analysisTypeOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Выберите тип анализа"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.analysisType }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.analysisType" class="p-error">{{ errors.analysisType }}</small>
|
||||||
|
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите тип анализа, который будет сгенерирован</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" />
|
<Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" />
|
||||||
|
|
||||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p>
|
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p>
|
||||||
@@ -77,7 +93,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header mb-4">
|
<div class="card-header mb-4">
|
||||||
<div class="flex justify-content-between align-items-center">
|
<div class="flex justify-content-between align-items-center">
|
||||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
||||||
|
<div v-if="currentAnalysisType" class="mt-2">
|
||||||
|
<Tag :value="getAnalysisTypeLabel(currentAnalysisType)" severity="info" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
|
<Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,22 +200,32 @@ import ProgressSpinner from 'primevue/progressspinner';
|
|||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onBeforeUnmount, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
product: '',
|
product: '',
|
||||||
location: '',
|
location: '',
|
||||||
client: null,
|
client: null,
|
||||||
differentiator: ''
|
differentiator: '',
|
||||||
|
analysisType: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
||||||
|
|
||||||
|
const analysisTypeOptions = [
|
||||||
|
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
|
||||||
|
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
|
||||||
|
{ label: 'ЦА - Анализ целевой аудитории', value: 'ЦА' },
|
||||||
|
{ label: 'КАНАЛЫ - Анализ маркетинговых каналов', value: 'КАНАЛЫ' },
|
||||||
|
{ label: 'SWOT - SWOT-анализ', value: 'SWOT' }
|
||||||
|
];
|
||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const errors = ref({});
|
const errors = ref({});
|
||||||
|
|
||||||
@@ -202,6 +233,7 @@ const errors = ref({});
|
|||||||
const analysisId = ref(null);
|
const analysisId = ref(null);
|
||||||
const status = ref(null);
|
const status = ref(null);
|
||||||
const report = ref(null);
|
const report = ref(null);
|
||||||
|
const currentAnalysisType = ref(null);
|
||||||
const estimatedCompletionTime = ref(null);
|
const estimatedCompletionTime = ref(null);
|
||||||
const pollingInterval = ref(null);
|
const pollingInterval = ref(null);
|
||||||
const pollingAttempts = ref(0);
|
const pollingAttempts = ref(0);
|
||||||
@@ -241,6 +273,16 @@ const validateForm = () => {
|
|||||||
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
|
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnalysisType validation
|
||||||
|
if (!formData.value.analysisType) {
|
||||||
|
errors.value.analysisType = 'Пожалуйста, выберите тип анализа';
|
||||||
|
} else {
|
||||||
|
const validTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
|
||||||
|
if (!validTypes.includes(formData.value.analysisType)) {
|
||||||
|
errors.value.analysisType = 'Выбран недопустимый тип анализа';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(errors.value).length === 0;
|
return Object.keys(errors.value).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,6 +310,10 @@ const startPolling = (id) => {
|
|||||||
if (result.status === 'completed' && result.report) {
|
if (result.status === 'completed' && result.report) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
report.value = result.report;
|
report.value = result.report;
|
||||||
|
// Сохраняем тип анализа из результата, если он есть
|
||||||
|
if (result.analysisType) {
|
||||||
|
currentAnalysisType.value = result.analysisType;
|
||||||
|
}
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Анализ завершен',
|
summary: 'Анализ завершен',
|
||||||
@@ -317,11 +363,13 @@ const handleSubmit = async () => {
|
|||||||
product: formData.value.product.trim(),
|
product: formData.value.product.trim(),
|
||||||
location: formData.value.location.trim(),
|
location: formData.value.location.trim(),
|
||||||
client: formData.value.client,
|
client: formData.value.client,
|
||||||
differentiator: formData.value.differentiator.trim()
|
differentiator: formData.value.differentiator.trim(),
|
||||||
|
analysisType: formData.value.analysisType
|
||||||
});
|
});
|
||||||
|
|
||||||
analysisId.value = result.analysisId;
|
analysisId.value = result.analysisId;
|
||||||
status.value = result.status;
|
status.value = result.status;
|
||||||
|
currentAnalysisType.value = formData.value.analysisType;
|
||||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -379,13 +427,15 @@ const resetAnalysis = () => {
|
|||||||
analysisId.value = null;
|
analysisId.value = null;
|
||||||
status.value = null;
|
status.value = null;
|
||||||
report.value = null;
|
report.value = null;
|
||||||
|
currentAnalysisType.value = null;
|
||||||
estimatedCompletionTime.value = null;
|
estimatedCompletionTime.value = null;
|
||||||
pollingAttempts.value = 0;
|
pollingAttempts.value = 0;
|
||||||
formData.value = {
|
formData.value = {
|
||||||
product: '',
|
product: '',
|
||||||
location: '',
|
location: '',
|
||||||
client: null,
|
client: null,
|
||||||
differentiator: ''
|
differentiator: '',
|
||||||
|
analysisType: null
|
||||||
};
|
};
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
};
|
};
|
||||||
@@ -407,6 +457,18 @@ const renderMarkdown = (markdown) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get analysis type label
|
||||||
|
const getAnalysisTypeLabel = (type) => {
|
||||||
|
const labels = {
|
||||||
|
РЫНОК: 'Анализ рынка',
|
||||||
|
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||||
|
ЦА: 'Анализ целевой аудитории',
|
||||||
|
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||||
|
SWOT: 'SWOT-анализ'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
// Format date time
|
// Format date time
|
||||||
const formatDateTime = (dateString) => {
|
const formatDateTime = (dateString) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
@@ -438,6 +500,39 @@ const goToStrategy = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load analysis from query params
|
||||||
|
onMounted(async () => {
|
||||||
|
const queryAnalysisId = route.query.analysisId;
|
||||||
|
if (queryAnalysisId) {
|
||||||
|
// Try to load existing analysis
|
||||||
|
try {
|
||||||
|
analysisId.value = queryAnalysisId;
|
||||||
|
const result = await MarketingService.getAnalysisResult(queryAnalysisId);
|
||||||
|
status.value = result.status;
|
||||||
|
|
||||||
|
// Сохраняем тип анализа, если он есть
|
||||||
|
if (result.analysisType) {
|
||||||
|
currentAnalysisType.value = result.analysisType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'completed' && result.report) {
|
||||||
|
report.value = result.report;
|
||||||
|
} else if (result.status === 'processing' || result.status === 'queued') {
|
||||||
|
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||||
|
startPolling(queryAnalysisId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке анализа:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить анализ',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<div class="marketing-credentials-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||||
|
<Toast />
|
||||||
|
<div class="max-w-[1200px] mx-auto">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header mb-4">
|
||||||
|
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Управление credentials социальных сетей</h2>
|
||||||
|
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">Настройте access tokens для платформ, на которых вы хотите автоматически публиковать контент</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<form @submit.prevent="handleSaveCredentials" class="space-y-4">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 md:col-4">
|
||||||
|
<div class="field">
|
||||||
|
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||||
|
Платформа <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
id="platform"
|
||||||
|
v-model="formData.platform"
|
||||||
|
:options="platformOptions"
|
||||||
|
placeholder="Выберите платформу"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.platform }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-6">
|
||||||
|
<div class="field">
|
||||||
|
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||||
|
Access Token <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
id="credentials"
|
||||||
|
v-model="formData.credentials"
|
||||||
|
type="password"
|
||||||
|
placeholder="Введите access token"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.credentials }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
|
||||||
|
<small class="text-surface-500 dark:text-surface-400 mt-1 block">
|
||||||
|
Токен будет зашифрован перед сохранением
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-2">
|
||||||
|
<div class="field">
|
||||||
|
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> </label>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
label="Сохранить"
|
||||||
|
icon="pi pi-save"
|
||||||
|
class="w-full"
|
||||||
|
:loading="saving"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header mb-4">
|
||||||
|
<div class="flex justify-content-between align-items-center">
|
||||||
|
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Настроенные платформы</h3>
|
||||||
|
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadCredentials" :loading="loading" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<DataTable
|
||||||
|
:value="credentials"
|
||||||
|
:loading="loading"
|
||||||
|
paginator
|
||||||
|
:rows="10"
|
||||||
|
:rowsPerPageOptions="[5, 10, 20]"
|
||||||
|
dataKey="platform"
|
||||||
|
:emptyMessage="'Нет настроенных платформ'"
|
||||||
|
>
|
||||||
|
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag :value="slotProps.data.platform" severity="info" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag
|
||||||
|
:value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'"
|
||||||
|
:severity="slotProps.data.hasCredentials ? 'success' : 'warning'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="createdAt" header="Создано" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.createdAt">{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="updatedAt" header="Обновлено" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.updatedAt">{{ formatDateTime(slotProps.data.updatedAt) }}</span>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Действия" style="min-width: 150px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
v-tooltip.top="'Удалить credentials'"
|
||||||
|
@click="confirmDelete(slotProps.data.platform)"
|
||||||
|
:disabled="!slotProps.data.hasCredentials"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Диалог подтверждения удаления -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="deleteDialogVisible"
|
||||||
|
:header="'Подтверждение удаления'"
|
||||||
|
:style="{ width: '450px' }"
|
||||||
|
:modal="true"
|
||||||
|
>
|
||||||
|
<div class="confirmation-content">
|
||||||
|
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
||||||
|
<span>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b>?</span>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
|
||||||
|
<Button label="Удалить" icon="pi pi-check" @click="handleDeleteCredentials" severity="danger" :loading="deleting" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarketingService from '@/service/MarketingService';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Column from 'primevue/column';
|
||||||
|
import DataTable from 'primevue/datatable';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
const formData = ref({
|
||||||
|
platform: null,
|
||||||
|
credentials: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
|
||||||
|
|
||||||
|
const saving = ref(false);
|
||||||
|
const errors = ref({});
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const credentials = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const deleteDialogVisible = ref(false);
|
||||||
|
const platformToDelete = ref(null);
|
||||||
|
const deleting = ref(false);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const validateForm = () => {
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
if (!formData.value.platform) {
|
||||||
|
errors.value.platform = 'Пожалуйста, выберите платформу';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
|
||||||
|
errors.value.credentials = 'Access token обязателен';
|
||||||
|
} else if (formData.value.credentials.trim().length < 10) {
|
||||||
|
errors.value.credentials = 'Access token слишком короткий';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(errors.value).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load credentials
|
||||||
|
const loadCredentials = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await MarketingService.getAllCredentials();
|
||||||
|
credentials.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке credentials:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить список credentials',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save credentials
|
||||||
|
const handleSaveCredentials = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка валидации',
|
||||||
|
detail: 'Пожалуйста, исправьте ошибки в форме',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MarketingService.saveCredentials(formData.value.platform, formData.value.credentials.trim());
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Успешно',
|
||||||
|
detail: `Credentials для ${formData.value.platform} успешно сохранены`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
formData.value = {
|
||||||
|
platform: null,
|
||||||
|
credentials: ''
|
||||||
|
};
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
// Reload credentials list
|
||||||
|
await loadCredentials();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении credentials:', error);
|
||||||
|
|
||||||
|
// Handle specific error codes
|
||||||
|
if (error.code === 'VALIDATION_ERROR') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка валидации',
|
||||||
|
detail: error.message || 'Проверьте правильность введенных данных',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'UNAUTHORIZED') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка авторизации',
|
||||||
|
detail: 'Необходимо войти в систему',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось сохранить credentials',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirm delete
|
||||||
|
const confirmDelete = (platform) => {
|
||||||
|
platformToDelete.value = platform;
|
||||||
|
deleteDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle delete credentials
|
||||||
|
const handleDeleteCredentials = async () => {
|
||||||
|
if (!platformToDelete.value) return;
|
||||||
|
|
||||||
|
deleting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MarketingService.deleteCredentials(platformToDelete.value);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Успешно',
|
||||||
|
detail: `Credentials для ${platformToDelete.value} успешно удалены`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteDialogVisible.value = false;
|
||||||
|
platformToDelete.value = null;
|
||||||
|
|
||||||
|
// Reload credentials list
|
||||||
|
await loadCredentials();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при удалении credentials:', error);
|
||||||
|
|
||||||
|
if (error.code === 'NOT_FOUND') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Не найдено',
|
||||||
|
detail: `Credentials для ${platformToDelete.value} не найдены`,
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'UNAUTHORIZED') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка авторизации',
|
||||||
|
detail: 'Необходимо войти в систему',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось удалить credentials',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deleting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format date time
|
||||||
|
const formatDateTime = (dateString) => {
|
||||||
|
if (!dateString) return '—';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load on mount
|
||||||
|
onMounted(() => {
|
||||||
|
loadCredentials();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.marketing-credentials-page {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -72,6 +72,13 @@ const menuItems = [
|
|||||||
iconColor: 'text-primary-500',
|
iconColor: 'text-primary-500',
|
||||||
route: '/marketing-analysis/strategies'
|
route: '/marketing-analysis/strategies'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'credentials',
|
||||||
|
label: 'Credentials социальных сетей',
|
||||||
|
icon: 'pi pi-key',
|
||||||
|
iconColor: 'text-primary-500',
|
||||||
|
route: '/marketing-analysis/credentials'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'results',
|
id: 'results',
|
||||||
label: 'Результаты',
|
label: 'Результаты',
|
||||||
|
|||||||
@@ -77,7 +77,17 @@
|
|||||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2>
|
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2>
|
||||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p>
|
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
label="Запустить стратегию"
|
||||||
|
icon="pi pi-play"
|
||||||
|
severity="success"
|
||||||
|
@click="handleStartStrategy"
|
||||||
|
:loading="startingStrategy"
|
||||||
|
:disabled="startingStrategy"
|
||||||
|
/>
|
||||||
|
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,12 +225,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Диалог для настройки credentials -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="credentialsDialogVisible"
|
||||||
|
:header="'Настройка credentials'"
|
||||||
|
:style="{ width: '600px' }"
|
||||||
|
:modal="true"
|
||||||
|
>
|
||||||
|
<div class="credentials-dialog-content">
|
||||||
|
<p class="text-surface-700 dark:text-surface-300 mb-4">
|
||||||
|
Для запуска стратегии необходимо настроить credentials для следующих платформ:
|
||||||
|
</p>
|
||||||
|
<div class="mb-4">
|
||||||
|
<Tag
|
||||||
|
v-for="platform in missingPlatforms"
|
||||||
|
:key="platform"
|
||||||
|
:value="platform"
|
||||||
|
severity="warning"
|
||||||
|
class="mr-2 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-surface-600 dark:text-surface-400 text-sm">
|
||||||
|
Перейдите на страницу управления credentials для настройки необходимых платформ.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Отмена" icon="pi pi-times" @click="credentialsDialogVisible = false" severity="secondary" />
|
||||||
|
<Button
|
||||||
|
label="Настроить credentials"
|
||||||
|
icon="pi pi-cog"
|
||||||
|
@click="goToCredentials"
|
||||||
|
severity="info"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import MarketingService from '@/service/MarketingService';
|
import MarketingService from '@/service/MarketingService';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
import InputNumber from 'primevue/inputnumber';
|
import InputNumber from 'primevue/inputnumber';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import MultiSelect from 'primevue/multiselect';
|
import MultiSelect from 'primevue/multiselect';
|
||||||
@@ -229,10 +275,11 @@ import Tag from 'primevue/tag';
|
|||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@@ -255,6 +302,9 @@ const pollingAttempts = ref(0);
|
|||||||
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
|
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
|
||||||
const pollingIntervalMs = 10000; // 10 seconds
|
const pollingIntervalMs = 10000; // 10 seconds
|
||||||
const groupByDate = ref(true);
|
const groupByDate = ref(true);
|
||||||
|
const startingStrategy = ref(false);
|
||||||
|
const credentialsDialogVisible = ref(false);
|
||||||
|
const missingPlatforms = ref([]);
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const postsByDate = computed(() => {
|
const postsByDate = computed(() => {
|
||||||
@@ -462,6 +512,98 @@ const toggleGroupByDate = () => {
|
|||||||
groupByDate.value = !groupByDate.value;
|
groupByDate.value = !groupByDate.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle start strategy
|
||||||
|
const handleStartStrategy = async () => {
|
||||||
|
if (!strategyId.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: 'ID стратегии не найден',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startingStrategy.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await MarketingService.startStrategy(strategyId.value);
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Стратегия запущена',
|
||||||
|
detail: `Стратегия успешно запущена. Создано задач: ${result.tasksCreated}. Платформы: ${result.platforms?.join(', ') || 'N/A'}`,
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при запуске стратегии:', error);
|
||||||
|
|
||||||
|
// Handle specific error codes
|
||||||
|
if (error.code === 'MISSING_CREDENTIALS') {
|
||||||
|
// Extract platform from error message (format: "Credentials not found for platform: facebook")
|
||||||
|
const platformMatch = error.message?.match(/platform:\s*(\w+)/i) || error.message?.match(/for platform\s+(\w+)/i);
|
||||||
|
if (platformMatch) {
|
||||||
|
missingPlatforms.value = [platformMatch[1].toLowerCase()];
|
||||||
|
} else {
|
||||||
|
// Try to get platforms from strategy data
|
||||||
|
const platforms = strategyData.value?.priorityPlatforms || strategyData.value?.strategy?.postCalendar?.map(p => p.platform).filter((v, i, a) => a.indexOf(v) === i) || [];
|
||||||
|
missingPlatforms.value = platforms.length > 0 ? platforms : ['facebook']; // Default to facebook if unknown
|
||||||
|
}
|
||||||
|
credentialsDialogVisible.value = true;
|
||||||
|
} else if (error.code === 'INVALID_STATUS') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Стратегия не готова',
|
||||||
|
detail: error.message || 'Стратегия еще не завершена. Дождитесь завершения генерации.',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'UNAUTHORIZED') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка авторизации',
|
||||||
|
detail: 'Необходимо войти в систему',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'NOT_FOUND') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Не найдено',
|
||||||
|
detail: 'Стратегия не найдена',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'FORBIDDEN') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Доступ запрещен',
|
||||||
|
detail: 'У вас нет доступа к этой стратегии',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else if (error.code === 'VALIDATION_ERROR') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка валидации',
|
||||||
|
detail: error.message || 'Проверьте правильность данных',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось запустить стратегию. Попробуйте позже.',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
startingStrategy.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Go to credentials page
|
||||||
|
const goToCredentials = () => {
|
||||||
|
credentialsDialogVisible.value = false;
|
||||||
|
router.push({ name: 'marketing-credentials' });
|
||||||
|
};
|
||||||
|
|
||||||
// Format date
|
// Format date
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|||||||
Reference in New Issue
Block a user