refactor: Изменен порядок формы - проверка полиса на первом шаге

Изменения в UX:
- Шаг 1: Проверка полиса (было: телефон + SMS)
- Шаг 2: Детали происшествия (без изменений)
- Шаг 3: Телефон + SMS + Выплата (было: только выплата)

Обновленные компоненты:
- Удален: Step1Phone.tsx
- Создан: Step1Policy.tsx - проверка полиса через API
- Обновлен: Step3Payment.tsx - добавлена SMS верификация
- Обновлен: ClaimForm.tsx - новая структура шагов

Логика: сначала проверяем полис, потом детали, в конце верификация телефона и выплата
This commit is contained in:
AI Assistant
2025-10-24 20:40:44 +03:00
parent 8b0bd156bb
commit cfd84e0f9d
5 changed files with 375 additions and 266 deletions

View File

@@ -420,7 +420,7 @@ pip install python-multipart==0.0.20
- **API endpoints:** 8
- **Сервисов интегрировано:** 6 (PostgreSQL, Redis, RabbitMQ, MySQL, OCR, SMS)
- **Проблем решено:** 9 критических
- **Коммитов:** 0 (еще не закоммитили!)
- **Коммитов:** 3 (последний: `8b0bd15` - перезапуск платформы)
---
@@ -501,8 +501,24 @@ docker ps | grep frontend
---
## 📦 Git Commits
### Коммит #3: `8b0bd15` - Перезапуск платформы
**Дата:** 24 октября 2025
**Сообщение:** fix: Перезапуск платформы - исправлены зависимости и TypeScript ошибки
**Изменения:**
- 9 файлов изменено (+918 / -134 строк)
- Новые файлы: SESSION_LOG, policy.py, upload.py, policy_service.py
- Обновлены: requirements.txt, Step3Payment.tsx, config.py, main.py, sms_service.py
**Статус:** ✅ Успешно запушено в origin/main
**Gitea:** http://147.45.146.17:3002/negodiy/erv-platform/commit/8b0bd15
---
*Документ создан: 24 октября 2025*
*Последнее обновление: 24 октября 2025 (перезапуск сервисов)*
*Последнее обновление: 24 октября 2025 (git commit + перезапуск)*
*Платформа: ERV Insurance Platform v1.0.0*
*Tech Stack: Python FastAPI + React TypeScript + PostgreSQL + Redis + RabbitMQ*

View File

@@ -1,199 +0,0 @@
import { useState } from 'react';
import { Form, Input, Button, message, Space } from 'antd';
import { PhoneOutlined, SafetyOutlined, FileProtectOutlined } from '@ant-design/icons';
interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
isPhoneVerified: boolean;
setIsPhoneVerified: (verified: boolean) => void;
}
export default function Step1Phone({ formData, updateFormData, onNext, isPhoneVerified, setIsPhoneVerified }: Props) {
const [form] = Form.useForm();
const [codeSent, setCodeSent] = useState(false);
const [loading, setLoading] = useState(false);
const [verifyLoading, setVerifyLoading] = useState(false);
const sendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('Введите номер телефона');
return;
}
setLoading(true);
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) {
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
if (result.debug_code) {
message.info(`DEBUG: Код ${result.debug_code}`);
}
} else {
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);
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) {
message.success('Телефон подтвержден!');
setIsPhoneVerified(true);
} else {
message.error(result.detail || 'Неверный код');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setVerifyLoading(false);
}
};
const handleNext = async () => {
try {
const values = await form.validateFields();
updateFormData(values);
onNext();
} catch (error) {
message.error('Заполните все обязательные поля');
}
};
return (
<Form
form={form}
layout="vertical"
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Номер телефона"
name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="+79001234567"
disabled={isPhoneVerified}
maxLength={12}
/>
</Form.Item>
{!isPhoneVerified && (
<>
<Form.Item>
<Button
type="primary"
onClick={sendCode}
loading={loading}
disabled={codeSent}
>
{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%' }}
/>
<Button
type="primary"
onClick={verifyCode}
loading={verifyLoading}
style={{ width: '30%' }}
>
Проверить
</Button>
</Space.Compact>
</Form.Item>
)}
</>
)}
{isPhoneVerified && (
<>
<Form.Item
label="Email (необязательно)"
name="email"
rules={[{ type: 'email', message: 'Неверный формат email' }]}
>
<Input placeholder="example@mail.ru" />
</Form.Item>
<Form.Item
label="ИНН (необязательно)"
name="inn"
>
<Input placeholder="1234567890" maxLength={12} />
</Form.Item>
<Form.Item
label="Номер полиса"
name="policyNumber"
rules={[{ required: true, message: 'Введите номер полиса' }]}
>
<Input prefix={<FileProtectOutlined />} placeholder="123456789" />
</Form.Item>
<Form.Item
label="Серия полиса (необязательно)"
name="policySeries"
>
<Input placeholder="AB" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleNext} size="large" block>
Далее
</Button>
</Form.Item>
</>
)}
</Form>
);
}

