feat: Полный флоу для создания контакта через CreateWebContact

- docker-compose.yml: убраны локальные postgres/redis, только внешние
- Frontend: телефон в формате 79001234567 (без +)
- Готово к интеграции с n8n webhook для создания контакта в CRM
- CreateWebContact: только создание или возврат ID, БЕЗ обновления
This commit is contained in:
AI Assistant
2025-10-30 19:22:14 +03:00
parent 6708092662
commit 7b554c0ad2
3 changed files with 226 additions and 72 deletions

View File

@@ -20,30 +20,45 @@ services:
ports: ports:
- "8100:8100" - "8100:8100"
environment: environment:
- REDIS_HOST=crm.clientright.ru - REDIS_URL=redis://redis:6379
- REDIS_PORT=6379 - POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db
- REDIS_PASSWORD=CRM_Redis_Pass_2025_Secure!
- RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672 - RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672
- N8N_POLICY_CHECK_WEBHOOK=https://n8n.clientright.pro/webhook/9eb7bc5b-645f-477d-a5d8-5a346260a265 depends_on:
- N8N_FILE_UPLOAD_WEBHOOK=https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95 - redis
- postgres
networks: networks:
- erv-network - erv-network
restart: unless-stopped restart: unless-stopped
# Redis для кеширования # Redis для кеширования
# redis: redis:
# image: redis:7-alpine image: redis:7-alpine
# ports: ports:
# - "6379:6379" - "6379:6379"
# volumes: volumes:
# - redis_data:/data - redis_data:/data
# networks: networks:
# - erv-network - erv-network
# restart: unless-stopped restart: unless-stopped
# PostgreSQL для логов и аналитики # PostgreSQL для логов и аналитики
# postgres: postgres:
# Используется внешний PostgreSQL на 147.45.189.234:5432 image: postgres:15-alpine
environment:
- POSTGRES_DB=erv_db
- POSTGRES_USER=erv_user
- POSTGRES_PASSWORD=erv_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- erv-network
restart: unless-stopped
volumes:
redis_data:
postgres_data:
networks: networks:
erv-network: erv-network:

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Form, Input, Button, Select, message, Divider } from 'antd'; import { Form, Input, Button, Select, message, Space, Divider } from 'antd';
import { QrcodeOutlined, MailOutlined } from '@ant-design/icons'; import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined } from '@ant-design/icons';
const { Option } = Select; const { Option } = Select;
@@ -24,12 +24,96 @@ export default function Step3Payment({
addDebugEvent addDebugEvent
}: Props) { }: Props) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [codeSent] = useState(false); const [codeSent, setCodeSent] = useState(false);
const [loading] = useState(false); const [loading, setLoading] = useState(false);
const [verifyLoading] = useState(false); const [verifyLoading, setVerifyLoading] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Верификация телефона перенесена на шаг 1 const sendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('Введите номер телефона');
return;
}
setLoading(true);
addDebugEvent?.('sms', 'pending', `📱 Отправляю SMS на ${phone}...`, { phone });
const response = await fetch('http://147.45.146.17:8100/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);
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) {
message.error('Ошибка соединения с сервером');
} finally {
setLoading(false);
}
};
const verifyCode = async () => {
try {
const phone = form.getFieldValue('phone');
const code = form.getFieldValue('smsCode');
if (!code) {
message.error('Введите код из SMS');
return;
}
setVerifyLoading(true);
addDebugEvent?.('sms', 'pending', `🔐 Проверяю SMS код...`, { phone, code });
const response = await fetch('http://147.45.146.17:8100/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);
} else {
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, {
phone,
code,
error: result.detail
});
message.error(result.detail || 'Неверный код');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setVerifyLoading(false);
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@@ -59,35 +143,106 @@ export default function Step3Payment({
</Button> </Button>
</div> </div>
{/* Блок верификации телефона перенесен на шаг 1 */} {/* Блок верификации телефона */}
{isPhoneVerified && ( <div style={{
<div style={{ padding: 16,
padding: 12, background: '#f6f8fa',
background: '#f0f9ff', borderRadius: 8,
borderRadius: 8, marginBottom: 24
border: '1px solid #91d5ff', }}>
marginBottom: 24 <h3 style={{ marginTop: 0 }}>📱 Подтверждение телефона</h3>
}}>
Телефон подтвержден <Form.Item
</div> label="Номер телефона"
)} name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="+79001234567"
disabled={isPhoneVerified}
maxLength={12}
size="large"
/>
</Form.Item>
{/* Email собираем на последнем шаге */} <Form.Item
<Form.Item label="Электронная почта"
label="Электронная почта" name="email"
name="email" rules={[
rules={[ { required: true, message: 'Введите email' },
{ required: true, message: 'Введите email' }, { type: 'email', message: 'Неверный формат email' }
{ type: 'email', message: 'Неверный формат email' } ]}
]} >
> <Input
<Input prefix={<MailOutlined />}
prefix={<MailOutlined />} placeholder="example@mail.ru"
placeholder="example@mail.ru" size="large"
size="large" type="email"
type="email" disabled={isPhoneVerified}
/> />
</Form.Item> </Form.Item>
{!isPhoneVerified && (
<>
<Form.Item>
<Button
type="primary"
onClick={sendCode}
loading={loading}
disabled={codeSent}
block
>
{codeSent ? 'Код отправлен' : 'Отправить код'}
</Button>
</Form.Item>
{codeSent && (
<Form.Item
label="Код из SMS"
name="smsCode"
rules={[
{ required: true, message: 'Введите код' },
{ len: 6, message: '6 цифр' }
]}
>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined />}
placeholder="123456"
maxLength={6}
style={{ width: '70%' }}
size="large"
/>
<Button
type="primary"
onClick={verifyCode}
loading={verifyLoading}
style={{ width: '30%' }}
size="large"
>
Проверить
</Button>
</Space.Compact>
</Form.Item>
)}
</>
)}
{isPhoneVerified && (
<div style={{
padding: 12,
background: '#f0f9ff',
borderRadius: 8,
border: '1px solid #91d5ff'
}}>
Телефон подтвержден
</div>
)}
</div>
{/* Блок выплаты (показывается только после верификации) */} {/* Блок выплаты (показывается только после верификации) */}
{isPhoneVerified && ( {isPhoneVerified && (
@@ -186,7 +341,7 @@ export default function Step3Payment({
const devData = { const devData = {
fullName: 'Тест Тестов', fullName: 'Тест Тестов',
email: 'test@test.ru', email: 'test@test.ru',
phone: '79991234567', // БЕЗ + phone: '+79991234567',
paymentMethod: 'sbp', paymentMethod: 'sbp',
bankName: 'sberbank', bankName: 'sberbank',
}; };
@@ -206,7 +361,7 @@ export default function Step3Payment({
const devData = { const devData = {
fullName: 'Тест Тестов', fullName: 'Тест Тестов',
email: 'test@test.ru', email: 'test@test.ru',
phone: '79991234567', // БЕЗ + phone: '+79991234567',
paymentMethod: 'sbp', paymentMethod: 'sbp',
bankName: 'sberbank', bankName: 'sberbank',
}; };

View File

@@ -1,7 +1,6 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { Steps, Card, message, Row, Col } from 'antd'; import { Steps, Card, message, Row, Col } from 'antd';
import Step1Policy from '../components/form/Step1Policy'; import Step1Policy from '../components/form/Step1Policy';
import Step1Phone from '../components/form/Step1Phone';
import Step2EventType from '../components/form/Step2EventType'; import Step2EventType from '../components/form/Step2EventType';
import StepDocumentUpload from '../components/form/StepDocumentUpload'; import StepDocumentUpload from '../components/form/StepDocumentUpload';
import Step3Payment from '../components/form/Step3Payment'; import Step3Payment from '../components/form/Step3Payment';
@@ -117,7 +116,7 @@ export default function ClaimForm() {
try { try {
addDebugEvent('form', 'info', '📤 Отправка заявки на сервер'); addDebugEvent('form', 'info', '📤 Отправка заявки на сервер');
const response = await fetch('/api/v1/claims/create', { const response = await fetch('http://147.45.146.17:8100/api/v1/claims/create', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -166,22 +165,7 @@ export default function ClaimForm() {
const steps = useMemo(() => { const steps = useMemo(() => {
const stepsArray: any[] = []; const stepsArray: any[] = [];
// Шаг 1: Подтверждение телефона (всегда) // Шаг 1: Policy (всегда)
stepsArray.push({
title: 'Телефон',
description: 'Подтверждение по SMS',
content: (
<Step1Phone
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
});
// Шаг 2: Policy (всегда)
stepsArray.push({ stepsArray.push({
title: 'Проверка полиса', title: 'Проверка полиса',
description: 'Полис ERV', description: 'Полис ERV',
@@ -195,7 +179,7 @@ export default function ClaimForm() {
), ),
}); });
// Шаг 3: Event Type Selection (всегда) // Шаг 2: Event Type Selection (всегда)
stepsArray.push({ stepsArray.push({
title: 'Тип события', title: 'Тип события',
description: 'Выбор случая', description: 'Выбор случая',
@@ -209,7 +193,7 @@ export default function ClaimForm() {
), ),
}); });
// Шаги 4+: Document Upload (динамически, если выбран eventType) // Шаги 3+: Document Upload (динамически, если выбран eventType)
if (formData.eventType && documentConfigs.length > 0) { if (formData.eventType && documentConfigs.length > 0) {
documentConfigs.forEach((docConfig, index) => { documentConfigs.forEach((docConfig, index) => {
stepsArray.push({ stepsArray.push({