diff --git a/data.json b/data.json new file mode 100644 index 0000000..171ccc5 --- /dev/null +++ b/data.json @@ -0,0 +1,198 @@ +{ + "report_title": "Маркетинговый анализ для интернет-продвижения", + "sections": { + "market_overview": { + "title": "Обзор рынка", + "metrics": { + "online_demand_index": "76/100", + "online_potential": "Высокий", + "demand_growth_rate_yoy": "+27%", + "seasonality_coefficient_spring": "1.34x", + "seasonality_range": "1.29 - 1.38" + }, + "monthly_online_index": { + "march": 1.32, + "april": 1.34, + "may": 1.38 + }, + "geography": "Казахстан (анализ по регионам представлен на тепловой карте)", + "conclusion": "Онлайн-спрос показывает устойчивый рост в течение года, весной достигая пика. Интернет-продвижение даст максимальный эффект." + }, + "target_audience": { + "title": "Целевая аудитория", + "metrics": { + "total_audience_size": "1.8 млн человек", + "leading_segment_share": "29%", + "above_average_income_share": "35%" + }, + "segments": [ + { + "name": "Женщины, 25–34 года", + "share": "29%" + }, + { + "name": "Мужчины, 25–44 года", + "share": "24%" + }, + { + "name": "Доход выше среднего", + "share": "35%" + } + ], + "age_structure": { + "18-24": "10%", + "25-34": "29%", + "35-44": "Высокая доля (визуально на графике)", + "45-54": "15-24%", + "55_plus": "Низкая доля" + }, + "conclusion": "Основную ценность представляют мужчины и женщины 25–44 лет с доходом выше среднего (до 35% аудитории)." + }, + "competitive_environment": { + "title": "Конкурентная среда", + "metrics": { + "market_share_traffic": "16%", + "visibility_rank": "#3", + "competitor_share_in_context_ads": "27%", + "benchmark_astana": "13-25%" + }, + "digital_visibility_benchmarks": { + "competitor_a": 1050, + "your_business": 600, + "lead": 300, + "payment": 450, + "others": 680 + }, + "competitor_strengths": { + "your_business": ["Цена", "Оффер"], + "competitor_a": ["Отзывы", "Рекламный бюджет"], + "competitor_b": ["Контент", "Соцсети", "Экспириенс"], + "competitor_c": ["Контент", "Сайт"] + }, + "conclusion": "Ваш бизнес занимает 3-е место по видимости. Основные преимущества конкурентов — соцдоказательства и контент. Ценовая борьба не является оптимальной." + }, + "demand_and_behavior": { + "title": "Спрос и поведение", + "metrics": { + "search_volume_growth": "+62%", + "interactions_before_purchase": "3-4", + "repeat_refusal_probability": "54%" + }, + "search_query_types": { + "direct_interest_catalog": "52%", + "question_based_articles": "28%", + "competitors_reviews": "13%", + "problem_solving_hot_promo": "7%" + }, + "decision_factors": { + "promo_discount": "43%", + "review_recommendation": "36%", + "warranty_quality": "33%", + "convenience_service": "25%", + "availability": "21%", + "price": "19%" + }, + "reasons_for_refusal": { + "high_price": "37%", + "no_reviews_social_proof": "34%", + "choice_difficulty_unclear_offer": "29%" + }, + "conclusion": "Основной трафик приходит из поиска. Для отклика ключевую роль играют акции и отзывы." + }, + "customer_journey": { + "title": "Путь клиента", + "funnel_metrics": { + "ad_traffic": "Реклама", + "site_visitors": "Сайт / Лендинг", + "leads": 5600, + "bookings": 1200, + "payments": 680 + }, + "conversion_rates": { + "lead_to_booking": "21%", + "booking_to_payment": "57%", + "lead_to_payment_total": "12%" + }, + "funnel_bottlenecks": { + "lead_to_booking": "Медленная скорость ответа", + "booking_to_payment": "Недостаточно касаний / напоминаний" + }, + "conclusion": "Основные потери происходят на этапе лид -> запись (конверсия 21%) из-за медленных ответов менеджеров." + }, + "acquisition_channels": { + "title": "Каналы привлечения", + "metrics": { + "search_traffic_share": "48%", + "average_lead_cost": "37 P", + "growth_potential": "+21%" + }, + "channel_shares": { + "seo": "48%", + "yandex_direct": "31%", + "retargeting": "13%", + "vkontakte": "6%" + }, + "channel_efficiency": { + "seo": { "cost": "25 P", "status": "Эффективен" }, + "yandex": { "cost": "45 P", "status": "Эффективен" }, + "retargeting": { "cost": "40 P", "status": "Удовлетворительно" }, + "vkontakte": { "cost": "57 P", "status": "Завышено" }, + "youtube": { "cost": "63 P", "status": "Завышено" }, + "instagram": { "cost": "77 P", "status": "Завышено" } + }, + "conclusion": "Основной источник лидов — SEO. VK, Youtube и Instagram требуют оптимизации из-за завышенной стоимости лида." + }, + "marketing_economics": { + "title": "Экономика маркетинга", + "metrics": { + "customer_acquisition_cost_cac": "2900 P", + "repeat_sales_conversion": "32%", + "roi": "3.5x" + }, + "financial_performance": { + "marketing_expenses": { "month": "1.97 млн", "year": "23.6 млн" }, + "clients": { "month": 680, "year": 8160 }, + "cac": { "month": "2900 P", "year": "2900 P" }, + "revenue_from_client": { "month": "10.200 P", "year": "23.040 P" }, + "profit_from_client": { "month": "7.300 P", "year": "16.720 P" } + }, + "conclusion": "Экономика digital-маркетинга эффективна: возврат инвестиций 3.5х. Точки роста — повторные продажи и удержание." + }, + "bottlenecks": { + "title": "Проблемы и узкие места", + "statistics": { + "landing_page_issues": "51%", + "slow_manager_responses": "60%", + "repeat_interaction_refusal": "36%" + }, + "details": { + "landing": ["Низкая конверсия (2%)", "Проблемы с формой записи", "Уход посетителей"], + "lead_management": ["Медленные ответы (30-40+ мин)", "Недостаток напоминаний", "Пропущенные лиды"], + "retention": ["Недостаток повторных предложений", "Нет прогрева", "Слабая работа с CRM"] + }, + "conclusion": "Теряются десятки процентов лидов из-за медленных ответов менеджеров и слабой ретаргетинговой работы." + }, + "recommendations": { + "title": "Рекомендации (итоги)", + "core_pillars": [ + { + "strategy": "Фокус на эффективных каналах", + "action": "Перераспределение бюджета на каналы с устойчивым спросом (Яндекс/SEO)" + }, + { + "strategy": "Увеличение повторных касаний", + "action": "Усиление удерживающих кампаний и серий касаний с клиентами" + }, + { + "strategy": "Приоритет высокоценных аудиторий", + "action": "Смещение акцента на платёжеспособные сегменты и B2B" + }, + { + "strategy": "Рост эффективности трафика", + "action": "Снижение зависимости от дорогих источников и оптимизация каналов" + } + ], + "conclusion": "Рекомендации фокусируют усилия на эффективности интернет-продвижения и служат базой для следующих шагов." + } + } +} diff --git a/docs/FRONTEND_API_CHANGES_V2.md b/docs/FRONTEND_API_CHANGES_V2.md new file mode 100644 index 0000000..115bf7c --- /dev/null +++ b/docs/FRONTEND_API_CHANGES_V2.md @@ -0,0 +1,193 @@ +# Изменения в API: Интеграция v2 анализа + +## Обзор изменений + +Добавлена возможность опционального создания и сохранения v2 анализа вместе с обычным анализом. Все v2 анализы сохраняются в MongoDB и доступны через новые эндпойнты. + +--- + +## 1. POST `/api/marketing/analysis/start` - Обновлен + +### Новый query параметр: + +- **`generateV2`** (boolean, опционально, по умолчанию `false`) + - Если `true`, вместе с обычным анализом создается и сохраняется v2 анализ + - Оба анализа автоматически связываются между собой + +### Изменения в ответе: + +**Если `generateV2=false` (по умолчанию):** + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "string", + "status": "processing", + "estimatedCompletionTime": "2024-01-01T12:00:00", + "message": "Комплексный анализ (...) запущен успешно..." + } +} +``` + +**Если `generateV2=true`:** + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "string", + "status": "processing", + "estimatedCompletionTime": "2024-01-01T12:00:00", + "message": "Комплексный анализ (...) запущен успешно...", + "v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ + } +} +``` + +### Пример использования: + +```javascript +// Создание обычного анализа +POST /api/marketing/analysis/start +Body: { ... } + +// Создание обычного анализа + v2 анализа +POST /api/marketing/analysis/start?generateV2=true +Body: { ... } +``` + +--- + +## 2. GET `/api/marketing/analysis/my` - Обновлен + +### Новое поле в ответе: + +Каждый элемент в списке теперь содержит опциональное поле `v2AnalysisId`: + +```json +{ + "success": true, + "data": [ + { + "analysisId": "string", + "businessNiche": "string", + "product": "string", + // ... другие поля ... + "v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ (может быть null) + } + ] +} +``` + +**Примечание:** Поле `v2AnalysisId` будет присутствовать только если для данного анализа был создан связанный v2 анализ. + +--- + +## 3. GET `/api/marketing/analysis/{id}` - Обновлен + +### Новое поле в ответе: + +Эндпойнт теперь возвращает опциональное поле `v2AnalysisId`: + +```json +{ + "success": true, + "data": { + "analysisId": "string", + "status": "string", + "createdAt": "2024-01-01T12:00:00", + "completedAt": "2024-01-01T12:00:00", + "v2AnalysisId": "string", // ← НОВОЕ ПОЛЕ (может быть null) + "report": { ... } + } +} +``` + +**Примечание:** Поле `v2AnalysisId` будет присутствовать только если для данного анализа был создан связанный v2 анализ. + +--- + +## 4. GET `/api/marketing/analysis/v2/{id}` - Новый эндпойнт + +### Описание: + +Получение сохраненного v2 анализа по ID. + +### Параметры: + +- **Path:** `id` (string) - ID v2 анализа +- **Header:** `Authorization` (JWT токен) + +### Ответ: + +**Успешный ответ (200):** + +```json +{ + "success": true, + "data": { + "reportTitle": "string", + "sections": { + "marketOverview": { ... }, + "targetAudience": { ... }, + "competitiveEnvironment": { ... }, + // ... другие секции ... + } + } +} +``` + +**Ошибки:** + +- `401` - Не авторизован +- `404` - Анализ не найден +- `403` - Нет доступа к этому анализу + +### Пример использования: + +```javascript +GET /api/marketing/analysis/v2/507f1f77bcf86cd799439011 +Authorization: Bearer +``` + +--- + +## 5. POST `/api/marketing/analysis/start/v2` - Без изменений + +Эндпойнт работает как прежде - создает только v2 анализ без связи с обычным анализом. + +--- + +## Рекомендации для фронтенда + +1. **При создании анализа:** + + - Используйте `generateV2=true` если нужен v2 анализ сразу + - Сохраняйте `v2AnalysisId` из ответа для последующего доступа + +2. **При отображении списка анализов:** + + - Проверяйте наличие поля `v2AnalysisId` + - Если поле присутствует, можно показать кнопку/ссылку для просмотра v2 анализа + +3. **При получении конкретного анализа:** + + - Поле `v2AnalysisId` доступно в ответе `GET /api/marketing/analysis/{id}` + - Используйте это поле для навигации к v2 анализу + +4. **При получении v2 анализа:** + - Используйте `GET /api/marketing/analysis/v2/{id}` для получения полных данных + - Обрабатывайте случаи, когда v2 анализ может быть не найден (404) + +--- + +## Обратная совместимость + +✅ Все изменения обратно совместимы: + +- Существующие запросы без `generateV2` работают как прежде +- Поле `v2AnalysisId` опционально и не ломает существующий код +- Новые эндпойнты не влияют на старую функциональность diff --git a/src/components/marketing/PptConclusionBox.vue b/src/components/marketing/PptConclusionBox.vue new file mode 100644 index 0000000..326464c --- /dev/null +++ b/src/components/marketing/PptConclusionBox.vue @@ -0,0 +1,22 @@ + + + + diff --git a/src/components/marketing/PptFunnel.vue b/src/components/marketing/PptFunnel.vue new file mode 100644 index 0000000..99942f9 --- /dev/null +++ b/src/components/marketing/PptFunnel.vue @@ -0,0 +1,100 @@ + + + + diff --git a/src/components/marketing/PptKpiCard.vue b/src/components/marketing/PptKpiCard.vue new file mode 100644 index 0000000..7e9e201 --- /dev/null +++ b/src/components/marketing/PptKpiCard.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/config/api.js b/src/config/api.js index ef1738d..6e31280 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -52,6 +52,8 @@ export const API_CONFIG = { // MarketingAnalysisController - Маркетинговый анализ MARKETING_ANALYSIS_START: '/api/marketing/analysis/start', + MARKETING_ANALYSIS_START_V2: '/api/marketing/analysis/start/v2', + MARKETING_ANALYSIS_V2_GET: '/api/marketing/analysis/v2', MARKETING_ANALYSIS_GET: '/api/marketing/analysis', MARKETING_ANALYSIS_DOWNLOAD: '/api/marketing/analysis', MARKETING_ANALYSIS_MY: '/api/marketing/analysis/my', diff --git a/src/router/index.js b/src/router/index.js index 7676f58..0e8b3c1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -235,6 +235,12 @@ const router = createRouter({ component: () => import('@/views/pages/marketing/MarketingAnalysis.vue'), meta: { requiresAuth: true } }, + { + path: '/marketing-analysis/dashboard', + name: 'marketing-analysis-dashboard', + redirect: { name: 'marketing-analysis' }, + meta: { requiresAuth: true } + }, { path: '/marketing-analysis/promotion', name: 'marketing-promotion', diff --git a/src/service/MarketingService.js b/src/service/MarketingService.js index 714c78c..2609e0b 100644 --- a/src/service/MarketingService.js +++ b/src/service/MarketingService.js @@ -22,8 +22,9 @@ class MarketingService { * @param {string} [data.weakSide] - Слабая сторона бизнеса (опционально) * @returns {Promise} Объект с analysisId и статусом (или массив объектов при множественном выборе типов анализа) */ - async startAnalysis(data) { + async startAnalysis(data, options = {}) { try { + const generateV2 = options?.generateV2 === true; const requestBody = { businessNiche: data.businessNiche, product: data.product, @@ -42,7 +43,8 @@ class MarketingService { requestBody.weakSide = data.weakSide; } - const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_START}`, { + const url = `${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_START}${generateV2 ? '?generateV2=true' : ''}`; + const response = await AuthService.authFetch(url, { method: 'POST', ...DEFAULT_REQUEST_CONFIG, body: JSON.stringify(requestBody) @@ -66,6 +68,78 @@ class MarketingService { } } + /** + * Получение сохраненного v2 анализа по ID + * GET /api/marketing/analysis/v2/{id} + * @param {string} v2AnalysisId + * @returns {Promise} MarketingAnalysisResponseV2 data payload + */ + async getAnalysisV2ById(v2AnalysisId) { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_V2_GET}/${v2AnalysisId}`); + + const result = await response.json().catch(() => null); + + if (!response.ok) { + throw new Error(result?.message || result?.error?.message || 'Ошибка при получении v2 анализа'); + } + + if (!result || !result.success) { + throw new Error(result?.message || 'Ошибка при получении v2 анализа'); + } + + return result.data || null; + } catch (error) { + console.error('Ошибка при получении v2 анализа:', error); + throw error; + } + } + + /** + * Запуск маркетингового анализа v2 (синхронный ответ с полным отчётом) + * POST /api/marketing/analysis/start/v2 + * @param {Object} data - MarketingAnalysisRequest + * @returns {Promise} MarketingAnalysisResponseV2 (сырой JSON без обёртки {success,data}) + */ + async fetchAnalysisV2(data) { + try { + const requestBody = { + businessNiche: data.businessNiche, + product: data.product, + targetAudience: data.targetAudience, + region: data.region, + goal: data.goal, + detailLevel: data.detailLevel, + analysisType: data.analysisType + }; + + if (data.strongSide) requestBody.strongSide = data.strongSide; + if (data.weakSide) requestBody.weakSide = data.weakSide; + + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_START_V2}`, { + method: 'POST', + ...DEFAULT_REQUEST_CONFIG, + body: JSON.stringify(requestBody) + }); + + const result = await response.json().catch(() => null); + + if (!response.ok) { + throw new Error(result?.message || result?.error?.message || `HTTP ${response.status}: ${response.statusText}`); + } + + // v2: ожидаем сырой отчёт, без result.success/result.data + if (!result) { + throw new Error('Пустой ответ от сервера'); + } + + return result; + } catch (error) { + console.error('Ошибка при запуске маркетингового анализа v2:', error); + throw error; + } + } + /** * Получение результата анализа * GET /api/marketing/analysis/{analysisId} @@ -598,6 +672,48 @@ class MarketingService { throw error; } } + + /** + * Регенерация изображения для поста + * POST /api/marketing/analysis/strategy/{strategyId}/post/{postIndex}/regenerate-image + * @param {string} strategyId - ID стратегии + * @param {number} postIndex - Индекс поста в календаре (0-based) + * @returns {Promise} Объект с информацией о регенерации изображения + */ + async regenerateImage(strategyId, postIndex) { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}/api/marketing/analysis/strategy/${strategyId}/post/${postIndex}/regenerate-image`, { + 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; + } + } } export default new MarketingService(); diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue new file mode 100644 index 0000000..20d60b4 --- /dev/null +++ b/src/views/pages/MarketingAnalysis.vue @@ -0,0 +1,1249 @@ +