From 1fdb244fd4eafc1e688e173d879556ea1589974d Mon Sep 17 00:00:00 2001 From: Fedor Date: Fri, 5 Dec 2025 12:39:08 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20clipboard=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=20ticket=5Fnumber=20=D0=B2=20webhook=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлена ошибка 'Cannot read properties of undefined (reading writeText)' в Step1Phone, StepClaimConfirmation, Step3Payment - Добавлена проверка на существование navigator.clipboard - Добавлен fallback для старых браузеров (document.execCommand) - Добавлена обработка ошибок с try/catch - Добавлена передача ticket_number и ticket_id в webhook поддержки - При обработке события out_of_scope сохраняются claim_id, ticket_number, ticket_id из payload - Эти поля теперь передаются в webhook при отправке в поддержку - Обновлён n8n код для обработки ошибок и out_of_scope - Добавлено явное сохранение ticket_number в data объекта события - ticket_number теперь гарантированно попадает в событие для Redis --- ...N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js | 156 ++++++ .../src/components/form/Step1Phone.tsx | 216 +++++--- .../src/components/form/Step3Payment.tsx | 41 +- .../components/form/StepClaimConfirmation.tsx | 110 +++- .../src/components/form/StepWizardPlan.tsx | 517 ++++++++++++++++-- 5 files changed, 888 insertions(+), 152 deletions(-) create mode 100644 ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js diff --git a/ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js b/ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js new file mode 100644 index 00000000..b78ec6d4 --- /dev/null +++ b/ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js @@ -0,0 +1,156 @@ +// Code для n8n-nodes-base.code (JS), Mode = Run Once for All Items +// Обработка ошибок и случаев "не нашей тематики" для веб-формы + +// Берём все входные элементы +const items = $input.all(); + +// Предполагаем, что нас интересует первый элемент массива +const data = items[0].json; + +// Извлекаем данные +const consumerRights = data.consumer_rights; // true/false +const sessionToken = data.session_token || data.session_id; +const reason = data.reason || ''; +const ticket = data.ticket || ''; +const answerText = data.answer_text || ''; +const ticketNumber = data.ticket_number || data.ticketnomber || null; // ✅ Номер заявки (HD001234) + +// Формируем timestamp +const timestamp = new Date().toISOString(); + +// Определяем тип события и сообщение +let eventType; +let status; +let message; +let errorDetails = null; +let suggestedActions = []; + +if (consumerRights === false) { + // ⚠️ СЛУЧАЙ: Вопрос не нашей тематики + eventType = 'out_of_scope'; + status = 'out_of_scope'; + + message = reason || + 'К сожалению, ваш вопрос не относится к нашей компетенции. Мы помогаем с защитой прав потребителей в сфере услуг, товаров и туризма.'; + + // Рекомендуемые действия + suggestedActions = [ + { + title: 'Обратиться в другую организацию', + description: reason || 'Для вашего вопроса лучше обратиться в соответствующую организацию', + url: 'https://akn16.ru', + urlText: 'Вы можете обратиться к нашим партнёрам', + actionType: 'external_link' // Для внешней ссылки + }, + { + title: 'Связаться с поддержкой', + description: 'Если вы считаете, что это ошибка, свяжитесь с нами', + actionType: 'contact_support', // Для модалки с отправкой в поддержку + url: null + } + ]; + + console.log('⚠️ Вопрос не нашей тематики:', { + ticket, + reason, + session_token: sessionToken + }); + +} else { + // ❌ СЛУЧАЙ: Ошибка обработки (но вопрос нашей тематики) + eventType = 'documents_list_error'; + status = 'error'; + + message = 'Не удалось обработать ваш запрос. Попробуйте позже или обратитесь в поддержку.'; + + errorDetails = { + code: 'PROCESSING_FAILED', + reason: reason || 'Ошибка при обработке запроса', + ticket: ticket, + answer_text: answerText, + timestamp: timestamp + }; + + console.log('❌ Ошибка обработки:', { + ticket, + reason, + session_token: sessionToken + }); +} + +// Формируем событие для публикации в Redis +const event = { + event_type: eventType, + status: status, + session_id: sessionToken, + message: message, + timestamp: timestamp +}; + +// Добавляем специфичные поля в зависимости от типа события +if (eventType === 'out_of_scope') { + event.suggested_actions = suggestedActions; + event.reason = reason; + event.ticket = ticket; +} else if (eventType === 'documents_list_error') { + event.error_message = message; + event.error_details = errorDetails; +} + +// Если есть claim_id, добавляем его +if (data.claim_id) { + event.claim_id = data.claim_id; +} + +// Если есть ticket_number, добавляем его +if (ticketNumber) { + event.ticket_number = ticketNumber; +} + +// Формируем канал Redis +const channel = `ocr_events:${sessionToken}`; + +// Возвращаем данные для HTTP Request или Redis Publish node +return [ + { + json: { + // Для HTTP Request к /api/v1/events/{session_token} + event_type: event.event_type, + status: event.status, + message: event.message, + data: { + ...event, + // ✅ ticket_number должен быть в data (если был в event) + ticket_number: event.ticket_number || ticketNumber || null, + // Убираем дублирующиеся поля из data + event_type: undefined, + status: undefined, + message: undefined + }, + timestamp: event.timestamp, + + // Для Redis Publish (альтернативный вариант) + channel: channel, + redis_message: JSON.stringify(event), + + // Передаём дальше для следующих нод + session_token: sessionToken, + claim_id: data.claim_id, + ticket_number: ticketNumber, // ✅ Номер заявки (HD001234) + user_id: data.user_id, + contactid: data.contactid, + mobile: data.mobile, + ticket: ticket, + reason: reason, + consumer_rights: consumerRights, + + // Для отладки + _debug: { + event_type: eventType, + status: status, + channel: channel + } + } + } +]; + diff --git a/ticket_form/frontend/src/components/form/Step1Phone.tsx b/ticket_form/frontend/src/components/form/Step1Phone.tsx index bfdc0f28..b10599b7 100644 --- a/ticket_form/frontend/src/components/form/Step1Phone.tsx +++ b/ticket_form/frontend/src/components/form/Step1Phone.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; -import { Form, Input, Button, message, Space } from 'antd'; -import { PhoneOutlined, SafetyOutlined } from '@ant-design/icons'; +import { Form, Input, Button, message, Space, Modal } from 'antd'; +import { PhoneOutlined, SafetyOutlined, CopyOutlined } from '@ant-design/icons'; +import { debugLog } from '../../utils/debugLog'; interface Props { formData: any; @@ -18,11 +19,13 @@ export default function Step1Phone({ addDebugEvent }: Props) { // 🆕 VERSION CHECK: 2025-11-20 12:40 - session_id fix - console.log('📱 Step1Phone v2.0 - 2025-11-20 14:40 - Session creation with debug logs'); + debugLog.log('📱 Step1Phone v2.0 - 2025-11-20 14:40 - Session creation with debug logs'); const [form] = Form.useForm(); const [codeSent, setCodeSent] = useState(false); const [loading, setLoading] = useState(false); const [verifyLoading, setVerifyLoading] = useState(false); + const [debugCode, setDebugCode] = useState(null); + const [debugModalVisible, setDebugModalVisible] = useState(false); const sendCode = async () => { try { @@ -41,17 +44,24 @@ export default function Step1Phone({ const result = await response.json(); if (response.ok) { - addDebugEvent?.('sms', 'success', `✅ SMS отправлен (DEBUG mode)`, { + addDebugEvent?.('sms', 'success', `✅ SMS отправлен`, { phone, debug_code: result.debug_code, - message: result.message + message: result.message, + is_dev_mode: !!result.debug_code }); - message.success('Код отправлен на ваш телефон'); + + // ✅ Если есть debug_code - показываем модалку (dev режим на бэкенде) + if (result.debug_code) { + setDebugCode(result.debug_code); + setDebugModalVisible(true); + } else { + // В проде - обычное сообщение (SMS отправлена реально) + message.success('Код отправлен на ваш телефон'); + } + setCodeSent(true); updateFormData({ phone }); - if (result.debug_code) { - message.info(`DEBUG: Код ${result.debug_code}`); - } } else { addDebugEvent?.('sms', 'error', `❌ Ошибка SMS: ${result.detail}`, { error: result.detail }); message.error(result.detail || 'Ошибка отправки кода'); @@ -110,17 +120,17 @@ export default function Step1Phone({ crmResult = crmResult[0]; } - console.log('🔥 N8N CRM Response (after array check):', crmResult); - console.log('🔥 N8N CRM Response FULL:', JSON.stringify(crmResult, null, 2)); + debugLog.log('🔥 N8N CRM Response (after array check):', crmResult); + debugLog.log('🔥 N8N CRM Response FULL:', JSON.stringify(crmResult, null, 2)); if (crmResponse.ok && crmResult.success) { // n8n возвращает: {success: true, result: {claim_id, contact_id, ...}} const result = crmResult.result || crmResult; - console.log('🔥 Extracted result:', result); - console.log('🔥 result.unified_id:', result.unified_id); - console.log('🔥 typeof result.unified_id:', typeof result.unified_id); - console.log('🔥 result keys:', Object.keys(result)); + debugLog.log('🔥 Extracted result:', result); + debugLog.log('🔥 result.unified_id:', result.unified_id); + debugLog.log('🔥 typeof result.unified_id:', typeof result.unified_id); + debugLog.log('🔥 result keys:', Object.keys(result)); // ✅ ВАЖНО: Проверяем наличие unified_id if (!result.unified_id) { @@ -129,25 +139,25 @@ export default function Step1Phone({ console.error('❌ Полный ответ crmResult:', crmResult); message.warning('⚠️ unified_id не получен от n8n, черновики могут не отображаться'); } else { - console.log('✅ unified_id получен:', result.unified_id); + debugLog.log('✅ unified_id получен:', result.unified_id); } // ✅ Извлекаем session_id от n8n (если есть) const session_id_from_n8n = result.session; - console.log('🔍 Проверка session_id от n8n:'); - console.log('🔍 result.session:', result.session); - console.log('🔍 session_id_from_n8n:', session_id_from_n8n); - console.log('🔍 formData.session_id (текущий):', formData.session_id); + debugLog.log('🔍 Проверка session_id от n8n:'); + debugLog.log('🔍 result.session:', result.session); + debugLog.log('🔍 session_id_from_n8n:', session_id_from_n8n); + debugLog.log('🔍 formData.session_id (текущий):', formData.session_id); if (session_id_from_n8n) { - console.log('✅ session_id получен от n8n:', session_id_from_n8n); + debugLog.log('✅ session_id получен от n8n:', session_id_from_n8n); } else { - console.warn('⚠️ session_id не найден в ответе n8n, используем текущий:', formData.session_id); + debugLog.warn('⚠️ session_id не найден в ответе n8n, используем текущий:', formData.session_id); } const finalSessionId = session_id_from_n8n || formData.session_id; - console.log('🔍 finalSessionId (будет сохранён):', finalSessionId); + debugLog.log('🔍 finalSessionId (будет сохранён):', finalSessionId); const dataToSave = { phone, @@ -159,11 +169,11 @@ export default function Step1Phone({ is_new_contact: result.is_new_contact }; - console.log('🔥 ========== SAVING TO FORMDATA =========='); - console.log('🔥 Saving to formData:', JSON.stringify(dataToSave, null, 2)); - console.log('🔥 dataToSave.unified_id:', dataToSave.unified_id); - console.log('🔥 dataToSave.session_id:', dataToSave.session_id); - console.log('🔥 ========================================='); + debugLog.log('🔥 ========== SAVING TO FORMDATA =========='); + debugLog.log('🔥 Saving to formData:', JSON.stringify(dataToSave, null, 2)); + debugLog.log('🔥 dataToSave.unified_id:', dataToSave.unified_id); + debugLog.log('🔥 dataToSave.session_id:', dataToSave.session_id); + debugLog.log('🔥 ========================================='); addDebugEvent?.('crm', 'success', `✅ Контакт создан/найден в CRM`, result); @@ -177,7 +187,7 @@ export default function Step1Phone({ // 🔑 Создаём сессию в Redis для живучести (24 часа) try { - console.log('🔑 Создаём сессию в Redis:', { + debugLog.log('🔑 Создаём сессию в Redis:', { session_token: finalSessionId, unified_id: result.unified_id, phone: phone, @@ -196,20 +206,20 @@ export default function Step1Phone({ }) }); - console.log('🔑 Session create response status:', sessionResponse.status); + debugLog.log('🔑 Session create response status:', sessionResponse.status); if (sessionResponse.ok) { const sessionData = await sessionResponse.json(); - console.log('🔑 Session create response data:', sessionData); + debugLog.log('🔑 Session create response data:', sessionData); // Сохраняем session_token в localStorage для последующих визитов localStorage.setItem('session_token', finalSessionId); - console.log('✅ Сессия создана в Redis, session_token сохранён в localStorage:', finalSessionId); - console.log('✅ Проверка: localStorage.getItem("session_token"):', localStorage.getItem('session_token')); + debugLog.log('✅ Сессия создана в Redis, session_token сохранён в localStorage:', finalSessionId); + debugLog.log('✅ Проверка: localStorage.getItem("session_token"):', localStorage.getItem('session_token')); addDebugEvent?.('session', 'success', '✅ Сессия создана (TTL 24h)'); } else { const errorText = await sessionResponse.text(); - console.warn('⚠️ Не удалось создать сессию в Redis:', sessionResponse.status, errorText); + debugLog.warn('⚠️ Не удалось создать сессию в Redis:', sessionResponse.status, errorText); } } catch (sessionError) { console.error('❌ Ошибка создания сессии:', sessionError); @@ -219,11 +229,11 @@ export default function Step1Phone({ // ✅ Передаем unified_id напрямую в onNext для проверки черновиков // Это нужно, потому что formData может еще не обновиться const unifiedIdToPass = result.unified_id; - console.log('🔥 ============================================'); - console.log('🔥 Передаём unified_id в onNext:', unifiedIdToPass); - console.log('🔥 typeof unifiedIdToPass:', typeof unifiedIdToPass); - console.log('🔥 Вызываем onNext с unified_id:', unifiedIdToPass); - console.log('🔥 ============================================'); + debugLog.log('🔥 ============================================'); + debugLog.log('🔥 Передаём unified_id в onNext:', unifiedIdToPass); + debugLog.log('🔥 typeof unifiedIdToPass:', typeof unifiedIdToPass); + debugLog.log('🔥 Вызываем onNext с unified_id:', unifiedIdToPass); + debugLog.log('🔥 ============================================'); onNext(unifiedIdToPass); } else { addDebugEvent?.('crm', 'error', '❌ Ошибка создания контакта в CRM', crmResult); @@ -336,38 +346,106 @@ export default function Step1Phone({ )} - {/* 🔧 Технические кнопки для разработки */} -
-
- 🔧 DEV MODE - Быстрая навигация (без валидации) -
-
- , + + ]} + width={400} + > +
+

