fix: исправлены ошибки clipboard и добавлена передача ticket_number в webhook поддержки
- Исправлена ошибка '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
This commit is contained in:
156
ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js
Normal file
156
ticket_form/docs/N8N_CODE_HANDLE_ERRORS_AND_OUT_OF_SCOPE.js
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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<string | null>(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({
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* 🔧 Технические кнопки для разработки */}
|
||||
<div style={{
|
||||
marginTop: 24,
|
||||
padding: 16,
|
||||
background: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
border: '2px dashed #999'
|
||||
}}>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
|
||||
🔧 DEV MODE - Быстрая навигация (без валидации)
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
// Автозаполняем телефон и email
|
||||
const devData = {
|
||||
phone: '79001234567', // БЕЗ +
|
||||
email: 'test@test.ru',
|
||||
};
|
||||
updateFormData(devData);
|
||||
setIsPhoneVerified(true);
|
||||
message.success('DEV: Телефон автоматически подтверждён');
|
||||
onNext();
|
||||
}}
|
||||
size="small"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Далее → (Step 2) [пропустить]
|
||||
{/* 🔧 DEV MODE: Модалка с SMS кодом */}
|
||||
<Modal
|
||||
title="🔧 DEV MODE - SMS Код верификации"
|
||||
open={debugModalVisible}
|
||||
onCancel={() => setDebugModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="copy" icon={<CopyOutlined />} onClick={async () => {
|
||||
if (debugCode) {
|
||||
try {
|
||||
// Проверяем доступность Clipboard API
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(debugCode);
|
||||
message.success('Код скопирован в буфер обмена');
|
||||
} else {
|
||||
// Fallback для старых браузеров
|
||||
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('Не удалось скопировать код');
|
||||
}
|
||||
}
|
||||
}}>
|
||||
Скопировать код
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={() => setDebugModalVisible(false)}>
|
||||
Закрыть
|
||||
</Button>
|
||||
]}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ padding: '20px 0', textAlign: 'center' }}>
|
||||
<p style={{ marginBottom: '16px', fontSize: '14px', color: '#666' }}>
|
||||
В режиме разработки SMS не отправляется реально.
|
||||
Используйте этот код для верификации:
|
||||
</p>
|
||||
<div style={{
|
||||
fontSize: '32px',
|
||||
fontWeight: 'bold',
|
||||
color: '#1890ff',
|
||||
letterSpacing: '8px',
|
||||
padding: '20px',
|
||||
background: '#f0f2f5',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '16px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{debugCode}
|
||||
</div>
|
||||
<p style={{ fontSize: '12px', color: '#999' }}>
|
||||
Код действителен 10 минут
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 🔧 Технические кнопки для разработки (только в dev режиме) */}
|
||||
{import.meta.env.DEV && (
|
||||
<div style={{
|
||||
marginTop: 24,
|
||||
padding: 16,
|
||||
background: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
border: '2px dashed #999'
|
||||
}}>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
|
||||
🔧 DEV MODE - Быстрая навигация (без валидации)
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
// Автозаполняем телефон и email
|
||||
const devData = {
|
||||
phone: '79001234567', // БЕЗ +
|
||||
email: 'test@test.ru',
|
||||
};
|
||||
updateFormData(devData);
|
||||
setIsPhoneVerified(true);
|
||||
message.success('DEV: Телефон автоматически подтверждён');
|
||||
onNext();
|
||||
}}
|
||||
size="small"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Далее → (Step 2) [пропустить]
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{debugCode && !isPhoneVerified && (
|
||||
{import.meta.env.DEV && debugCode && !isPhoneVerified && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
@@ -348,9 +356,29 @@ export default function Step3Payment({
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
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({
|
||||
<AutoComplete
|
||||
placeholder={banksLoading ? "Загрузка списка банков..." : "Начните вводить название банка"}
|
||||
size="large"
|
||||
loading={banksLoading}
|
||||
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден. Попробуйте ввести другое название"}
|
||||
options={banks.map((bank) => ({
|
||||
value: bank.bankname,
|
||||
|
||||
@@ -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<any>(null);
|
||||
const [debugCode, setDebugCode] = useState<string | null>(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({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ✅ Дебажный код (только в DEV режиме) */}
|
||||
{import.meta.env.DEV && debugCode && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
padding: 12,
|
||||
background: '#fafafa',
|
||||
borderRadius: 8,
|
||||
border: '1px dashed #d9d9d9',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '14px' }}>
|
||||
<strong>DEBUG код:</strong> {debugCode}
|
||||
</span>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
size="small"
|
||||
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('Не удалось скопировать код');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Скопировать
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button, Card, Checkbox, Form, Input, Radio, Result, Select, Skeleton, Space, Tag, Typography, Upload, message, Progress } from 'antd';
|
||||
import { debugLog } from '../../utils/debugLog';
|
||||
import { Button, Card, Checkbox, Form, Input, Radio, Result, Select, Skeleton, Space, Tag, Typography, Upload, message, Progress, Alert, Modal } from 'antd';
|
||||
import { LoadingOutlined, PlusOutlined, ThunderboltOutlined, InboxOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import AiWorkingIllustration from '../../assets/ai-working.svg';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
@@ -51,6 +52,7 @@ interface Props {
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
|
||||
onGoToStart?: () => void; // Callback для перехода к началу (список черновиков)
|
||||
}
|
||||
|
||||
const evaluateCondition = (condition: WizardQuestion['ask_if'], values: Record<string, any>) => {
|
||||
@@ -111,14 +113,17 @@ export default function StepWizardPlan({
|
||||
onNext,
|
||||
onPrev,
|
||||
addDebugEvent,
|
||||
onGoToStart,
|
||||
}: Props) {
|
||||
console.log('🔥 StepWizardPlan v1.4 - 2025-11-20 15:00 - Add unified_id and claim_id to wizard payload');
|
||||
debugLog.log('🔥 StepWizardPlan v1.4 - 2025-11-20 15:00 - Add unified_id and claim_id to wizard payload');
|
||||
const [form] = Form.useForm();
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debugLoggerRef = useRef<typeof addDebugEvent>(addDebugEvent);
|
||||
const [isWaiting, setIsWaiting] = useState(!formData.wizardPlan);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [supportModalVisible, setSupportModalVisible] = useState(false);
|
||||
const [sendingToSupport, setSendingToSupport] = useState(false);
|
||||
const [plan, setPlan] = useState<any>(formData.wizardPlan || null);
|
||||
const [prefillMap, setPrefillMap] = useState<Record<string, any>>(
|
||||
formData.wizardPrefill || buildPrefillMap(formData.wizardPrefillArray)
|
||||
@@ -160,7 +165,7 @@ export default function StepWizardPlan({
|
||||
const hasAnswers = Object.keys(answers).some(key => answers[key] !== undefined && answers[key] !== '');
|
||||
|
||||
if (hasAnswers) {
|
||||
console.log('💾 Автосохранение прогресса:', { claim_id: formData.claim_id, answersCount: Object.keys(answers).length });
|
||||
debugLog.log('💾 Автосохранение прогресса:', { claim_id: formData.claim_id, answersCount: Object.keys(answers).length });
|
||||
|
||||
// Обновляем formData с текущими ответами
|
||||
updateFormData({
|
||||
@@ -380,7 +385,7 @@ export default function StepWizardPlan({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWaiting || !formData.session_id || plan) {
|
||||
console.log('⏭️ StepWizardPlan: пропускаем подписку SSE', {
|
||||
debugLog.log('⏭️ StepWizardPlan: пропускаем подписку SSE', {
|
||||
isWaiting,
|
||||
hasSessionId: !!formData.session_id,
|
||||
hasPlan: !!plan,
|
||||
@@ -389,13 +394,13 @@ export default function StepWizardPlan({
|
||||
}
|
||||
|
||||
const sessionId = formData.session_id;
|
||||
console.log('🔌 StepWizardPlan: подписываемся на SSE канал для получения wizard_plan', {
|
||||
debugLog.log('🔌 StepWizardPlan: подписываемся на SSE канал для получения wizard_plan', {
|
||||
session_id: sessionId,
|
||||
sse_url: `/events/${sessionId}`,
|
||||
sse_url: `/api/v1/events/${sessionId}`,
|
||||
redis_channel: `ocr_events:${sessionId}`,
|
||||
});
|
||||
|
||||
const source = new EventSource(`/events/${sessionId}`);
|
||||
const source = new EventSource(`/api/v1/events/${sessionId}`);
|
||||
eventSourceRef.current = source;
|
||||
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { session_id: sessionId });
|
||||
|
||||
@@ -461,25 +466,113 @@ export default function StepWizardPlan({
|
||||
payload_preview: JSON.stringify(payload).substring(0, 200),
|
||||
});
|
||||
|
||||
// ✅ НОВЫЙ ФЛОУ: Обработка списка документов
|
||||
if (eventType === 'documents_list_ready') {
|
||||
const documentsRequired = payload.documents_required || [];
|
||||
// ✅ ОБРАБОТКА ОШИБОК: Если пришла ошибка обработки
|
||||
if (eventType === 'documents_list_error' ||
|
||||
eventType === 'processing_error' ||
|
||||
payload.status === 'error' && eventType === 'documents_list_ready') {
|
||||
const errorMessage = payload.message || payload.error_message || 'Произошла ошибка при обработке вашего запроса';
|
||||
const errorDetails = payload.error_details || payload.details || null;
|
||||
|
||||
debugLoggerRef.current?.('wizard', 'error', '❌ Ошибка обработки запроса', {
|
||||
session_id: sessionId,
|
||||
event_type: eventType,
|
||||
error_message: errorMessage,
|
||||
error_details: errorDetails,
|
||||
});
|
||||
|
||||
setIsWaiting(false);
|
||||
setConnectionError(errorMessage);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
message.error(errorMessage);
|
||||
source.close();
|
||||
|
||||
// Сохраняем информацию об ошибке
|
||||
updateFormData({
|
||||
wizardPlanStatus: 'error',
|
||||
errorMessage: errorMessage,
|
||||
errorDetails: errorDetails,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ ОБРАБОТКА "НЕ НАШЕЙ ТЕМАТИКИ": Если вопрос не нашей тематики
|
||||
if (eventType === 'out_of_scope' ||
|
||||
eventType === 'not_our_topic' ||
|
||||
eventType === 'topic_not_supported') {
|
||||
const outOfScopeMessage = payload.message ||
|
||||
'К сожалению, ваш вопрос не относится к нашей компетенции. Мы помогаем с защитой прав потребителей в сфере услуг, товаров и туризма.';
|
||||
const suggestedActions = payload.suggested_actions || payload.actions || [];
|
||||
|
||||
// ✅ Извлекаем claim_id и ticket_number из payload (могут быть в data или в корне)
|
||||
const claimIdFromPayload = payload.claim_id || payload.data?.claim_id;
|
||||
const ticketNumberFromPayload = payload.ticket_number || payload.data?.ticket_number;
|
||||
const ticketIdFromPayload = payload.ticket_id || payload.data?.ticket_id;
|
||||
|
||||
debugLoggerRef.current?.('wizard', 'warning', '⚠️ Вопрос не нашей тематики', {
|
||||
session_id: sessionId,
|
||||
event_type: eventType,
|
||||
message: outOfScopeMessage,
|
||||
suggested_actions: suggestedActions,
|
||||
claim_id: claimIdFromPayload,
|
||||
ticket_number: ticketNumberFromPayload,
|
||||
});
|
||||
|
||||
setIsWaiting(false);
|
||||
setConnectionError(outOfScopeMessage);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
message.warning(outOfScopeMessage);
|
||||
source.close();
|
||||
|
||||
// Сохраняем информацию о том, что вопрос не нашей тематики
|
||||
updateFormData({
|
||||
wizardPlanStatus: 'out_of_scope',
|
||||
outOfScopeMessage: outOfScopeMessage,
|
||||
suggestedActions: suggestedActions,
|
||||
// ✅ Сохраняем claim_id и ticket_number из payload (если есть)
|
||||
claim_id: claimIdFromPayload || formData.claim_id,
|
||||
ticket_number: ticketNumberFromPayload || formData.ticket_number,
|
||||
ticket_id: ticketIdFromPayload || formData.ticket_id,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ НОВЫЙ ФЛОУ: Приоритет - если есть documents_required, показываем загрузку документов
|
||||
// Проверяем documents_required в payload (может быть в корне или в wizard_plan)
|
||||
const documentsRequired = payload.documents_required || payload.documents || [];
|
||||
|
||||
// ✅ ВАЖНО: Если eventType === 'documents_list_ready' ИЛИ есть documents_required, показываем загрузку документов
|
||||
const isDocumentsListReady = eventType === 'documents_list_ready' || documentsRequired.length > 0;
|
||||
|
||||
if (isDocumentsListReady && documentsRequired.length > 0) {
|
||||
debugLoggerRef.current?.('wizard', 'success', '📋 Получен список документов!', {
|
||||
session_id: sessionId,
|
||||
documents_count: documentsRequired.length,
|
||||
documents: documentsRequired.map((d: any) => d.name),
|
||||
documents: documentsRequired.map((d: any) => d.name || d.id),
|
||||
});
|
||||
|
||||
console.log('📋 documents_list_ready:', {
|
||||
debugLog.log('📋 documents_required найден в payload:', {
|
||||
eventType,
|
||||
claim_id: payload.claim_id,
|
||||
documents_required: documentsRequired,
|
||||
payload_keys: Object.keys(payload),
|
||||
});
|
||||
|
||||
// Сохраняем в formData для нового флоу
|
||||
updateFormData({
|
||||
documents_required: documentsRequired,
|
||||
claim_id: payload.claim_id,
|
||||
claim_id: payload.claim_id || formData.claim_id,
|
||||
wizardPlanStatus: 'documents_ready', // Новый статус
|
||||
});
|
||||
|
||||
@@ -491,13 +584,13 @@ export default function StepWizardPlan({
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Пока показываем alert для теста, потом переход к StepDocumentsNew
|
||||
message.success(`Получен список документов: ${documentsRequired.length} шт.`);
|
||||
|
||||
// TODO: onNext() для перехода к StepDocumentsNew
|
||||
// Не показываем визард, если есть документы для загрузки
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ СТАРЫЙ ФЛОУ: Если нет documents_required, показываем визард
|
||||
const wizardPayload = extractWizardPayload(payload);
|
||||
const hasWizardPlan = Boolean(wizardPayload);
|
||||
|
||||
@@ -797,7 +890,7 @@ export default function StepWizardPlan({
|
||||
);
|
||||
|
||||
// 🔍 Логируем отправляемые метаданные документов
|
||||
console.log(`📁 Группа ${i}:`, {
|
||||
debugLog.log(`📁 Группа ${i}:`, {
|
||||
field_name: fieldLabel,
|
||||
field_label: finalFieldLabel,
|
||||
description: block.description,
|
||||
@@ -814,7 +907,7 @@ export default function StepWizardPlan({
|
||||
});
|
||||
|
||||
// Логируем ключевые поля перед отправкой
|
||||
console.log('📤 Отправка в n8n:', {
|
||||
debugLog.log('📤 Отправка в n8n:', {
|
||||
session_id: formData.session_id,
|
||||
unified_id: formData.unified_id,
|
||||
claim_id: formData.claim_id,
|
||||
@@ -853,7 +946,7 @@ export default function StepWizardPlan({
|
||||
if (formData.session_id) {
|
||||
subscribeToClaimPlan(formData.session_id);
|
||||
} else {
|
||||
console.warn('⚠️ session_id отсутствует, не можем подписаться на claim:plan');
|
||||
debugLog.warn('⚠️ session_id отсутствует, не можем подписаться на claim:plan');
|
||||
onNext();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -869,7 +962,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Функция подписки на канал claim:plan
|
||||
const subscribeToClaimPlan = useCallback((sessionToken: string) => {
|
||||
console.log('📡 Подписка на канал claim:plan для session:', sessionToken);
|
||||
debugLog.log('📡 Подписка на канал claim:plan для session:', sessionToken);
|
||||
|
||||
// Закрываем предыдущее соединение, если есть
|
||||
if (eventSourceRef.current) {
|
||||
@@ -882,7 +975,7 @@ export default function StepWizardPlan({
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ Подключено к каналу claim:plan');
|
||||
debugLog.log('✅ Подключено к каналу claim:plan');
|
||||
addDebugEvent?.('claim-plan', 'info', '📡 Ожидание данных заявления...');
|
||||
message.loading('Ожидание данных заявления...', 0);
|
||||
};
|
||||
@@ -890,7 +983,7 @@ export default function StepWizardPlan({
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📥 Получены данные из claim:plan:', data);
|
||||
debugLog.log('📥 Получены данные из claim:plan:', data);
|
||||
|
||||
if (data.event_type === 'claim_plan_ready' && data.status === 'ready') {
|
||||
// Данные заявления получены!
|
||||
@@ -940,7 +1033,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Таймаут на 5 минут
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.warn('⏰ Таймаут ожидания данных заявления');
|
||||
debugLog.warn('⏰ Таймаут ожидания данных заявления');
|
||||
message.destroy();
|
||||
message.warning('Превышено время ожидания данных заявления');
|
||||
eventSource.close();
|
||||
@@ -1287,7 +1380,7 @@ export default function StepWizardPlan({
|
||||
);
|
||||
|
||||
const renderQuestions = () => {
|
||||
console.log('🔍 StepWizardPlan renderQuestions:', {
|
||||
debugLog.log('🔍 StepWizardPlan renderQuestions:', {
|
||||
questionsCount: questions.length,
|
||||
documentsCount: documents.length,
|
||||
questions: questions.map(q => ({ name: q.name, label: q.label, input_type: q.input_type, required: q.required }))
|
||||
@@ -1330,7 +1423,7 @@ export default function StepWizardPlan({
|
||||
{() => {
|
||||
const values = form.getFieldsValue(true);
|
||||
if (!evaluateCondition(question.ask_if, values)) {
|
||||
console.log(`⏭️ Question ${question.name} skipped: condition not met`, question.ask_if, values);
|
||||
debugLog.log(`⏭️ Question ${question.name} skipped: condition not met`, question.ask_if, values);
|
||||
return null;
|
||||
}
|
||||
const questionDocs = documentGroups[question.name] || [];
|
||||
@@ -1347,7 +1440,7 @@ export default function StepWizardPlan({
|
||||
questionNameLower === 'correspondence_exist' ||
|
||||
questionNameLower.includes('docs_exist');
|
||||
if (isDocsExistQuestion && documents.length > 0) {
|
||||
console.log(`🚫 Question ${question.name} hidden: docs_exist with documents`);
|
||||
debugLog.log(`🚫 Question ${question.name} hidden: docs_exist with documents`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1371,11 +1464,11 @@ export default function StepWizardPlan({
|
||||
// (даже если вопрос не связан с documentGroups)
|
||||
// Загрузка файлов уже реализована через блоки документов (documents)
|
||||
if (isDocumentUploadQuestion && documents.length > 0) {
|
||||
console.log(`🚫 Question ${question.name} hidden: isDocumentUploadQuestion=true, documents.length=${documents.length}`);
|
||||
debugLog.log(`🚫 Question ${question.name} hidden: isDocumentUploadQuestion=true, documents.length=${documents.length}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`✅ Question ${question.name} will render:`, { input_type: question.input_type, label: question.label, required: question.required });
|
||||
debugLog.log(`✅ Question ${question.name} will render:`, { input_type: question.input_type, label: question.label, required: question.required });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1438,11 +1531,38 @@ export default function StepWizardPlan({
|
||||
}
|
||||
|
||||
// ✅ НОВЫЙ ФЛОУ: Если есть documents_required, показываем загрузку документов
|
||||
// Также проверяем plan.documents - если n8n вернул wizard_plan с documents, конвертируем их
|
||||
const documentsRequired = formData.documents_required || [];
|
||||
const hasNewFlowDocs = documentsRequired.length > 0;
|
||||
const planDocuments = plan?.documents || [];
|
||||
|
||||
// ✅ ВАЖНО: Если есть documents в plan, но нет в formData.documents_required, конвертируем их
|
||||
const hasPlanDocuments = planDocuments.length > 0 && documentsRequired.length === 0;
|
||||
const hasNewFlowDocs = documentsRequired.length > 0 || hasPlanDocuments;
|
||||
|
||||
// Конвертируем plan.documents в documents_required при первом рендере
|
||||
useEffect(() => {
|
||||
if (hasPlanDocuments && !formData.documents_required) {
|
||||
debugLog.log('🔄 Конвертируем plan.documents в documents_required:', planDocuments);
|
||||
const convertedDocs = planDocuments.map((doc: any) => ({
|
||||
id: doc.id || doc.name?.toLowerCase().replace(/\s+/g, '_'),
|
||||
name: doc.name,
|
||||
hints: doc.hints || doc.description,
|
||||
accept: doc.accept || ['pdf', 'jpg', 'png'],
|
||||
required: doc.required !== false,
|
||||
priority: doc.priority || 1,
|
||||
}));
|
||||
|
||||
updateFormData({
|
||||
documents_required: convertedDocs,
|
||||
wizardPlanStatus: 'documents_ready',
|
||||
});
|
||||
|
||||
debugLog.log('✅ Конвертировано документов:', convertedDocs.length);
|
||||
}
|
||||
}, [hasPlanDocuments, planDocuments, formData.documents_required, updateFormData]);
|
||||
|
||||
// 🔍 ОТЛАДКА: Логируем состояние для диагностики
|
||||
console.log('🔍 StepWizardPlan - определение флоу:', {
|
||||
debugLog.log('🔍 StepWizardPlan - определение флоу:', {
|
||||
documentsRequiredCount: documentsRequired.length,
|
||||
documentsRequired: documentsRequired,
|
||||
hasNewFlowDocs,
|
||||
@@ -1459,7 +1579,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Отладка: логируем инициализацию
|
||||
useEffect(() => {
|
||||
console.log('🔍 Инициализация документов:', {
|
||||
debugLog.log('🔍 Инициализация документов:', {
|
||||
documentsRequiredCount: documentsRequired.length,
|
||||
initialUploadedDocs,
|
||||
uploadedDocs,
|
||||
@@ -1512,7 +1632,7 @@ export default function StepWizardPlan({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 Инициализация currentDocIndex:', {
|
||||
debugLog.log('🔍 Инициализация currentDocIndex:', {
|
||||
savedIndex,
|
||||
firstUnprocessed,
|
||||
documentsRequiredLength: documentsRequired.length,
|
||||
@@ -1536,7 +1656,7 @@ export default function StepWizardPlan({
|
||||
// Если текущий индекс выходит за границы, исправляем
|
||||
if (currentDocIndex >= documentsRequired.length) {
|
||||
const firstUnprocessed = findFirstUnprocessedDoc(0);
|
||||
console.log('🔄 Исправление currentDocIndex (выход за границы):', {
|
||||
debugLog.log('🔄 Исправление currentDocIndex (выход за границы):', {
|
||||
currentIndex: currentDocIndex,
|
||||
documentsRequiredLength: documentsRequired.length,
|
||||
firstUnprocessed,
|
||||
@@ -1552,7 +1672,7 @@ export default function StepWizardPlan({
|
||||
const docId = currentDoc.id || currentDoc.name;
|
||||
if (uploadedDocs.includes(docId) || skippedDocs.includes(docId)) {
|
||||
const firstUnprocessed = findFirstUnprocessedDoc(currentDocIndex + 1);
|
||||
console.log('🔄 Исправление currentDocIndex (документ уже обработан):', {
|
||||
debugLog.log('🔄 Исправление currentDocIndex (документ уже обработан):', {
|
||||
currentIndex: currentDocIndex,
|
||||
currentDocId: docId,
|
||||
firstUnprocessed,
|
||||
@@ -1581,7 +1701,7 @@ export default function StepWizardPlan({
|
||||
const processedCount = uploadedDocs.length + skippedDocs.length;
|
||||
const allProcessed = allRequiredDocsProcessed && processedCount >= documentsRequired.length;
|
||||
|
||||
console.log('🔍 Проверка завершённости:', {
|
||||
debugLog.log('🔍 Проверка завершённости:', {
|
||||
uploadedDocs: uploadedDocs.length,
|
||||
skippedDocs: skippedDocs.length,
|
||||
totalRequired: documentsRequired.length,
|
||||
@@ -1606,7 +1726,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Отладка: логируем состояние текущего документа
|
||||
useEffect(() => {
|
||||
console.log('🔍 Текущий документ для загрузки:', {
|
||||
debugLog.log('🔍 Текущий документ для загрузки:', {
|
||||
currentDocIndex,
|
||||
documentsRequiredLength: documentsRequired.length,
|
||||
currentDoc: currentDoc ? { id: currentDoc.id, name: currentDoc.name } : null,
|
||||
@@ -1625,7 +1745,7 @@ export default function StepWizardPlan({
|
||||
const isAlreadySkipped = skippedDocs.includes(docId);
|
||||
|
||||
if (isAlreadyUploaded || isAlreadySkipped) {
|
||||
console.log(`⏭️ Документ "${currentDoc.name}" уже обработан, переходим к следующему`);
|
||||
debugLog.log(`⏭️ Документ "${currentDoc.name}" уже обработан, переходим к следующему`);
|
||||
const nextIndex = findFirstUnprocessedDoc(currentDocIndex + 1);
|
||||
|
||||
// Обновляем только если следующий индекс отличается от текущего
|
||||
@@ -1640,7 +1760,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Обработчик выбора файлов (НЕ отправляем сразу, только сохраняем)
|
||||
const handleFilesChange = (fileList: any[]) => {
|
||||
console.log('📁 handleFilesChange:', fileList.length, 'файлов', fileList.map(f => f.name));
|
||||
debugLog.log('📁 handleFilesChange:', fileList.length, 'файлов', fileList.map(f => f.name));
|
||||
setCurrentUploadedFiles(fileList);
|
||||
if (fileList.length > 0) {
|
||||
setDocChoice('upload');
|
||||
@@ -1661,7 +1781,7 @@ export default function StepWizardPlan({
|
||||
setSkippedDocs(newSkipped);
|
||||
|
||||
// ✅ ЛОГИРОВАНИЕ: Пропуск документа
|
||||
console.log('⏭️ Документ пропущен:', {
|
||||
debugLog.log('⏭️ Документ пропущен:', {
|
||||
document_id: currentDoc.id,
|
||||
document_name: currentDoc.name,
|
||||
document_type: currentDoc.type || currentDoc.id,
|
||||
@@ -1714,7 +1834,7 @@ export default function StepWizardPlan({
|
||||
if (formData.contact_id) formDataToSend.append('contact_id', formData.contact_id);
|
||||
if (formData.phone) formDataToSend.append('phone', formData.phone);
|
||||
|
||||
console.log('💾 Отправка пропущенного документа в n8n:', {
|
||||
debugLog.log('💾 Отправка пропущенного документа в n8n:', {
|
||||
claim_id: formData.claim_id,
|
||||
document_type: currentDoc.type || currentDoc.id,
|
||||
document_name: currentDoc.name,
|
||||
@@ -1732,7 +1852,7 @@ export default function StepWizardPlan({
|
||||
// Не блокируем пользователя - данные сохранятся при следующем сохранении черновика
|
||||
} else {
|
||||
const result = await response.json();
|
||||
console.log('✅ Пропущенный документ отправлен в n8n:', result);
|
||||
debugLog.log('✅ Пропущенный документ отправлен в n8n:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отправки пропущенного документа в n8n:', error);
|
||||
@@ -1750,7 +1870,7 @@ export default function StepWizardPlan({
|
||||
try {
|
||||
setSubmitting(true);
|
||||
|
||||
console.log('📤 Загружаем все файлы одним запросом:', {
|
||||
debugLog.log('📤 Загружаем все файлы одним запросом:', {
|
||||
totalFiles: currentUploadedFiles.length,
|
||||
files: currentUploadedFiles.map(f => ({ name: f.name, uid: f.uid, size: f.size }))
|
||||
});
|
||||
@@ -1782,7 +1902,7 @@ export default function StepWizardPlan({
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('✅ Все файлы загружены:', result);
|
||||
debugLog.log('✅ Все файлы загружены:', result);
|
||||
|
||||
// Обновляем состояние
|
||||
const uploadedDocsData = [...(formData.documents_uploaded || [])];
|
||||
@@ -1885,7 +2005,7 @@ export default function StepWizardPlan({
|
||||
if (!response.ok) {
|
||||
console.error('❌ Ошибка отправки кастомного документа:', await response.text());
|
||||
} else {
|
||||
console.log('✅ Кастомный документ отправлен:', block.description);
|
||||
debugLog.log('✅ Кастомный документ отправлен:', block.description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1915,11 +2035,11 @@ export default function StepWizardPlan({
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('✅ OCR status check:', data);
|
||||
debugLog.log('✅ OCR status check:', data);
|
||||
|
||||
// Если есть кэш — сразу переходим
|
||||
if (data.from_cache && data.form_draft) {
|
||||
console.log('✅ Используем кэшированные данные:', data.form_draft);
|
||||
debugLog.log('✅ Используем кэшированные данные:', data.form_draft);
|
||||
const formDraft = data.form_draft;
|
||||
const user = formDraft.user || {};
|
||||
const project = formDraft.project || {};
|
||||
@@ -1997,20 +2117,20 @@ export default function StepWizardPlan({
|
||||
|
||||
// Иначе подключаемся к SSE и ждём результат от n8n
|
||||
const sessionId = formData.session_id;
|
||||
console.log('📡 Подключаемся к SSE:', `/api/v1/events/${sessionId}`);
|
||||
debugLog.log('📡 Подключаемся к SSE:', `/api/v1/events/${sessionId}`);
|
||||
|
||||
const eventSource = new EventSource(`/api/v1/events/${sessionId}`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const eventData = JSON.parse(event.data);
|
||||
console.log('📥 SSE event:', eventData);
|
||||
debugLog.log('📥 SSE event:', eventData);
|
||||
|
||||
// Обрабатываем событие ocr_status
|
||||
if (eventData.event_type === 'ocr_status') {
|
||||
if (eventData.status === 'ready') {
|
||||
// ✅ Успех — данные готовы
|
||||
console.log('✅ Заявление готово:', eventData.data);
|
||||
debugLog.log('✅ Заявление готово:', eventData.data);
|
||||
const formDraft = eventData.data?.form_draft;
|
||||
|
||||
// Формируем claimPlanData для StepClaimConfirmation
|
||||
@@ -2116,7 +2236,7 @@ export default function StepWizardPlan({
|
||||
// Таймаут 3 минуты (RAG может занять время)
|
||||
setTimeout(() => {
|
||||
if (eventSource.readyState !== EventSource.CLOSED) {
|
||||
console.warn('⏰ SSE timeout');
|
||||
debugLog.warn('⏰ SSE timeout');
|
||||
message.destroy();
|
||||
setIsFormingClaim(false);
|
||||
setRagError('Превышено время ожидания. Попробуйте ещё раз.');
|
||||
@@ -2125,7 +2245,7 @@ export default function StepWizardPlan({
|
||||
}, 180000); // 3 минуты для RAG
|
||||
|
||||
} else {
|
||||
console.warn('⚠️ OCR status check failed:', await response.text());
|
||||
debugLog.warn('⚠️ OCR status check failed:', await response.text());
|
||||
message.destroy();
|
||||
onNext();
|
||||
}
|
||||
@@ -2378,8 +2498,151 @@ export default function StepWizardPlan({
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* СТАРЫЙ ФЛОУ: Ожидание визарда */}
|
||||
{!hasNewFlowDocs && isWaiting && (
|
||||
{/* ❌ ЭКРАН ОШИБКИ: Показываем, если wizardPlanStatus === 'error' */}
|
||||
{formData.wizardPlanStatus === 'error' && formData.errorMessage && (
|
||||
<div style={{ textAlign: 'center', padding: '60px 20px', maxWidth: 600, margin: '0 auto' }}>
|
||||
<div style={{ fontSize: 64, marginBottom: 24 }}>❌</div>
|
||||
<Title level={3} style={{ marginBottom: 16 }}>Ошибка обработки запроса</Title>
|
||||
<Paragraph style={{ fontSize: 16, marginBottom: 24, color: '#595959' }}>
|
||||
{formData.errorMessage}
|
||||
</Paragraph>
|
||||
{formData.errorDetails && (
|
||||
<Alert
|
||||
type="info"
|
||||
message="Детали ошибки"
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary">Код: {formData.errorDetails.code || 'UNKNOWN'}</Text>
|
||||
{formData.errorDetails.reason && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">Причина: {formData.errorDetails.reason}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: 24, textAlign: 'left' }}
|
||||
/>
|
||||
)}
|
||||
<Space>
|
||||
<Button type="primary" size="large" onClick={handleRefreshPlan}>
|
||||
Попробовать снова
|
||||
</Button>
|
||||
<Button size="large" onClick={onPrev}>
|
||||
Вернуться назад
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ⚠️ ЭКРАН "НЕ НАШЕЙ ТЕМАТИКИ": Показываем, если wizardPlanStatus === 'out_of_scope' */}
|
||||
{formData.wizardPlanStatus === 'out_of_scope' && formData.outOfScopeMessage && (
|
||||
<div style={{ textAlign: 'center', padding: '60px 20px', maxWidth: 700, margin: '0 auto' }}>
|
||||
<div style={{ fontSize: 64, marginBottom: 24 }}>⚠️</div>
|
||||
<Title level={3} style={{ marginBottom: 16 }}>Вопрос не нашей компетенции</Title>
|
||||
<Paragraph style={{ fontSize: 16, marginBottom: 32, color: '#595959', lineHeight: 1.8 }}>
|
||||
{formData.outOfScopeMessage}
|
||||
</Paragraph>
|
||||
|
||||
{formData.suggestedActions && formData.suggestedActions.length > 0 && (
|
||||
<div style={{ marginBottom: 32, textAlign: 'left' }}>
|
||||
<Title level={4} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
Рекомендуемые действия:
|
||||
</Title>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{formData.suggestedActions.map((action: any, index: number) => {
|
||||
// Обработка кнопки "Связаться с поддержкой"
|
||||
if (action.actionType === 'contact_support' || (!action.url && action.title?.toLowerCase().includes('поддержк'))) {
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
style={{ textAlign: 'left' }}
|
||||
actions={[
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setSupportModalVisible(true)}
|
||||
>
|
||||
Отправить в поддержку →
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={action.title}
|
||||
description={action.description}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Обработка внешней ссылки (партнёры) - если есть 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 (
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
style={{ textAlign: 'left' }}
|
||||
actions={[
|
||||
<Button
|
||||
type="link"
|
||||
href={action.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontWeight: 500 }}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={action.title}
|
||||
description={
|
||||
<div>
|
||||
<div>{action.description}</div>
|
||||
{isPartnerLink && action.urlText && (
|
||||
<div style={{ marginTop: 8, color: '#1890ff', fontWeight: 500 }}>
|
||||
{action.urlText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Обычная карточка без ссылки
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
style={{ textAlign: 'left' }}
|
||||
>
|
||||
<Card.Meta
|
||||
title={action.title}
|
||||
description={action.description}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Space>
|
||||
<Button type="primary" size="large" onClick={onPrev}>
|
||||
Вернуться к описанию
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* СТАРЫЙ ФЛОУ: Ожидание визарда (только если нет ошибок) */}
|
||||
{!hasNewFlowDocs && isWaiting && formData.wizardPlanStatus !== 'error' && formData.wizardPlanStatus !== 'out_of_scope' && (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<img
|
||||
src={AiWorkingIllustration}
|
||||
@@ -2411,8 +2674,44 @@ export default function StepWizardPlan({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* СТАРЫЙ ФЛОУ: Визард готов */}
|
||||
{!hasNewFlowDocs && !isWaiting && plan && (
|
||||
{/* ✅ ПЕРЕХОДНЫЙ СЛУЧАЙ: Если есть documents в plan, но нет в formData.documents_required */}
|
||||
{!hasNewFlowDocs && !isWaiting && plan && plan.documents && plan.documents.length > 0 && (
|
||||
<div style={{ padding: '24px 0' }}>
|
||||
<Alert
|
||||
message="Обнаружены документы в плане"
|
||||
description="Конвертируем план в новый формат загрузки документов..."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
// Конвертируем plan.documents в documents_required
|
||||
const convertedDocs = plan.documents.map((doc: any) => ({
|
||||
id: doc.id || doc.name?.toLowerCase().replace(/\s+/g, '_'),
|
||||
name: doc.name,
|
||||
hints: doc.hints || doc.description,
|
||||
accept: doc.accept || ['pdf', 'jpg', 'png'],
|
||||
required: doc.required !== false,
|
||||
priority: doc.priority || 1,
|
||||
}));
|
||||
|
||||
updateFormData({
|
||||
documents_required: convertedDocs,
|
||||
wizardPlanStatus: 'documents_ready',
|
||||
});
|
||||
|
||||
message.success(`Конвертировано ${convertedDocs.length} документов`);
|
||||
}}
|
||||
>
|
||||
Перейти к пошаговой загрузке документов
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* СТАРЫЙ ФЛОУ: Визард готов (только если НЕТ documents в plan) */}
|
||||
{!hasNewFlowDocs && !isWaiting && plan && (!plan.documents || plan.documents.length === 0) && (
|
||||
<div>
|
||||
<Title level={4} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user