View File

@@ -0,0 +1,118 @@
import { useState } from 'react';
import { Form, Input, Button, message } from 'antd';
import { FileProtectOutlined } from '@ant-design/icons';
interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
}
export default function Step1Policy({ formData, updateFormData, onNext }: Props) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const checkPolicy = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// Проверка полиса через API
const response = await fetch('http://147.45.146.17:8100/api/v1/policy/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
policy_number: values.policyNumber,
policy_series: values.policySeries,
inn: values.inn,
}),
});
const result = await response.json();
if (response.ok) {
if (result.found) {
message.success(`Полис найден! ${result.holder_name}`);
updateFormData(values);
onNext();
} else {
message.warning('Полис не найден. Загрузите скан полиса на следующем шаге.');
updateFormData(values);
onNext();
}
} else {
message.error(result.detail || 'Ошибка проверки полиса');
}
} catch (error: any) {
if (error.errorFields) {
message.error('Заполните все обязательные поля');
} else {
message.error('Ошибка соединения с сервером');
}
} finally {
setLoading(false);
}
};
return (
<Form
form={form}
layout="vertical"
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Номер полиса"
name="policyNumber"
rules={[{ required: true, message: 'Введите номер полиса' }]}
>
<Input
prefix={<FileProtectOutlined />}
placeholder="123456789"
size="large"
/>
</Form.Item>
<Form.Item
label="Серия полиса (необязательно)"
name="policySeries"
>
<Input placeholder="AB" size="large" />
</Form.Item>
<Form.Item
label="ИНН (необязательно)"
name="inn"
>
<Input placeholder="1234567890" maxLength={12} size="large" />
</Form.Item>
<Form.Item
label="Email (необязательно)"
name="email"
rules={[{ type: 'email', message: 'Неверный формат email' }]}
>
<Input placeholder="example@mail.ru" size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
onClick={checkPolicy}
loading={loading}
size="large"
block
>
Проверить полис и продолжить
</Button>
</Form.Item>
<div style={{ marginTop: 16, padding: 12, background: '#f0f9ff', borderRadius: 8 }}>
<p style={{ margin: 0, fontSize: 13, color: '#666' }}>
💡 Если полис не найден в базе, на следующем шаге вы сможете загрузить его скан
</p>
</div>
</Form>
);
}

View File

