Files
aiform_dev/frontend/src/components/form/Step1Phone.tsx

390 lines
17 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { Form, Input, Button, message, Space, Modal } from 'antd';
import { PhoneOutlined, SafetyOutlined, CopyOutlined } from '@ant-design/icons';
interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: (unified_id?: string) => void; // ✅ Может принимать unified_id
setIsPhoneVerified: (verified: boolean) => void;
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
export default function Step1Phone({
formData,
updateFormData,
onNext,
setIsPhoneVerified,
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');
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 [showDebugModal, setShowDebugModal] = useState(false);
const sendCode = async () => {
try {
const values = await form.validateFields(['phone']);
const phone = `7${values.phone}`; // БЕЗ +, формат: 79001234567
setLoading(true);
addDebugEvent?.('sms', 'pending', `📱 Отправляю SMS на ${phone}...`, { phone });
const response = await fetch('/api/v1/sms/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ SMS отправлен (DEBUG mode)`, {
phone,
debug_code: result.debug_code,
message: result.message
});
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
updateFormData({ phone });
// 🔧 DEV MODE: показываем debug код в модалке
if (result.debug_code) {
setDebugCode(result.debug_code);
setShowDebugModal(true);
}
} else {
addDebugEvent?.('sms', 'error', `❌ Ошибка SMS: ${result.detail}`, { error: result.detail });
message.error(result.detail || 'Ошибка отправки кода');
}
} catch (error) {
if ((error as any)?.errorFields) {
message.error('Введите номер телефона');
} else {
message.error('Ошибка соединения с сервером');
}
} finally {
setLoading(false);
}
};
const verifyCode = async () => {
try {
const values = await form.validateFields(['phone', 'smsCode']);
const phone = `7${values.phone}`; // БЕЗ +, формат: 79001234567
const code = values.smsCode;
setVerifyLoading(true);
addDebugEvent?.('sms', 'pending', `🔐 Проверяю SMS код...`, { phone, code });
const response = await fetch('/api/v1/sms/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code })
});
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ Телефон подтвержден успешно`, { phone, verified: true });
message.success('Телефон подтвержден!');
setIsPhoneVerified(true);
// После верификации создаём контакт в CRM через n8n
try {
addDebugEvent?.('crm', 'info', '📞 Создание контакта в CRM...', { phone });
const crmResponse = await fetch('/api/n8n/contact/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone,
session_id: formData.session_id, // ✅ Передаём session_id
form_id: 'ticket_form' // ✅ Маркируем источник формы
})
});
let crmResult = await crmResponse.json();
// ✅ n8n может вернуть массив - берём первый элемент
if (Array.isArray(crmResult) && crmResult.length > 0) {
crmResult = crmResult[0];
}
console.log('🔥 N8N CRM Response (after array check):', crmResult);
console.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));
// ✅ ВАЖНО: Проверяем наличие unified_id
if (!result.unified_id) {
console.error('❌ unified_id отсутствует в ответе n8n!');
console.error('❌ Полный ответ result:', result);
console.error('❌ Полный ответ crmResult:', crmResult);
message.warning('⚠️ unified_id не получен от n8n, черновики могут не отображаться');
} else {
console.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);
if (session_id_from_n8n) {
console.log('✅ session_id получен от n8n:', session_id_from_n8n);
} else {
console.warn('⚠️ session_id не найден в ответе n8n, используем текущий:', formData.session_id);
}
const finalSessionId = session_id_from_n8n || formData.session_id;
console.log('🔍 finalSessionId (будет сохранён):', finalSessionId);
const dataToSave = {
phone,
smsCode: code,
contact_id: result.contact_id,
unified_id: result.unified_id, // ✅ Unified ID из PostgreSQL (получаем от n8n)
session_id: finalSessionId, // ✅ Используем session_id от n8n, если есть
// claim_id убран - используем только session_id на этих этапах
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('🔥 =========================================');
addDebugEvent?.('crm', 'success', `✅ Контакт создан/найден в CRM`, result);
// Сохраняем данные из CRM в форму
updateFormData(dataToSave);
message.success(result.is_new_contact ? 'Контакт создан!' : 'Контакт найден!');
// ✅ Устанавливаем isPhoneVerified = true после успешной верификации
setIsPhoneVerified(true);
// 🔑 Создаём сессию в Redis для живучести (24 часа)
try {
console.log('🔑 Создаём сессию в Redis:', {
session_token: finalSessionId,
unified_id: result.unified_id,
phone: phone,
contact_id: result.contact_id
});
const sessionResponse = await fetch('/api/v1/session/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_token: finalSessionId,
unified_id: result.unified_id,
phone: phone,
contact_id: result.contact_id,
ttl_hours: 24
})
});
console.log('🔑 Session create response status:', sessionResponse.status);
if (sessionResponse.ok) {
const sessionData = await sessionResponse.json();
console.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'));
addDebugEvent?.('session', 'success', '✅ Сессия создана (TTL 24h)');
} else {
const errorText = await sessionResponse.text();
console.warn('⚠️ Не удалось создать сессию в Redis:', sessionResponse.status, errorText);
}
} catch (sessionError) {
console.error('❌ Ошибка создания сессии:', sessionError);
// Не блокируем дальнейшую работу
}
// ✅ Передаем 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('🔥 ============================================');
onNext(unifiedIdToPass);
} else {
addDebugEvent?.('crm', 'error', '❌ Ошибка создания контакта в CRM', crmResult);
message.error('Ошибка создания контакта в CRM');
}
} catch (crmError) {
addDebugEvent?.('crm', 'error', '❌ Ошибка соединения с CRM', { error: String(crmError) });
message.error('Ошибка соединения с CRM');
}
} else {
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, { phone, code, error: result.detail });
message.error(result.detail || 'Неверный код');
}
} catch (error) {
if ((error as any)?.errorFields) {
message.error('Введите код из SMS');
} else {
message.error('Ошибка соединения с сервером');
}
} finally {
setVerifyLoading(false);
}
};
return (
<Form
form={form}
layout="vertical"
initialValues={formData}
style={{ marginTop: 24 }}
>
<h3 style={{ marginTop: 0 }}>📱 Подтверждение телефона</h3>
<Form.Item
label="Номер телефона"
name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\d{10}$/, message: 'Введите 10 цифр без кода страны' }
]}
>
<Space.Compact style={{ width: '100%' }}>
<Input
readOnly
value="+7"
size="large"
style={{ width: '50px', textAlign: 'center', pointerEvents: 'none', background: '#f5f5f5' }}
/>
<Input
prefix={<PhoneOutlined />}
placeholder="9001234567"
maxLength={10}
size="large"
style={{ flex: 1 }}
onPaste={(e) => {
// Обработка вставки: очищаем от +7, пробелов и других символов
e.preventDefault();
const pastedText = (e.clipboardData || (window as any).clipboardData).getData('text');
// Убираем все нецифровые символы
let cleanText = pastedText.replace(/\D/g, '');
// Если начинается с 7 или 8, убираем первую цифру (код страны)
if (cleanText.length === 11 && (cleanText.startsWith('7') || cleanText.startsWith('8'))) {
cleanText = cleanText.substring(1);
}
// Оставляем только первые 10 цифр
cleanText = cleanText.substring(0, 10);
// ✅ Устанавливаем значение напрямую в input, затем синхронизируем с формой
const target = e.target as HTMLInputElement;
if (target) {
target.value = cleanText;
// Триггерим событие input для синхронизации с формой
const inputEvent = new Event('input', { bubbles: true });
target.dispatchEvent(inputEvent);
}
// ✅ Синхронизируем с формой через requestAnimationFrame для избежания циклических ссылок
requestAnimationFrame(() => {
form.setFieldValue('phone', cleanText);
// Показываем предупреждение, если номер был обрезан
if (pastedText.replace(/\D/g, '').length > 10) {
message.warning('Номер автоматически обрезан до 10 цифр');
}
});
}}
/>
</Space.Compact>
</Form.Item>
<Form.Item>
{!codeSent ? (
<Button type="primary" onClick={sendCode} loading={loading} block>
Отправить код
</Button>
) : (
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined />}
placeholder="123456"
maxLength={6}
style={{ width: '70%' }}
size="large"
name="smsCode"
onChange={(e) => form.setFieldValue('smsCode', e.target.value)}
/>
<Button type="primary" onClick={verifyCode} loading={verifyLoading} style={{ width: '30%' }} size="large">
Проверить
</Button>
</Space.Compact>
)}
</Form.Item>
{/* 🔧 DEV MODE: Модалка с SMS кодом */}
<Modal
title="🔧 DEV MODE - SMS Код"
open={showDebugModal}
onCancel={() => setShowDebugModal(false)}
footer={[
<Button
key="copy"
icon={<CopyOutlined />}
onClick={() => {
if (debugCode) {
navigator.clipboard.writeText(debugCode);
message.success('Код скопирован!');
}
}}
>
Скопировать
</Button>,
<Button key="close" type="primary" onClick={() => setShowDebugModal(false)}>
Закрыть
</Button>
]}
>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<p style={{ marginBottom: 16, color: '#666' }}>
Это DEV режим. SMS не отправляется реально.
</p>
<div style={{
fontSize: 32,
fontWeight: 'bold',
fontFamily: 'monospace',
background: '#f5f5f5',
padding: '16px 32px',
borderRadius: 8,
display: 'inline-block',
letterSpacing: 8
}}>
{debugCode}
</div>
</div>
</Modal>
</Form>
);
}