Files
aiform_prod/frontend/src/components/form/Step1Phone.tsx
AI Assistant 080e7ec105 feat: Получение cf_2624 из MySQL и блокировка полей при подтверждении данных
- Добавлен сервис CrmMySQLService для прямого подключения к MySQL CRM
- Обновлён метод get_draft() для получения cf_2624 напрямую из БД
- Реализована блокировка полей (readonly) при contact_data_confirmed = true
- Добавлен выбор банка для СБП выплат с динамической загрузкой из API
- Обновлена документация по работе с cf_2624 и MySQL
- Добавлен network_mode: host в docker-compose для доступа к MySQL
- Обновлены компоненты формы для поддержки блокировки полей
2025-12-04 12:22:23 +03:00

376 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Form, Input, Button, message, Space } from 'antd';
import { PhoneOutlined, SafetyOutlined } 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 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 });
if (result.debug_code) {
message.info(`DEBUG: Код ${result.debug_code}`);
}
} 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>
{/* 🔧 Технические кнопки для разработки */}
<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>
);
}