@@ -1,6 +1,6 @@
import { Form, Button, Select, message } from 'antd';
import { QrcodeOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { Form, Input, Button, Select, message, Space, Divider } from 'antd';
import { PhoneOutlined, SafetyOutlined, QrcodeOutlined } from '@ant-design/icons';
const { Option } = Select;
@@ -9,12 +9,89 @@ interface Props {
updateFormData: (data: any) => void;
onPrev: () => void;
onSubmit: () => void;
isPhoneVerified: boolean;
setIsPhoneVerified: (verified: boolean) => void;
}
export default function Step3Payment({ formData, updateFormData, onPrev, onSubmit }: Props) {
export default function Step3Payment({
formData,
updateFormData,
onPrev,
onSubmit,
isPhoneVerified,
setIsPhoneVerified
}: Props) {
const [form] = Form.useForm();
const [codeSent, setCodeSent] = useState(false);
const [loading, setLoading] = useState(false);
const [verifyLoading, setVerifyLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const sendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('Введите номер телефона');
return;
}
setLoading(true);
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) {
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
if (result.debug_code) {
message.info(`DEBUG: Код ${result.debug_code}`);
}
} else {
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);
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) {
message.success('Телефон подтвержден!');
setIsPhoneVerified(true);
} else {
message.error(result.detail || 'Неверный код');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setVerifyLoading(false);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
@@ -36,65 +113,162 @@ export default function Step3Payment({ formData, updateFormData, onPrev, onSubmi
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Способ выплаты"
name="paymentMethod"
initialValue="sbp"
>
<div style={{ padding: '12px', background: '#f0f9ff', borderRadius: '8px', border: '1px solid #91d5ff' }}>
<QrcodeOutlined style={{ fontSize: 20, color: '#1890ff', marginRight: 8 }} />
<strong>СБП (Система быстрых платежей)</strong>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: 13 }}>
Выплата поступит на ваш счет в течение нескольких минут
</p>
</div>
</Form.Item>
<Form.Item
label="Выберите ваш банк"
name="bankName"
rules={[{ required: true, message: 'Выберите банк для получения выплаты' }]}
>
<Select
placeholder="Выберите банк"
size="large"
showSearch
filterOption={(input: string, option: any) => {
const children = option?.children;
if (typeof children === 'string') {
return children.toLowerCase().includes(input.toLowerCase());
}
return false;
}}
{/* Блок верификации телефона */}
<div style={{
padding: 16,
background: '#f6f8fa',
borderRadius: 8,
marginBottom: 24
}}>
<h3 style={{ marginTop: 0 }}>📱 Подтверждение телефона</h3>
<Form.Item
label="Номер телефона"
name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
]}
>
<Option value="sberbank">🟢 Сбербанк</Option>
<Option value="tinkoff">🟡 Тинькофф</Option>
<Option value="vtb">🔵 ВТБ</Option>
<Option value="alfabank">🔴 Альфа-Банк</Option>
<Option value="raiffeisen">🟡 Райффайзенбанк</Option>
<Option value="gazprombank">🔵 Газпромбанк</Option>
<Option value="rosbank">🔴 Росбанк</Option>
<Option value="sovcombank">🟢 Совкомбанк</Option>
<Option value="otkritie">🔵 Открытие</Option>
<Option value="other">💳 Другой банк</Option>
</Select>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev}>Назад</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={submitting}
style={{ flex: 1 }}
<Input
prefix={<PhoneOutlined />}
placeholder="+79001234567"
disabled={isPhoneVerified}
maxLength={12}
size="large"
/>
</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 && (
<>
<Divider />
<h3>💳 Способ получения выплаты</h3>
<Form.Item
label="Способ выплаты"
name="paymentMethod"
initialValue="sbp"
>
Отправить заявку
</Button>
</div>
</Form.Item>
<div style={{
padding: '12px',
background: '#f0f9ff',
borderRadius: '8px',
border: '1px solid #91d5ff'
}}>
<QrcodeOutlined style={{ fontSize: 20, color: '#1890ff', marginRight: 8 }} />
<strong>СБП (Система быстрых платежей)</strong>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: 13 }}>
Выплата поступит на ваш счет в течение нескольких минут
</p>
</div>
</Form.Item>
<Form.Item
label="Выберите ваш банк"
name="bankName"
rules={[{ required: true, message: 'Выберите банк для получения выплаты' }]}
>
<Select
placeholder="Выберите банк"
size="large"
showSearch
filterOption={(input: string, option: any) => {
const children = option?.children;
if (typeof children === 'string') {
return children.toLowerCase().includes(input.toLowerCase());
}
return false;
}}
>
<Option value="sberbank">🟢 Сбербанк</Option>
<Option value="tinkoff">🟡 Тинькофф</Option>
<Option value="vtb">🔵 ВТБ</Option>
<Option value="alfabank">🔴 Альфа-Банк</Option>
<Option value="raiffeisen">🟡 Райффайзенбанк</Option>
<Option value="gazprombank">🔵 Газпромбанк</Option>
<Option value="rosbank">🔴 Росбанк</Option>
<Option value="sovcombank">🟢 Совкомбанк</Option>
<Option value="otkritie">🔵 Открытие</Option>
<Option value="other">💳 Другой банк</Option>
</Select>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev} size="large">Назад</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={submitting}
style={{ flex: 1 }}
size="large"
>
Отправить заявку
</Button>
</div>
</Form.Item>
</>
)}
</Form>
);
}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Steps, Card, message } from 'antd';
import Step1Phone from '../components/form/Step1Phone';
import Step1Policy from '../components/form/Step1Policy';
import Step2Details from '../components/form/Step2Details';
import Step3Payment from '../components/form/Step3Payment';
import './ClaimForm.css';
@@ -96,14 +96,12 @@ export default function ClaimForm() {
const steps = [
{
title: 'Телефон и полис',
title: 'Проверка полиса',
content: (
<Step1Phone
<Step1Policy
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
/>
),
},
@@ -119,13 +117,15 @@ export default function ClaimForm() {
),
},
{
title: 'Способ выплаты',
title: 'Телефон и выплата',
content: (
<Step3Payment
formData={formData}
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
/>
),
},