390 lines
17 KiB
TypeScript
390 lines
17 KiB
TypeScript
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>
|
||
);
|
||
}
|
||
|
||
|