.
This commit is contained in:
@@ -0,0 +1,987 @@
|
|||||||
|
# API Документация: Маркетинговый анализ и стратегии (Frontend/AI Agent)
|
||||||
|
|
||||||
|
## Базовый URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://api.konturai.kz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
API для генерации маркетингового анализа и стратегий на основе данных о бизнесе. Все эндпоинты требуют JWT аутентификации для связи данных с пользователем.
|
||||||
|
|
||||||
|
**Важные изменения:**
|
||||||
|
|
||||||
|
- ✅ Все эндпоинты теперь требуют JWT токен в заголовке `Authorization`
|
||||||
|
- ✅ Пользователи могут видеть только свои анализы и стратегии
|
||||||
|
- ✅ Добавлена детальная история статусов для каждого анализа и стратегии
|
||||||
|
- ✅ Новые эндпоинты для получения списка всех анализов/стратегий пользователя
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
Все запросы должны включать JWT токен в заголовке `Authorization`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура JWT токена
|
||||||
|
|
||||||
|
JWT токен содержит следующую информацию:
|
||||||
|
|
||||||
|
- **sub**: Email пользователя
|
||||||
|
- **uid**: ID пользователя (Long) - используется для связи данных
|
||||||
|
- **roles**: Роли пользователя (String, разделённые запятыми)
|
||||||
|
|
||||||
|
### Ошибки аутентификации
|
||||||
|
|
||||||
|
Если токен отсутствует или невалиден, API вернет:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Не авторизован",
|
||||||
|
"error": {
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP статус**: `401 Unauthorized`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Эндпоинты
|
||||||
|
|
||||||
|
### 1. Запуск маркетингового анализа
|
||||||
|
|
||||||
|
**POST** `/api/marketing/analysis/start`
|
||||||
|
|
||||||
|
Создает новую задачу на генерацию маркетингового анализа и запускает асинхронную обработку. Анализ автоматически связывается с пользователем из JWT токена.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Тело запроса (JSON)
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||||
|
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||||
|
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||||
|
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||||
|
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||||
|
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||||
|
|
||||||
|
#### Валидация полей
|
||||||
|
|
||||||
|
**`product`** (string, обязательное)
|
||||||
|
|
||||||
|
- Минимальная длина: 3 символа
|
||||||
|
- Максимальная длина: 200 символов
|
||||||
|
|
||||||
|
**`location`** (string, обязательное)
|
||||||
|
|
||||||
|
- Минимальная длина: 2 символа
|
||||||
|
- Максимальная длина: 150 символов
|
||||||
|
|
||||||
|
**`client`** (string, обязательное)
|
||||||
|
|
||||||
|
- Допустимые значения:
|
||||||
|
- `"B2B клиенты"`
|
||||||
|
- `"B2C клиенты"`
|
||||||
|
- `"Частные лица"`
|
||||||
|
- `"Корпорации"`
|
||||||
|
- `"Малый бизнес"`
|
||||||
|
|
||||||
|
**`differentiator`** (string, обязательное)
|
||||||
|
|
||||||
|
- Минимальная длина: 10 символов
|
||||||
|
- Максимальная длина: 500 символов
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```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 недели',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Анализ запущен успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "processing",
|
||||||
|
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Получение результата анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}`
|
||||||
|
|
||||||
|
Возвращает статус и результаты анализа по идентификатору. Пользователь может получить только свои анализы.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример ответа (когда анализ завершен - 200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"createdAt": "2025-01-20T15:30:00",
|
||||||
|
"completedAt": "2025-01-20T15:38:00",
|
||||||
|
"report": {
|
||||||
|
"summary": "Краткое резюме анализа...",
|
||||||
|
"targetAudience": {
|
||||||
|
"description": "Описание целевой аудитории...",
|
||||||
|
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||||
|
},
|
||||||
|
"recommendations": ["Рекомендация 1", "Рекомендация 2"],
|
||||||
|
"strategy": {
|
||||||
|
"duration": "2 недели",
|
||||||
|
"channels": ["Instagram", "Telegram", "21MC"],
|
||||||
|
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||||
|
},
|
||||||
|
"pdfUrl": "/api/marketing/analysis/507f1f77bcf86cd799439011/download"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Статусы анализа
|
||||||
|
|
||||||
|
| Статус | Описание |
|
||||||
|
| ------------ | ----------------------------- |
|
||||||
|
| `queued` | Запрос в очереди на обработку |
|
||||||
|
| `processing` | Анализ выполняется |
|
||||||
|
| `completed` | Анализ завершен успешно |
|
||||||
|
| `failed` | Анализ завершился с ошибкой |
|
||||||
|
|
||||||
|
#### Ошибки доступа
|
||||||
|
|
||||||
|
Если пользователь пытается получить доступ к анализу другого пользователя:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Доступ запрещен",
|
||||||
|
"error": {
|
||||||
|
"code": "FORBIDDEN",
|
||||||
|
"message": "У вас нет доступа к этому анализу"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP статус**: `403 Forbidden`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Получение списка всех анализов пользователя
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/my`
|
||||||
|
|
||||||
|
Возвращает список всех анализов текущего пользователя, отсортированных по дате создания (новые первыми).
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"product": "Разработка мобильных приложений",
|
||||||
|
"location": "Нур-Султан, Казахстан",
|
||||||
|
"clientType": "B2B клиенты",
|
||||||
|
"differentiator": "Быстрая разработка MVP за 4 недели",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T15:30:00",
|
||||||
|
"completedAt": "2025-01-20T15:38:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:30:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:30:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ успешно завершен"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"analysisId": "507f1f77bcf86cd799439012",
|
||||||
|
"product": "Веб-разработка",
|
||||||
|
"location": "Алматы, Казахстан",
|
||||||
|
"clientType": "B2C клиенты",
|
||||||
|
"differentiator": "Современные технологии",
|
||||||
|
"status": "processing",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T14:00:00",
|
||||||
|
"completedAt": null,
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T14:00:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T14:00:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Структура ответа
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
| ---------------------------------- | ------ | ------------------------------------------------------ |
|
||||||
|
| `data[].analysisId` | string | Уникальный идентификатор анализа |
|
||||||
|
| `data[].product` | string | Название продукта или услуги |
|
||||||
|
| `data[].location` | string | Географическая локация |
|
||||||
|
| `data[].clientType` | string | Тип целевой аудитории |
|
||||||
|
| `data[].differentiator` | string | Уникальные особенности бизнеса |
|
||||||
|
| `data[].status` | string | Текущий статус анализа |
|
||||||
|
| `data[].userId` | string | ID пользователя (из JWT) |
|
||||||
|
| `data[].createdAt` | string | ISO 8601 дата/время создания |
|
||||||
|
| `data[].completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||||
|
| `data[].statusHistory` | array | Детальная история изменений статуса |
|
||||||
|
| `data[].statusHistory[].status` | string | Статус на момент изменения |
|
||||||
|
| `data[].statusHistory[].timestamp` | string | ISO 8601 дата/время изменения статуса |
|
||||||
|
| `data[].statusHistory[].message` | string | Описание изменения статуса |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Получение детальной истории анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/history`
|
||||||
|
|
||||||
|
Возвращает детальную информацию об анализе, включая полную историю изменений статуса.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/history`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"product": "Разработка мобильных приложений",
|
||||||
|
"location": "Нур-Султан, Казахстан",
|
||||||
|
"clientType": "B2B клиенты",
|
||||||
|
"differentiator": "Быстрая разработка MVP за 4 недели",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T15:30:00",
|
||||||
|
"completedAt": "2025-01-20T15:38:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:30:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:30:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ успешно завершен"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Скачивание PDF отчета
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/download`
|
||||||
|
|
||||||
|
Возвращает PDF файл с полным маркетинговым отчетом. Пользователь может скачать только свои отчеты.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/download`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `marketing_analysis_${analysisId}.pdf`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Успешный ответ (200 OK)
|
||||||
|
|
||||||
|
- **Content-Type**: `application/pdf`
|
||||||
|
- **Content-Disposition**: `attachment; filename="marketing_analysis_{analysisId}_{timestamp}.pdf"`
|
||||||
|
- **Body**: Бинарные данные PDF файла
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Генерация маркетинговой стратегии
|
||||||
|
|
||||||
|
**POST** `/api/marketing/analysis/strategy/generate`
|
||||||
|
|
||||||
|
Создает маркетинговую стратегию на основе завершенного анализа. Стратегия автоматически связывается с пользователем из JWT токена.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры запроса
|
||||||
|
|
||||||
|
| Параметр | Тип | Обязательный | Описание |
|
||||||
|
| ------------------- | ------- | ------------ | --------------------------------------------- |
|
||||||
|
| `analysisId` | string | ✅ | ID завершенного анализа (query parameter) |
|
||||||
|
| `durationWeeks` | integer | ❌ | Длительность стратегии в неделях (default: 4) |
|
||||||
|
| `priorityPlatforms` | array | ❌ | Приоритетные платформы (массив строк) |
|
||||||
|
|
||||||
|
#### Тело запроса (JSON, опционально)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/strategy/generate?analysisId=${analysisId}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
durationWeeks: 4,
|
||||||
|
priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "queued",
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": null,
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Получение результата стратегии
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/{strategyId}`
|
||||||
|
|
||||||
|
Возвращает статус и результаты стратегии. Пользователь может получить только свои стратегии.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | ----------------------- |
|
||||||
|
| `strategyId` | string | Идентификатор стратегии |
|
||||||
|
|
||||||
|
#### Пример ответа (когда стратегия завершена - 200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": "2025-01-20T15:43:00",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||||
|
"strategy": {
|
||||||
|
"weeklyPlans": [
|
||||||
|
{
|
||||||
|
"weekNumber": 1,
|
||||||
|
"mainThemes": ["Презентация продукта", "Преимущества"],
|
||||||
|
"contentRecommendations": "Создавайте контент, который демонстрирует ценность продукта",
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"publishDate": "2025-01-21T10:00:00",
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "пост",
|
||||||
|
"theme": "Презентация продукта",
|
||||||
|
"postText": "Полный текст поста для публикации...",
|
||||||
|
"hashtags": ["#маркетинг", "#бизнес"],
|
||||||
|
"publishTime": "10:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Получение стратегии по ID анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/strategy`
|
||||||
|
|
||||||
|
Возвращает стратегию, связанную с указанным анализом. Пользователь может получить только стратегии для своих анализов.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Получение списка всех стратегий пользователя
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/my`
|
||||||
|
|
||||||
|
Возвращает список всех стратегий текущего пользователя, отсортированных по дате создания (новые первыми).
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": "2025-01-20T15:43:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:40:00",
|
||||||
|
"message": "Стратегия создана и добавлена в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:40:05",
|
||||||
|
"message": "Начата генерация стратегии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:43:00",
|
||||||
|
"message": "Стратегия успешно сгенерирована"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Получение детальной истории стратегии
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/{strategyId}/history`
|
||||||
|
|
||||||
|
Возвращает детальную информацию о стратегии, включая полную историю изменений статуса.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | ----------------------- |
|
||||||
|
| `strategyId` | string | Идентификатор стратегии |
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": "2025-01-20T15:43:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:40:00",
|
||||||
|
"message": "Стратегия создана и добавлена в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:40:05",
|
||||||
|
"message": "Начата генерация стратегии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:43:00",
|
||||||
|
"message": "Стратегия успешно сгенерирована"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
### Коды ошибок
|
||||||
|
|
||||||
|
| Код | HTTP статус | Описание |
|
||||||
|
| ------------------------ | ----------- | ------------------------------- |
|
||||||
|
| `UNAUTHORIZED` | 401 | Требуется аутентификация |
|
||||||
|
| `FORBIDDEN` | 403 | Нет доступа к ресурсу |
|
||||||
|
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||||
|
| `NOT_FOUND` | 404 | Ресурс не найден |
|
||||||
|
| `INVALID_ANALYSIS` | 404 | Анализ не найден |
|
||||||
|
| `ANALYSIS_NOT_COMPLETED` | 400 | Анализ еще не завершен |
|
||||||
|
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||||
|
|
||||||
|
### Формат ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Описание ошибки",
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Детальное сообщение об ошибке",
|
||||||
|
"details": {
|
||||||
|
"field1": "Сообщение об ошибке для поля 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### JavaScript/TypeScript (Fetch API)
|
||||||
|
|
||||||
|
#### Получение JWT токена
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Предполагается, что токен получен при логине
|
||||||
|
const jwtToken = localStorage.getItem('jwtToken');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Запуск анализа с JWT
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function startMarketingAnalysis(data, jwtToken) {
|
||||||
|
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(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error('Требуется аутентификация');
|
||||||
|
}
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error?.message || 'Ошибка при запуске анализа');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data.analysisId;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение списка всех анализов пользователя
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getMyAnalyses(jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка при получении списка анализов');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data; // Массив анализов
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение детальной истории анализа
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getAnalysisHistory(analysisId, jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/history`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
throw new Error('Нет доступа к этому анализу');
|
||||||
|
}
|
||||||
|
throw new Error('Ошибка при получении истории');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Полный пример: создание анализа и отслеживание статуса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createAndTrackAnalysis(
|
||||||
|
product,
|
||||||
|
location,
|
||||||
|
client,
|
||||||
|
differentiator,
|
||||||
|
jwtToken
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 1. Запускаем анализ
|
||||||
|
const analysisId = await startMarketingAnalysis(
|
||||||
|
{
|
||||||
|
product,
|
||||||
|
location,
|
||||||
|
client,
|
||||||
|
differentiator,
|
||||||
|
},
|
||||||
|
jwtToken
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Анализ запущен: ${analysisId}`);
|
||||||
|
|
||||||
|
// 2. Получаем историю для отслеживания статуса
|
||||||
|
const checkStatus = async () => {
|
||||||
|
const history = await getAnalysisHistory(analysisId, jwtToken);
|
||||||
|
|
||||||
|
// Показываем последний статус
|
||||||
|
const lastStatus =
|
||||||
|
history.statusHistory[history.statusHistory.length - 1];
|
||||||
|
console.log(`Статус: ${lastStatus.status} - ${lastStatus.message}`);
|
||||||
|
|
||||||
|
return history.status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Polling: проверяем статус каждые 10 секунд
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
const status = await checkStatus();
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
console.log('Анализ завершен!');
|
||||||
|
// Получаем полный результат
|
||||||
|
const fullResult = await getAnalysisResult(analysisId, jwtToken);
|
||||||
|
return fullResult;
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
throw new Error('Анализ завершился с ошибкой');
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// Останавливаем polling через 15 минут
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
console.log('Превышено время ожидания');
|
||||||
|
}, 15 * 60 * 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение списка стратегий пользователя
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getMyStrategies(jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/strategy/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка при получении списка стратегий');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data; // Массив стратегий
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендации по интеграции
|
||||||
|
|
||||||
|
### 1. Обработка JWT токена
|
||||||
|
|
||||||
|
- Сохраняйте токен в безопасном месте (например, `localStorage` или `sessionStorage`)
|
||||||
|
- Проверяйте срок действия токена перед запросами
|
||||||
|
- Реализуйте механизм обновления токена при истечении
|
||||||
|
|
||||||
|
### 2. Обработка ошибок аутентификации
|
||||||
|
|
||||||
|
При получении `401 Unauthorized`:
|
||||||
|
|
||||||
|
- Перенаправляйте пользователя на страницу входа
|
||||||
|
- Очищайте сохраненный токен
|
||||||
|
- Показывайте понятное сообщение пользователю
|
||||||
|
|
||||||
|
### 3. Обработка ошибок доступа
|
||||||
|
|
||||||
|
При получении `403 Forbidden`:
|
||||||
|
|
||||||
|
- Показывайте сообщение о том, что ресурс недоступен
|
||||||
|
- Не пытайтесь повторять запрос с теми же параметрами
|
||||||
|
|
||||||
|
### 4. Polling стратегия
|
||||||
|
|
||||||
|
Для отслеживания статуса анализа/стратегии:
|
||||||
|
|
||||||
|
- Используйте интервал 10-15 секунд
|
||||||
|
- Максимальное время ожидания: 15 минут
|
||||||
|
- Показывайте прогресс пользователю на основе `statusHistory`
|
||||||
|
|
||||||
|
### 5. Отображение истории статусов
|
||||||
|
|
||||||
|
Используйте `statusHistory` для:
|
||||||
|
|
||||||
|
- Показывать временную шкалу изменений статуса
|
||||||
|
- Отображать детальную информацию о каждом этапе
|
||||||
|
- Информировать пользователя о прогрессе
|
||||||
|
|
||||||
|
### 6. Кэширование
|
||||||
|
|
||||||
|
- Кэшируйте список анализов/стратегий пользователя
|
||||||
|
- Обновляйте кэш при создании новых записей
|
||||||
|
- Используйте `analysisId`/`strategyId` как ключи кэша
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||||
|
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||||
|
3. **Асинхронность**: Анализ и стратегия выполняются асинхронно
|
||||||
|
4. **Безопасность**: Все эндпоинты требуют валидный JWT токен
|
||||||
|
5. **Изоляция данных**: Пользователи видят только свои данные
|
||||||
|
6. **История статусов**: Детальная история доступна для всех анализов и стратегий
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||||
|
|
||||||
|
- `analysisId` или `strategyId` (если есть)
|
||||||
|
- Время запроса
|
||||||
|
- Описание проблемы
|
||||||
|
- Код ошибки (если есть)
|
||||||
|
- JWT токен (только для отладки, не в продакшене!)
|
||||||
+5
-1
@@ -54,11 +54,15 @@ export const API_CONFIG = {
|
|||||||
MARKETING_ANALYSIS_START: '/api/marketing/analysis/start',
|
MARKETING_ANALYSIS_START: '/api/marketing/analysis/start',
|
||||||
MARKETING_ANALYSIS_GET: '/api/marketing/analysis',
|
MARKETING_ANALYSIS_GET: '/api/marketing/analysis',
|
||||||
MARKETING_ANALYSIS_DOWNLOAD: '/api/marketing/analysis',
|
MARKETING_ANALYSIS_DOWNLOAD: '/api/marketing/analysis',
|
||||||
|
MARKETING_ANALYSIS_MY: '/api/marketing/analysis/my',
|
||||||
|
MARKETING_ANALYSIS_HISTORY: '/api/marketing/analysis',
|
||||||
|
|
||||||
// MarketingStrategyController - Стратегия продвижения
|
// MarketingStrategyController - Стратегия продвижения
|
||||||
MARKETING_STRATEGY_GENERATE: '/api/marketing/analysis/strategy/generate',
|
MARKETING_STRATEGY_GENERATE: '/api/marketing/analysis/strategy/generate',
|
||||||
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_HISTORY: '/api/marketing/analysis/strategy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -243,6 +243,16 @@ const router = createRouter({
|
|||||||
name: 'marketing-results',
|
name: 'marketing-results',
|
||||||
component: () => import('@/views/pages/marketing/MarketingResults.vue')
|
component: () => import('@/views/pages/marketing/MarketingResults.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/marketing-analysis/analyses',
|
||||||
|
name: 'marketing-analyses-list',
|
||||||
|
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/marketing-analysis/strategies',
|
||||||
|
name: 'marketing-strategies-list',
|
||||||
|
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/pages/notfound',
|
path: '/pages/notfound',
|
||||||
name: 'notfound',
|
name: 'notfound',
|
||||||
|
|||||||
@@ -71,6 +71,59 @@ class MarketingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка всех анализов текущего пользователя
|
||||||
|
* GET /api/marketing/analysis/my
|
||||||
|
* @returns {Promise<Array>} Массив анализов с полями analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
|
||||||
|
*/
|
||||||
|
async getMyAnalyses() {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_MY}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка анализов');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении списка анализов');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении списка анализов:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение детальной истории анализа
|
||||||
|
* GET /api/marketing/analysis/{analysisId}/history
|
||||||
|
* @param {string} analysisId - Идентификатор анализа
|
||||||
|
* @returns {Promise<Object>} Объект анализа с полной историей статусов
|
||||||
|
*/
|
||||||
|
async getAnalysisHistory(analysisId) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_HISTORY}/${analysisId}/history`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении истории анализа');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении истории анализа');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении истории анализа:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачивание PDF отчета
|
* Скачивание PDF отчета
|
||||||
* GET /api/marketing/analysis/{analysisId}/download
|
* GET /api/marketing/analysis/{analysisId}/download
|
||||||
@@ -212,6 +265,59 @@ class MarketingService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка всех стратегий текущего пользователя
|
||||||
|
* GET /api/marketing/analysis/strategy/my
|
||||||
|
* @returns {Promise<Array>} Массив стратегий с полями strategyId, analysisId, status, userId, durationWeeks, priorityPlatforms, createdAt, completedAt, statusHistory
|
||||||
|
*/
|
||||||
|
async getMyStrategies() {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_MY}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка стратегий');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении списка стратегий');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении списка стратегий:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение детальной истории стратегии
|
||||||
|
* GET /api/marketing/analysis/strategy/{strategyId}/history
|
||||||
|
* @param {string} strategyId - Идентификатор стратегии
|
||||||
|
* @returns {Promise<Object>} Объект стратегии с полной историей статусов
|
||||||
|
*/
|
||||||
|
async getStrategyHistory(strategyId) {
|
||||||
|
try {
|
||||||
|
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_HISTORY}/${strategyId}/history`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || result.error?.message || 'Ошибка при получении истории стратегии');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || 'Ошибка при получении истории стратегии');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении истории стратегии:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MarketingService();
|
export default new MarketingService();
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<div class="marketing-analyses-list-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||||
|
<Toast />
|
||||||
|
<div class="max-w-[1400px] mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header mb-4">
|
||||||
|
<div class="flex justify-content-between align-items-center">
|
||||||
|
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Мои анализы</h2>
|
||||||
|
<Button label="Создать новый анализ" icon="pi pi-plus" severity="success" @click="goToCreateAnalysis" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<DataTable
|
||||||
|
:value="analyses"
|
||||||
|
:loading="loading"
|
||||||
|
paginator
|
||||||
|
:rows="10"
|
||||||
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||||
|
sortMode="multiple"
|
||||||
|
removableSort
|
||||||
|
:sortField="'createdAt'"
|
||||||
|
:sortOrder="-1"
|
||||||
|
filterDisplay="row"
|
||||||
|
:globalFilterFields="['product', 'location', 'clientType', 'status']"
|
||||||
|
v-model:filters="filters"
|
||||||
|
:filters="filters"
|
||||||
|
dataKey="analysisId"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<span class="p-input-icon-left">
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
<InputText v-model="filters['global'].value" placeholder="Поиск анализов..." />
|
||||||
|
</span>
|
||||||
|
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadAnalyses" :loading="loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="product" header="Продукт" :sortable="true" style="min-width: 200px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span class="font-semibold">{{ slotProps.data.product }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="location" header="Локация" :sortable="true" style="min-width: 150px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span>{{ slotProps.data.location }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="clientType" header="Тип клиента" :sortable="true" style="min-width: 150px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag :value="slotProps.data.clientType" severity="info" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||||
|
</template>
|
||||||
|
<template #filter="{ filterModel, filterCallback }">
|
||||||
|
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
|
||||||
|
<template #option="slotProps">
|
||||||
|
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="createdAt" header="Создан" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span>{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="completedAt" header="Завершен" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.completedAt">{{ formatDateTime(slotProps.data.completedAt) }}</span>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Действия" style="min-width: 250px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button icon="pi pi-eye" severity="info" size="small" v-tooltip.top="'Просмотр деталей'" @click="viewAnalysis(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||||
|
<Button icon="pi pi-history" severity="secondary" size="small" v-tooltip.top="'История статусов'" @click="viewHistory(slotProps.data.analysisId)" />
|
||||||
|
<Button icon="pi pi-download" severity="success" size="small" v-tooltip.top="'Скачать PDF'" @click="downloadPdf(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||||
|
<Button icon="pi pi-magic" severity="warning" size="small" v-tooltip.top="'Создать стратегию'" @click="createStrategy(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Диалог истории статусов -->
|
||||||
|
<Dialog v-model:visible="historyDialogVisible" :header="'История статусов анализа'" :style="{ width: '600px' }" :modal="true">
|
||||||
|
<div v-if="historyLoading" class="text-center py-4">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="statusHistory && statusHistory.length > 0" class="status-history">
|
||||||
|
<Timeline :value="statusHistory" align="left" class="w-full">
|
||||||
|
<template #marker="slotProps">
|
||||||
|
<span class="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1" :class="getStatusColorClass(slotProps.item.status)">
|
||||||
|
<i :class="getStatusIcon(slotProps.item.status)"></i>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #content="slotProps">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<div class="flex align-items-center gap-2 mb-2">
|
||||||
|
<Tag :value="getStatusLabel(slotProps.item.status)" :severity="getStatusSeverity(slotProps.item.status)" />
|
||||||
|
<span class="text-sm text-surface-500 dark:text-surface-400">{{ formatDateTime(slotProps.item.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-surface-700 dark:text-surface-300 m-0">{{ slotProps.item.message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Timeline>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-4 text-surface-500">История статусов недоступна</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Закрыть" icon="pi pi-times" @click="historyDialogVisible = false" severity="secondary" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarketingService from '@/service/MarketingService';
|
||||||
|
import { FilterMatchMode } from '@primevue/core/api';
|
||||||
|
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 ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
import Timeline from 'primevue/timeline';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const analyses = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const historyLoading = ref(false);
|
||||||
|
const statusHistory = ref([]);
|
||||||
|
const historyDialogVisible = ref(false);
|
||||||
|
const currentAnalysisId = ref(null);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const filters = ref({
|
||||||
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||||
|
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status options for filter
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'В очереди', value: 'queued', severity: 'info' },
|
||||||
|
{ label: 'Обрабатывается', value: 'processing', severity: 'warning' },
|
||||||
|
{ label: 'Завершен', value: 'completed', severity: 'success' },
|
||||||
|
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load analyses
|
||||||
|
const loadAnalyses = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await MarketingService.getMyAnalyses();
|
||||||
|
analyses.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке списка анализов:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить список анализов',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// View analysis details
|
||||||
|
const viewAnalysis = (analysisId) => {
|
||||||
|
router.push({
|
||||||
|
name: 'marketing-analysis',
|
||||||
|
query: { analysisId }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// View history
|
||||||
|
const viewHistory = async (analysisId) => {
|
||||||
|
currentAnalysisId.value = analysisId;
|
||||||
|
historyDialogVisible.value = true;
|
||||||
|
historyLoading.value = true;
|
||||||
|
statusHistory.value = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await MarketingService.getAnalysisHistory(analysisId);
|
||||||
|
statusHistory.value = data.statusHistory || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке истории:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить историю статусов',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download PDF
|
||||||
|
const downloadPdf = async (analysisId) => {
|
||||||
|
try {
|
||||||
|
await MarketingService.downloadPdfFile(analysisId);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'PDF скачан',
|
||||||
|
detail: 'PDF отчет успешно скачан',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при скачивании PDF:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось скачать PDF отчет',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create strategy
|
||||||
|
const createStrategy = (analysisId) => {
|
||||||
|
router.push({
|
||||||
|
name: 'marketing-promotion',
|
||||||
|
query: { analysisId }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to create analysis
|
||||||
|
const goToCreateAnalysis = () => {
|
||||||
|
router.push({ name: 'marketing-analysis' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status label
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const labels = {
|
||||||
|
queued: 'В очереди',
|
||||||
|
processing: 'Обрабатывается',
|
||||||
|
completed: 'Завершен',
|
||||||
|
failed: 'Ошибка'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status severity
|
||||||
|
const getStatusSeverity = (status) => {
|
||||||
|
const severities = {
|
||||||
|
queued: 'info',
|
||||||
|
processing: 'warning',
|
||||||
|
completed: 'success',
|
||||||
|
failed: 'danger'
|
||||||
|
};
|
||||||
|
return severities[status] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status color class for timeline
|
||||||
|
const getStatusColorClass = (status) => {
|
||||||
|
const classes = {
|
||||||
|
queued: 'bg-blue-500',
|
||||||
|
processing: 'bg-yellow-500',
|
||||||
|
completed: 'bg-green-500',
|
||||||
|
failed: 'bg-red-500'
|
||||||
|
};
|
||||||
|
return classes[status] || 'bg-surface-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status icon
|
||||||
|
const getStatusIcon = (status) => {
|
||||||
|
const icons = {
|
||||||
|
queued: 'pi pi-clock',
|
||||||
|
processing: 'pi pi-spin pi-spinner',
|
||||||
|
completed: 'pi pi-check',
|
||||||
|
failed: 'pi pi-times'
|
||||||
|
};
|
||||||
|
return icons[status] || 'pi pi-circle';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load on mount
|
||||||
|
onMounted(() => {
|
||||||
|
loadAnalyses();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.marketing-analyses-list-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-history {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -51,6 +51,13 @@ const menuItems = [
|
|||||||
iconColor: 'text-primary-500',
|
iconColor: 'text-primary-500',
|
||||||
route: '/marketing-analysis/analysis'
|
route: '/marketing-analysis/analysis'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'analyses-list',
|
||||||
|
label: 'Мои анализы',
|
||||||
|
icon: 'pi pi-list',
|
||||||
|
iconColor: 'text-primary-500',
|
||||||
|
route: '/marketing-analysis/analyses'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'promotion',
|
id: 'promotion',
|
||||||
label: 'Продвижение',
|
label: 'Продвижение',
|
||||||
@@ -58,6 +65,13 @@ const menuItems = [
|
|||||||
iconColor: 'text-primary-500',
|
iconColor: 'text-primary-500',
|
||||||
route: '/marketing-analysis/promotion'
|
route: '/marketing-analysis/promotion'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'strategies-list',
|
||||||
|
label: 'Мои стратегии',
|
||||||
|
icon: 'pi pi-briefcase',
|
||||||
|
iconColor: 'text-primary-500',
|
||||||
|
route: '/marketing-analysis/strategies'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'results',
|
id: 'results',
|
||||||
label: 'Результаты',
|
label: 'Результаты',
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<template>
|
||||||
|
<div class="marketing-strategies-list-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||||
|
<Toast />
|
||||||
|
<div class="max-w-[1400px] mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header mb-4">
|
||||||
|
<div class="flex justify-content-between align-items-center">
|
||||||
|
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Мои стратегии</h2>
|
||||||
|
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="success" @click="goToCreateStrategy" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<DataTable
|
||||||
|
:value="strategies"
|
||||||
|
:loading="loading"
|
||||||
|
paginator
|
||||||
|
:rows="10"
|
||||||
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||||
|
sortMode="multiple"
|
||||||
|
removableSort
|
||||||
|
:sortField="'createdAt'"
|
||||||
|
:sortOrder="-1"
|
||||||
|
filterDisplay="row"
|
||||||
|
:globalFilterFields="['analysisId', 'status']"
|
||||||
|
v-model:filters="filters"
|
||||||
|
:filters="filters"
|
||||||
|
dataKey="strategyId"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<span class="p-input-icon-left">
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
<InputText v-model="filters['global'].value" placeholder="Поиск стратегий..." />
|
||||||
|
</span>
|
||||||
|
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadStrategies" :loading="loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="analysisId" header="ID анализа" :sortable="true" style="min-width: 200px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span class="font-mono text-sm">{{ slotProps.data.analysisId }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||||
|
</template>
|
||||||
|
<template #filter="{ filterModel, filterCallback }">
|
||||||
|
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
|
||||||
|
<template #option="slotProps">
|
||||||
|
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="durationWeeks" header="Длительность" :sortable="true" style="min-width: 120px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span>{{ slotProps.data.durationWeeks }} {{ pluralize(slotProps.data.durationWeeks, 'неделя', 'недели', 'недель') }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="priorityPlatforms" header="Платформы" style="min-width: 200px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div v-if="slotProps.data.priorityPlatforms && slotProps.data.priorityPlatforms.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<Tag v-for="platform in slotProps.data.priorityPlatforms" :key="platform" :value="platform" severity="secondary" />
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="createdAt" header="Создана" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span>{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="completedAt" header="Завершена" :sortable="true" style="min-width: 180px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.completedAt">{{ formatDateTime(slotProps.data.completedAt) }}</span>
|
||||||
|
<span v-else class="text-surface-400">—</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Действия" style="min-width: 200px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button icon="pi pi-eye" severity="info" size="small" v-tooltip.top="'Просмотр деталей'" @click="viewStrategy(slotProps.data.strategyId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||||
|
<Button icon="pi pi-history" severity="secondary" size="small" v-tooltip.top="'История статусов'" @click="viewHistory(slotProps.data.strategyId)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Диалог истории статусов -->
|
||||||
|
<Dialog v-model:visible="historyDialogVisible" :header="'История статусов стратегии'" :style="{ width: '600px' }" :modal="true">
|
||||||
|
<div v-if="historyLoading" class="text-center py-4">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="statusHistory && statusHistory.length > 0" class="status-history">
|
||||||
|
<Timeline :value="statusHistory" align="left" class="w-full">
|
||||||
|
<template #marker="slotProps">
|
||||||
|
<span class="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1" :class="getStatusColorClass(slotProps.item.status)">
|
||||||
|
<i :class="getStatusIcon(slotProps.item.status)"></i>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #content="slotProps">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<div class="flex align-items-center gap-2 mb-2">
|
||||||
|
<Tag :value="getStatusLabel(slotProps.item.status)" :severity="getStatusSeverity(slotProps.item.status)" />
|
||||||
|
<span class="text-sm text-surface-500 dark:text-surface-400">{{ formatDateTime(slotProps.item.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-surface-700 dark:text-surface-300 m-0">{{ slotProps.item.message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Timeline>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-4 text-surface-500">История статусов недоступна</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Закрыть" icon="pi pi-times" @click="historyDialogVisible = false" severity="secondary" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarketingService from '@/service/MarketingService';
|
||||||
|
import { FilterMatchMode } from '@primevue/core/api';
|
||||||
|
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 ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
import Timeline from 'primevue/timeline';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const strategies = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const historyLoading = ref(false);
|
||||||
|
const statusHistory = ref([]);
|
||||||
|
const historyDialogVisible = ref(false);
|
||||||
|
const currentStrategyId = ref(null);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const filters = ref({
|
||||||
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||||
|
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status options for filter
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'В очереди', value: 'queued', severity: 'info' },
|
||||||
|
{ label: 'Обрабатывается', value: 'processing', severity: 'warning' },
|
||||||
|
{ label: 'Завершена', value: 'completed', severity: 'success' },
|
||||||
|
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load strategies
|
||||||
|
const loadStrategies = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await MarketingService.getMyStrategies();
|
||||||
|
strategies.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке списка стратегий:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить список стратегий',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// View strategy details
|
||||||
|
const viewStrategy = (strategyId) => {
|
||||||
|
router.push({
|
||||||
|
name: 'marketing-promotion',
|
||||||
|
query: { strategyId }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// View history
|
||||||
|
const viewHistory = async (strategyId) => {
|
||||||
|
currentStrategyId.value = strategyId;
|
||||||
|
historyDialogVisible.value = true;
|
||||||
|
historyLoading.value = true;
|
||||||
|
statusHistory.value = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await MarketingService.getStrategyHistory(strategyId);
|
||||||
|
statusHistory.value = data.statusHistory || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке истории:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: error.message || 'Не удалось загрузить историю статусов',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to create strategy
|
||||||
|
const goToCreateStrategy = () => {
|
||||||
|
router.push({ name: 'marketing-promotion' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status label
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const labels = {
|
||||||
|
queued: 'В очереди',
|
||||||
|
processing: 'Обрабатывается',
|
||||||
|
completed: 'Завершена',
|
||||||
|
failed: 'Ошибка'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status severity
|
||||||
|
const getStatusSeverity = (status) => {
|
||||||
|
const severities = {
|
||||||
|
queued: 'info',
|
||||||
|
processing: 'warning',
|
||||||
|
completed: 'success',
|
||||||
|
failed: 'danger'
|
||||||
|
};
|
||||||
|
return severities[status] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status color class for timeline
|
||||||
|
const getStatusColorClass = (status) => {
|
||||||
|
const classes = {
|
||||||
|
queued: 'bg-blue-500',
|
||||||
|
processing: 'bg-yellow-500',
|
||||||
|
completed: 'bg-green-500',
|
||||||
|
failed: 'bg-red-500'
|
||||||
|
};
|
||||||
|
return classes[status] || 'bg-surface-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get status icon
|
||||||
|
const getStatusIcon = (status) => {
|
||||||
|
const icons = {
|
||||||
|
queued: 'pi pi-clock',
|
||||||
|
processing: 'pi pi-spin pi-spinner',
|
||||||
|
completed: 'pi pi-check',
|
||||||
|
failed: 'pi pi-times'
|
||||||
|
};
|
||||||
|
return icons[status] || 'pi pi-circle';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pluralize
|
||||||
|
const pluralize = (count, one, few, many) => {
|
||||||
|
const mod10 = count % 10;
|
||||||
|
const mod100 = count % 100;
|
||||||
|
|
||||||
|
if (mod10 === 1 && mod100 !== 11) return one;
|
||||||
|
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
|
||||||
|
return many;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load on mount
|
||||||
|
onMounted(() => {
|
||||||
|
loadStrategies();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.marketing-strategies-list-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-history {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user