+ В режиме разработки SMS не отправляется реально. + Используйте этот код для верификации: +

+
+ {debugCode} +
+

+ Код действителен 10 минут +

-
+ + + {/* 🔧 Технические кнопки для разработки (только в dev режиме) */} + {import.meta.env.DEV && ( +
+
+ 🔧 DEV MODE - Быстрая навигация (без валидации) +
+
+ +
+
+ )} ); } diff --git a/ticket_form/frontend/src/components/form/Step3Payment.tsx b/ticket_form/frontend/src/components/form/Step3Payment.tsx index c125bd5d..23789941 100644 --- a/ticket_form/frontend/src/components/form/Step3Payment.tsx +++ b/ticket_form/frontend/src/components/form/Step3Payment.tsx @@ -51,7 +51,13 @@ export default function Step3Payment({ throw new Error(`HTTP ${response.status}`); } - const banksData: Bank[] = await response.json(); + const banksDataRaw: any[] = await response.json(); + + // ✅ Нормализуем формат данных (API возвращает bankId/bankName, а код ожидает bankid/bankname) + const banksData: Bank[] = banksDataRaw.map((bank: any) => ({ + bankid: bank.bankId || bank.bankid, + bankname: bank.bankName || bank.bankname + })); // Сортируем по названию для удобства banksData.sort((a, b) => a.bankname.localeCompare(b.bankname, 'ru')); @@ -133,7 +139,9 @@ export default function Step3Payment({ if (result.debug_code) { setDebugCode(result.debug_code); updateFormData({ smsDebugCode: result.debug_code }); - message.info(`DEBUG: Код ${result.debug_code}`); + if (import.meta.env.DEV) { + message.info(`DEBUG: Код ${result.debug_code}`); + } } } else { addDebugEvent?.('sms', 'error', `❌ Ошибка SMS: ${result.detail}`, { error: result.detail }); @@ -329,7 +337,7 @@ export default function Step3Payment({ )} - {debugCode && !isPhoneVerified && ( + {import.meta.env.DEV && debugCode && !isPhoneVerified && (
} size="small" - onClick={() => { - navigator.clipboard.writeText(debugCode); - message.success('Код скопирован'); + onClick={async () => { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(debugCode); + message.success('Код скопирован'); + } else { + const textArea = document.createElement('textarea'); + textArea.value = debugCode; + textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + message.success('Код скопирован'); + } catch (err) { + message.error('Не удалось скопировать код'); + } + document.body.removeChild(textArea); + } + } catch (err) { + message.error('Не удалось скопировать код'); + } }} > Скопировать @@ -427,7 +455,6 @@ export default function Step3Payment({ ({ value: bank.bankname, diff --git a/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx b/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx index 428f8cf1..b62da30d 100644 --- a/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx +++ b/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx @@ -1,12 +1,14 @@ import { useEffect, useRef, useState, useCallback } from 'react'; +import { debugLog } from '../../utils/debugLog'; import { Card, Spin, message, Modal, Input, Button, Form } from 'antd'; +import { CopyOutlined } from '@ant-design/icons'; import { generateConfirmationFormHTML } from './generateConfirmationFormHTML'; interface Props { claimPlanData: any; // Данные заявления от n8n contact_data_confirmed?: boolean; // ✅ Флаг подтверждения данных контакта onNext: () => void; - onPrev: () => void; + onPrev?: () => void; // Опциональный, так как не всегда используется onSubmitted?: () => void; // ✅ Callback после успешной отправки } @@ -27,6 +29,7 @@ export default function StepClaimConfirmation({ const [smsLoading, setSmsLoading] = useState(false); const [smsVerifyLoading, setSmsVerifyLoading] = useState(false); const [pendingFormData, setPendingFormData] = useState(null); + const [debugCode, setDebugCode] = useState(null); // ✅ Дебажный код для SMS const [smsForm] = Form.useForm(); useEffect(() => { @@ -35,18 +38,18 @@ export default function StepClaimConfirmation({ return; } - console.log('📋 StepClaimConfirmation: получены данные claimPlanData:', claimPlanData); - console.log('📋 claimPlanData.claim_id:', claimPlanData?.claim_id); - console.log('📋 claimPlanData.unified_id:', claimPlanData?.unified_id); - console.log('📋 claimPlanData.propertyName?.meta?.claim_id:', claimPlanData?.propertyName?.meta?.claim_id); - console.log('📋 claimPlanData.propertyName?.meta?.unified_id:', claimPlanData?.propertyName?.meta?.unified_id); + debugLog.log('📋 StepClaimConfirmation: получены данные claimPlanData:', claimPlanData); + debugLog.log('📋 claimPlanData.claim_id:', claimPlanData?.claim_id); + debugLog.log('📋 claimPlanData.unified_id:', claimPlanData?.unified_id); + debugLog.log('📋 claimPlanData.propertyName?.meta?.claim_id:', claimPlanData?.propertyName?.meta?.claim_id); + debugLog.log('📋 claimPlanData.propertyName?.meta?.unified_id:', claimPlanData?.propertyName?.meta?.unified_id); // Формируем данные для формы подтверждения // Формат должен соответствовать тому, что ожидает HTML форма const claimId = claimPlanData?.claim_id || claimPlanData?.propertyName?.meta?.claim_id || ''; const unifiedId = claimPlanData?.unified_id || claimPlanData?.propertyName?.meta?.unified_id || ''; - console.log('📋 Извлечённые ID:', { claimId, unifiedId }); + debugLog.log('📋 Извлечённые ID:', { claimId, unifiedId }); // Преобразуем данные из propertyName в формат для формы const applicant = claimPlanData?.propertyName?.applicant || {}; @@ -86,9 +89,9 @@ export default function StepClaimConfirmation({ }, }; - console.log('📋 Сформированные formData:', formData); - console.log('📋 formData.propertyName:', formData.propertyName); - console.log('📋 formData.propertyName?.meta:', formData.propertyName?.meta); + debugLog.log('📋 Сформированные formData:', formData); + debugLog.log('📋 formData.propertyName:', formData.propertyName); + debugLog.log('📋 formData.propertyName?.meta:', formData.propertyName?.meta); // ✅ Получаем флаги подтверждения данных из props, claimPlanData или formData const contact_data_confirmed = @@ -97,6 +100,12 @@ export default function StepClaimConfirmation({ claimPlanData?.propertyName?.meta?.contact_data_confirmed || false; + // ✅ Логируем для отладки + debugLog.log('🔒 StepClaimConfirmation: contact_data_confirmed =', contact_data_confirmed); + debugLog.log('🔒 prop_contact_data_confirmed =', prop_contact_data_confirmed); + debugLog.log('🔒 claimPlanData?.contact_data_confirmed =', claimPlanData?.contact_data_confirmed); + debugLog.log('🔒 claimPlanData?.propertyName?.meta?.contact_data_confirmed =', claimPlanData?.propertyName?.meta?.contact_data_confirmed); + // Генерируем HTML форму здесь, на нашей стороне const html = generateConfirmationFormHTML(formData, contact_data_confirmed); setHtmlContent(html); @@ -106,8 +115,8 @@ export default function StepClaimConfirmation({ // Функция сохранения данных формы - публикация в Redis канал // ⚠️ ВАЖНО: Эта функция должна вызываться ТОЛЬКО после SMS-верификации! const saveFormData = useCallback(async (formData: any, smsCode?: string) => { - console.log('💾 Публикуем данные формы в Redis канал:', formData); - console.log('📱 SMS код для публикации:', smsCode || '(не передан)'); + debugLog.log('💾 Публикуем данные формы в Redis канал:', formData); + debugLog.log('📱 SMS код для публикации:', smsCode || '(не передан)'); // Защита: если SMS код не передан, это ошибка (данные не должны отправляться без верификации) if (!smsCode || smsCode.trim() === '') { @@ -165,7 +174,7 @@ export default function StepClaimConfirmation({ original_data: formData?.originalData || {}, }; - console.log('📦 Payload для Redis:', { ...payload, sms_code: smsCode ? '***' : '(пусто)' }); + debugLog.log('📦 Payload для Redis:', { ...payload, sms_code: smsCode ? '***' : '(пусто)' }); // Публикуем в Redis канал через backend endpoint (fire-and-forget) // Канал: clientright:webform:approve @@ -181,7 +190,7 @@ export default function StepClaimConfirmation({ console.error('Ошибка публикации данных формы в Redis:', error); }); - console.log('✅ Данные формы опубликованы в Redis канал clientright:webform:approve'); + debugLog.log('✅ Данные формы опубликованы в Redis канал clientright:webform:approve'); }, [claimPlanData]); // Функция отправки SMS-кода @@ -202,8 +211,12 @@ export default function StepClaimConfirmation({ if (response.ok) { message.success('Код отправлен на ваш телефон'); setSmsCodeSent(true); + // ✅ Сохраняем дебажный код для отображения if (result.debug_code) { - message.info(`DEBUG: Код ${result.debug_code}`); + setDebugCode(result.debug_code); + if (import.meta.env.DEV) { + message.info(`DEBUG: Код ${result.debug_code}`); + } } } else { message.error(result.detail || 'Ошибка отправки кода'); @@ -232,7 +245,10 @@ export default function StepClaimConfirmation({ if (response.ok) { message.success('Код подтвержден!'); - console.log('✅ SMS код успешно проверен:', code); + debugLog.log('✅ SMS код успешно проверен:', code); + + // Очищаем дебажный код + setDebugCode(null); // Закрываем модалку setSmsModalVisible(false); @@ -240,7 +256,7 @@ export default function StepClaimConfirmation({ smsForm.resetFields(); // Отправляем данные в Redis канал с SMS кодом - console.log('📤 Вызываем saveFormData с SMS кодом:', code); + debugLog.log('📤 Вызываем saveFormData с SMS кодом:', code); saveFormData(pendingFormData, code); // Показываем сообщение об успешной отправке @@ -266,10 +282,10 @@ export default function StepClaimConfirmation({ useEffect(() => { // Слушаем сообщения от iframe const handleMessage = (event: MessageEvent) => { - console.log('📨 Message from iframe:', event.data); + debugLog.log('📨 Message from iframe:', event.data); if (event.data.type === 'claim_confirmed') { - console.log('✅ Заявление подтверждено с данными:', event.data.data); + debugLog.log('✅ Заявление подтверждено с данными:', event.data.data); // Сохраняем данные формы для последующего сохранения после SMS-апрува setPendingFormData(event.data.data); @@ -296,7 +312,7 @@ export default function StepClaimConfirmation({ sendSMSCode(phone); } else if (event.data.type === 'claim_cancelled') { message.info('Подтверждение отменено'); - onPrev(); + if (onPrev) onPrev(); } else if (event.data.type === 'claim_form_loaded') { setLoading(false); // Автоматически подстраиваем высоту iframe после загрузки @@ -315,7 +331,7 @@ export default function StepClaimConfirmation({ iframe.style.height = Math.max(height + 50, 800) + 'px'; } } catch (e) { - console.warn('Не удалось автоматически подстроить высоту iframe:', e); + debugLog.warn('Не удалось автоматически подстроить высоту iframe:', e); } } } else if (event.data.type === 'iframe_resize') { @@ -485,6 +501,58 @@ export default function StepClaimConfirmation({ /> + {/* ✅ Дебажный код (только в DEV режиме) */} + {import.meta.env.DEV && debugCode && ( +
+ + DEBUG код: {debugCode} + + +
+ )} +
+ + +
+ )} + + {/* ⚠️ ЭКРАН "НЕ НАШЕЙ ТЕМАТИКИ": Показываем, если wizardPlanStatus === 'out_of_scope' */} + {formData.wizardPlanStatus === 'out_of_scope' && formData.outOfScopeMessage && ( +
+
⚠️
+ Вопрос не нашей компетенции + + {formData.outOfScopeMessage} + + + {formData.suggestedActions && formData.suggestedActions.length > 0 && ( +
+ + Рекомендуемые действия: + + + {formData.suggestedActions.map((action: any, index: number) => { + // Обработка кнопки "Связаться с поддержкой" + if (action.actionType === 'contact_support' || (!action.url && action.title?.toLowerCase().includes('поддержк'))) { + return ( + setSupportModalVisible(true)} + > + Отправить в поддержку → + + ]} + > + + + ); + } + + // Обработка внешней ссылки (партнёры) - если есть URL, показываем ссылку + if (action.url) { + const isPartnerLink = action.url.includes('akn16.ru') || action.actionType === 'external_link'; + const buttonText = isPartnerLink && action.urlText + ? action.urlText + : (action.urlText || 'Перейти →'); + + return ( + + {buttonText} + + ]} + > + +
{action.description}
+ {isPartnerLink && action.urlText && ( +
+ {action.urlText} +
+ )} +
+ } + /> + + ); + } + + // Обычная карточка без ссылки + return ( + + + + ); + })} + +
+ )} + + + + +
+ )} + + {/* СТАРЫЙ ФЛОУ: Ожидание визарда (только если нет ошибок) */} + {!hasNewFlowDocs && isWaiting && formData.wizardPlanStatus !== 'error' && formData.wizardPlanStatus !== 'out_of_scope' && (
)} - {/* СТАРЫЙ ФЛОУ: Визард готов */} - {!hasNewFlowDocs && !isWaiting && plan && ( + {/* ✅ ПЕРЕХОДНЫЙ СЛУЧАЙ: Если есть documents в plan, но нет в formData.documents_required */} + {!hasNewFlowDocs && !isWaiting && plan && plan.documents && plan.documents.length > 0 && ( +
+ + +
+ )} + + {/* СТАРЫЙ ФЛОУ: Визард готов (только если НЕТ documents в plan) */} + {!hasNewFlowDocs && !isWaiting && plan && (!plan.documents || plan.documents.length === 0) && (
<ThunderboltOutlined style={{ color: '#595959' }} /> План действий @@ -2482,6 +2781,114 @@ export default function StepWizardPlan({ </div> )} </Card> + + {/* Модалка для отправки в поддержку */} + <Modal + title="Отправка в поддержку" + open={supportModalVisible} + onCancel={() => setSupportModalVisible(false)} + footer={[ + <Button key="cancel" onClick={() => setSupportModalVisible(false)}> + Отмена + </Button>, + <Button + key="submit" + type="primary" + loading={sendingToSupport} + onClick={async () => { + setSendingToSupport(true); + try { + const endpoint = 'https://n8n.clientright.pro/webhook/3ef6ff67-f3f2-418e-a300-86cb4659dbde'; + + // Собираем максимум данных для отправки + const payload = { + // Основные идентификаторы + session_id: formData.session_id, + session_token: formData.session_id, + claim_id: formData.claim_id, + claim_number: formData.claim_id, // Номер заявки (дублируем для удобства) + ticket_number: formData.ticket_number, // ✅ Номер заявки из n8n (HD001234) + ticket_id: formData.ticket_id, // ✅ ID заявки в vTiger + user_id: formData.unified_id, + contact_id: formData.contact_id, + + // Контактные данные + phone: formData.phone, + email: formData.email, + + // Информация о проблеме + problem_description: formData.problemDescription || formData.description, + reason: formData.outOfScopeMessage, + ticket: formData.problemDescription || formData.description, + + // Статус и действия + wizard_plan_status: formData.wizardPlanStatus, + out_of_scope_message: formData.outOfScopeMessage, + suggested_actions: formData.suggestedActions, + + // Дополнительные данные формы + wizard_plan: formData.wizardPlan, + wizard_answers: formData.wizardAnswers, + documents_required: formData.documents_required, + documents_uploaded: formData.documents_uploaded, + documents_skipped: formData.documents_skipped, + + // Метаданные + channel: 'web_form', + timestamp: new Date().toISOString(), + user_agent: navigator.userAgent, + referrer: document.referrer, + + // Дополнительные поля из formData (если есть) + ...(formData.contact_data_from_crm ? { + contact_data_from_crm: formData.contact_data_from_crm + } : {}), + ...(formData.contact_data_confirmed ? { + contact_data_confirmed: formData.contact_data_confirmed + } : {}), + }; + + debugLog.log('📤 Отправка в поддержку:', payload); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + const result = await response.json().catch(() => ({})); + debugLog.log('✅ Ответ от поддержки:', result); + message.success('Ситуация направлена в поддержку для оценки'); + setSupportModalVisible(false); + + // Переходим к началу (список черновиков) + if (onGoToStart) { + setTimeout(() => { + onGoToStart(); + }, 500); // Небольшая задержка для показа сообщения + } + } else { + const errorText = await response.text().catch(() => 'Ошибка отправки'); + throw new Error(errorText); + } + } catch (error) { + debugLog.error('❌ Ошибка отправки в поддержку:', error); + message.error('Не удалось отправить запрос. Попробуйте позже.'); + } finally { + setSendingToSupport(false); + } + }} + > + Отправить + </Button>, + ]} + > + <p> + Ваша ситуация будет направлена в поддержку для оценки. + Мы проверим, относится ли ваш вопрос к нашей компетенции, и свяжемся с вами. + </p> + </Modal> </div> ); }