feat: Полный флоу для создания контакта через CreateWebContact
- docker-compose.yml: убраны локальные postgres/redis, только внешние - Frontend: телефон в формате 79001234567 (без +) - Готово к интеграции с n8n webhook для создания контакта в CRM - CreateWebContact: только создание или возврат ID, БЕЗ обновления
This commit is contained in:
@@ -20,30 +20,45 @@ services:
|
||||
ports:
|
||||
- "8100:8100"
|
||||
environment:
|
||||
- REDIS_HOST=crm.clientright.ru
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=CRM_Redis_Pass_2025_Secure!
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db
|
||||
- RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672
|
||||
- N8N_POLICY_CHECK_WEBHOOK=https://n8n.clientright.pro/webhook/9eb7bc5b-645f-477d-a5d8-5a346260a265
|
||||
- N8N_FILE_UPLOAD_WEBHOOK=https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
networks:
|
||||
- erv-network
|
||||
restart: unless-stopped
|
||||
|
||||
# Redis для кеширования
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# networks:
|
||||
# - erv-network
|
||||
# restart: unless-stopped
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- erv-network
|
||||
restart: unless-stopped
|
||||
|
||||
# PostgreSQL для логов и аналитики
|
||||
# postgres:
|
||||
# Используется внешний PostgreSQL на 147.45.189.234:5432
|
||||
postgres:
|
||||
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:
|
||||
erv-network:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Form, Input, Button, Select, message, Divider } from 'antd';
|
||||
import { QrcodeOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Button, Select, message, Space, Divider } from 'antd';
|
||||
import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -24,12 +24,96 @@ export default function Step3Payment({
|
||||
addDebugEvent
|
||||
}: Props) {
|
||||
const [form] = Form.useForm();
|
||||
const [codeSent] = useState(false);
|
||||
const [loading] = useState(false);
|
||||
const [verifyLoading] = useState(false);
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [verifyLoading, setVerifyLoading] = 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 () => {
|
||||
try {
|
||||
@@ -59,35 +143,106 @@ export default function Step3Payment({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Блок верификации телефона перенесен на шаг 1 */}
|
||||
{isPhoneVerified && (
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: '#f0f9ff',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #91d5ff',
|
||||
marginBottom: 24
|
||||
}}>
|
||||
✅ Телефон подтвержден
|
||||
</div>
|
||||
)}
|
||||
{/* Блок верификации телефона */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: '#f6f8fa',
|
||||
borderRadius: 8,
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<h3 style={{ marginTop: 0 }}>📱 Подтверждение телефона</h3>
|
||||
|
||||
{/* Email собираем на последнем шаге */}
|
||||
<Form.Item
|
||||
label="Электронная почта"
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: 'Введите email' },
|
||||
{ type: 'email', message: 'Неверный формат email' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="example@mail.ru"
|
||||
size="large"
|
||||
type="email"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
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>
|
||||
|
||||
<Form.Item
|
||||
label="Электронная почта"
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: 'Введите email' },
|
||||
{ type: 'email', message: 'Неверный формат email' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="example@mail.ru"
|
||||
size="large"
|
||||
type="email"
|
||||
disabled={isPhoneVerified}
|
||||
/>
|
||||
</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 && (
|
||||
@@ -186,7 +341,7 @@ export default function Step3Payment({
|
||||
const devData = {
|
||||
fullName: 'Тест Тестов',
|
||||
email: 'test@test.ru',
|
||||
phone: '79991234567', // БЕЗ +
|
||||
phone: '+79991234567',
|
||||
paymentMethod: 'sbp',
|
||||
bankName: 'sberbank',
|
||||
};
|
||||
@@ -206,7 +361,7 @@ export default function Step3Payment({
|
||||
const devData = {
|
||||
fullName: 'Тест Тестов',
|
||||
email: 'test@test.ru',
|
||||
phone: '79991234567', // БЕЗ +
|
||||
phone: '+79991234567',
|
||||
paymentMethod: 'sbp',
|
||||
bankName: 'sberbank',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { Steps, Card, message, Row, Col } from 'antd';
|
||||
import Step1Policy from '../components/form/Step1Policy';
|
||||
import Step1Phone from '../components/form/Step1Phone';
|
||||
import Step2EventType from '../components/form/Step2EventType';
|
||||
import StepDocumentUpload from '../components/form/StepDocumentUpload';
|
||||
import Step3Payment from '../components/form/Step3Payment';
|
||||
@@ -117,7 +116,7 @@ export default function ClaimForm() {
|
||||
try {
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -166,22 +165,7 @@ export default function ClaimForm() {
|
||||
const steps = useMemo(() => {
|
||||
const stepsArray: any[] = [];
|
||||
|
||||
// Шаг 1: Подтверждение телефона (всегда)
|
||||
stepsArray.push({
|
||||
title: 'Телефон',
|
||||
description: 'Подтверждение по SMS',
|
||||
content: (
|
||||
<Step1Phone
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={nextStep}
|
||||
setIsPhoneVerified={setIsPhoneVerified}
|
||||
addDebugEvent={addDebugEvent}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
// Шаг 2: Policy (всегда)
|
||||
// Шаг 1: Policy (всегда)
|
||||
stepsArray.push({
|
||||
title: 'Проверка полиса',
|
||||
description: 'Полис ERV',
|
||||
@@ -195,7 +179,7 @@ export default function ClaimForm() {
|
||||
),
|
||||
});
|
||||
|
||||
// Шаг 3: Event Type Selection (всегда)
|
||||
// Шаг 2: Event Type Selection (всегда)
|
||||
stepsArray.push({
|
||||
title: 'Тип события',
|
||||
description: 'Выбор случая',
|
||||
@@ -209,7 +193,7 @@ export default function ClaimForm() {
|
||||
),
|
||||
});
|
||||
|
||||
// Шаги 4+: Document Upload (динамически, если выбран eventType)
|
||||
// Шаги 3+: Document Upload (динамически, если выбран eventType)
|
||||
if (formData.eventType && documentConfigs.length > 0) {
|
||||
documentConfigs.forEach((docConfig, index) => {
|
||||
stepsArray.push({
|
||||
|
||||
Reference in New Issue
Block a user