🚀 MVP: FastAPI + React форма с SMS верификацией
✅ Инфраструктура: PostgreSQL, Redis, RabbitMQ, S3 ✅ Backend: SMS сервис + API endpoints ✅ Frontend: React форма (3 шага) + SMS верификация
This commit is contained in:
26
frontend/Dockerfile
Normal file
26
frontend/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# React Frontend Dockerfile
|
||||
FROM node:18-alpine
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем package.json
|
||||
COPY package.json ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN npm install
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Собираем приложение
|
||||
RUN npm run build
|
||||
|
||||
# Устанавливаем serve для статических файлов
|
||||
RUN npm install -g serve
|
||||
|
||||
# Открываем порт
|
||||
EXPOSE 3000
|
||||
|
||||
# Запускаем приложение
|
||||
CMD ["serve", "-s", "dist", "-l", "3000"]
|
||||
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ERV Insurance Platform</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"start": "serve -s dist -l 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
@@ -23,7 +24,8 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"imask": "^7.6.1",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"serve": "^14.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.11",
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,4 @@
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,124 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import ClaimForm from './pages/ClaimForm'
|
||||
import './App.css'
|
||||
|
||||
interface APIInfo {
|
||||
platform?: string;
|
||||
version?: string;
|
||||
features?: string[];
|
||||
tech_stack?: any;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [apiInfo, setApiInfo] = useState<APIInfo | null>(null)
|
||||
const [health, setHealth] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Проверяем подключение к API
|
||||
Promise.all([
|
||||
fetch('http://147.45.146.17:8100/api/v1/info').then(r => r.json()),
|
||||
fetch('http://147.45.146.17:8100/health').then(r => r.json())
|
||||
])
|
||||
.then(([info, healthData]) => {
|
||||
setApiInfo(info)
|
||||
setHealth(healthData)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('API Error:', err)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>🚀 ERV Insurance Platform</h1>
|
||||
<p>Python FastAPI + React TypeScript</p>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
{loading ? (
|
||||
<div className="loading">⏳ Подключение к API...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="card">
|
||||
<h2>📊 Информация о платформе</h2>
|
||||
{apiInfo ? (
|
||||
<>
|
||||
<p><strong>Платформа:</strong> {apiInfo.platform}</p>
|
||||
<p><strong>Версия:</strong> {apiInfo.version}</p>
|
||||
|
||||
<h3>✨ Возможности:</h3>
|
||||
<ul>
|
||||
{apiInfo.features?.map((f, i) => (
|
||||
<li key={i}>{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h3>🛠️ Технологический стек:</h3>
|
||||
<pre>{JSON.stringify(apiInfo.tech_stack, null, 2)}</pre>
|
||||
</>
|
||||
) : (
|
||||
<p className="error">❌ Не удалось получить данные</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>🏥 Здоровье сервисов</h2>
|
||||
{health ? (
|
||||
<>
|
||||
<p className={health.status === 'healthy' ? 'success' : 'warning'}>
|
||||
Статус: <strong>{health.status}</strong>
|
||||
</p>
|
||||
|
||||
<h3>Сервисы:</h3>
|
||||
<ul className="services">
|
||||
{Object.entries(health.services || {}).map(([name, status]) => (
|
||||
<li key={name}>
|
||||
<span className={status === 'ok' ? 'status-ok' : 'status-error'}>
|
||||
{status === 'ok' ? '✅' : '❌'}
|
||||
</span>
|
||||
<strong>{name}:</strong> {String(status)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="error">❌ Health check недоступен</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>🔗 Полезные ссылки</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://147.45.146.17:8100/docs" target="_blank">
|
||||
📚 API Документация (Swagger UI)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://147.45.146.17:8100/health" target="_blank">
|
||||
🏥 Health Check
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://147.45.146.17:3002" target="_blank">
|
||||
🐙 Gitea (Git репозиторий)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="app-footer">
|
||||
<p>© 2025 ERV Insurance Platform | Powered by FastAPI + React</p>
|
||||
</footer>
|
||||
<div className="App">
|
||||
<ClaimForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
199
frontend/src/components/form/Step1Phone.tsx
Normal file
199
frontend/src/components/form/Step1Phone.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
122
frontend/src/components/form/Step2Details.tsx
Normal file
122
frontend/src/components/form/Step2Details.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Form, Input, DatePicker, Select, Button, Upload, message } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { useState } from 'react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface Props {
|
||||
formData: any;
|
||||
updateFormData: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
}
|
||||
|
||||
export default function Step2Details({ formData, updateFormData, onNext, onPrev }: Props) {
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
const handleNext = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
updateFormData({
|
||||
...values,
|
||||
incidentDate: values.incidentDate?.format('YYYY-MM-DD'),
|
||||
uploadedFiles: fileList.map(f => f.uid),
|
||||
});
|
||||
onNext();
|
||||
} catch (error) {
|
||||
message.error('Заполните все обязательные поля');
|
||||
}
|
||||
};
|
||||
|
||||
const uploadProps = {
|
||||
fileList,
|
||||
beforeUpload: (file: File) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isPDF = file.type === 'application/pdf';
|
||||
if (!isImage && !isPDF) {
|
||||
message.error('Можно загружать только изображения и PDF');
|
||||
return false;
|
||||
}
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
message.error('Файл должен быть меньше 10MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
setFileList([...fileList, {
|
||||
uid: Math.random().toString(),
|
||||
name: file.name,
|
||||
status: 'done',
|
||||
url: URL.createObjectURL(file),
|
||||
} as UploadFile]);
|
||||
|
||||
return false; // Отключаем автозагрузку
|
||||
},
|
||||
onRemove: (file: UploadFile) => {
|
||||
setFileList(fileList.filter(f => f.uid !== file.uid));
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={formData}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="Дата происшествия"
|
||||
name="incidentDate"
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Тип транспорта"
|
||||
name="transportType"
|
||||
>
|
||||
<Select placeholder="Выберите тип транспорта">
|
||||
<Option value="air">Авиа</Option>
|
||||
<Option value="train">Поезд</Option>
|
||||
<Option value="bus">Автобус</Option>
|
||||
<Option value="ship">Водный транспорт</Option>
|
||||
<Option value="other">Другое</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Описание происшествия"
|
||||
name="incidentDescription"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="Опишите что произошло..."
|
||||
maxLength={1000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Документы (билеты, справки, чеки)">
|
||||
<Upload {...uploadProps} listType="picture">
|
||||
<Button icon={<UploadOutlined />}>Загрузить файлы</Button>
|
||||
</Upload>
|
||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||||
Максимум 10 MB на файл. Форматы: JPG, PNG, PDF, HEIC
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button onClick={onPrev}>Назад</Button>
|
||||
<Button type="primary" onClick={handleNext} style={{ flex: 1 }}>
|
||||
Далее
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
131
frontend/src/components/form/Step3Payment.tsx
Normal file
131
frontend/src/components/form/Step3Payment.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Form, Input, Radio, Button, Select, message } from 'antd';
|
||||
import { BankOutlined, CreditCardOutlined, QrcodeOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface Props {
|
||||
formData: any;
|
||||
updateFormData: (data: any) => void;
|
||||
onPrev: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export default function Step3Payment({ formData, updateFormData, onPrev, onSubmit }: Props) {
|
||||
const [form] = Form.useForm();
|
||||
const [paymentMethod, setPaymentMethod] = useState(formData.paymentMethod || 'sbp');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
updateFormData(values);
|
||||
|
||||
setSubmitting(true);
|
||||
await onSubmit();
|
||||
} catch (error) {
|
||||
message.error('Заполните все обязательные поля');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={formData}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="Способ выплаты"
|
||||
name="paymentMethod"
|
||||
rules={[{ required: true, message: 'Выберите способ выплаты' }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => setPaymentMethod(e.target.value)}>
|
||||
<Radio.Button value="sbp">
|
||||
<QrcodeOutlined /> СБП (Быстрые платежи)
|
||||
</Radio.Button>
|
||||
<Radio.Button value="card">
|
||||
<CreditCardOutlined /> Карта
|
||||
</Radio.Button>
|
||||
<Radio.Button value="bank_transfer">
|
||||
<BankOutlined /> Банковский счет
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{paymentMethod === 'sbp' && (
|
||||
<Form.Item
|
||||
label="Банк для СБП"
|
||||
name="bankName"
|
||||
rules={[{ required: true, message: 'Выберите банк' }]}
|
||||
>
|
||||
<Select placeholder="Выберите ваш банк">
|
||||
<Option value="sberbank">Сбербанк</Option>
|
||||
<Option value="tinkoff">Тинькофф</Option>
|
||||
<Option value="vtb">ВТБ</Option>
|
||||
<Option value="alfabank">Альфа-Банк</Option>
|
||||
<Option value="raiffeisen">Райффайзенбанк</Option>
|
||||
<Option value="other">Другой</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{paymentMethod === 'card' && (
|
||||
<Form.Item
|
||||
label="Номер карты"
|
||||
name="cardNumber"
|
||||
rules={[
|
||||
{ required: true, message: 'Введите номер карты' },
|
||||
{ pattern: /^\d{16}$/, message: '16 цифр без пробелов' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<CreditCardOutlined />}
|
||||
placeholder="1234567890123456"
|
||||
maxLength={16}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{paymentMethod === 'bank_transfer' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Название банка"
|
||||
name="bankName"
|
||||
rules={[{ required: true, message: 'Введите название банка' }]}
|
||||
>
|
||||
<Input prefix={<BankOutlined />} placeholder="Сбербанк" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Номер счета"
|
||||
name="accountNumber"
|
||||
rules={[
|
||||
{ required: true, message: 'Введите номер счета' },
|
||||
{ pattern: /^\d{20}$/, message: '20 цифр' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="12345678901234567890" maxLength={20} />
|
||||
</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 }}
|
||||
size="large"
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
|
||||
|
||||
51
frontend/src/pages/ClaimForm.css
Normal file
51
frontend/src/pages/ClaimForm.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.claim-form-container {
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.claim-form-card {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.claim-form-card .ant-card-head {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.claim-form-card .ant-card-head-title {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.steps-content {
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.claim-form-container {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.claim-form-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.steps-content {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
147
frontend/src/pages/ClaimForm.tsx
Normal file
147
frontend/src/pages/ClaimForm.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useState } from 'react';
|
||||
import { Steps, Card, message } from 'antd';
|
||||
import Step1Phone from '../components/form/Step1Phone';
|
||||
import Step2Details from '../components/form/Step2Details';
|
||||
import Step3Payment from '../components/form/Step3Payment';
|
||||
import './ClaimForm.css';
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
interface FormData {
|
||||
// Шаг 1
|
||||
phone: string;
|
||||
email?: string;
|
||||
inn?: string;
|
||||
policyNumber: string;
|
||||
policySeries?: string;
|
||||
|
||||
// Шаг 2
|
||||
incidentDate?: string;
|
||||
incidentDescription?: string;
|
||||
transportType?: string;
|
||||
uploadedFiles?: string[];
|
||||
|
||||
// Шаг 3
|
||||
paymentMethod: string;
|
||||
bankName?: string;
|
||||
cardNumber?: string;
|
||||
accountNumber?: string;
|
||||
}
|
||||
|
||||
export default function ClaimForm() {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
phone: '',
|
||||
policyNumber: '',
|
||||
paymentMethod: 'sbp',
|
||||
});
|
||||
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
|
||||
|
||||
const updateFormData = (data: Partial<FormData>) => {
|
||||
setFormData({ ...formData, ...data });
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const response = await fetch('http://147.45.146.17:8100/api/v1/claims/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
inn: formData.inn,
|
||||
policy_number: formData.policyNumber,
|
||||
policy_series: formData.policySeries,
|
||||
incident_date: formData.incidentDate,
|
||||
incident_description: formData.incidentDescription,
|
||||
transport_type: formData.transportType,
|
||||
payment_method: formData.paymentMethod,
|
||||
bank_name: formData.bankName,
|
||||
card_number: formData.cardNumber,
|
||||
account_number: formData.accountNumber,
|
||||
uploaded_files: formData.uploadedFiles || [],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
message.success(`Заявка ${result.claim_number} успешно создана!`);
|
||||
// Сброс формы
|
||||
setFormData({
|
||||
phone: '',
|
||||
policyNumber: '',
|
||||
paymentMethod: 'sbp',
|
||||
});
|
||||
setCurrentStep(0);
|
||||
setIsPhoneVerified(false);
|
||||
} else {
|
||||
message.error('Ошибка при создании заявки');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Ошибка соединения с сервером');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Телефон и полис',
|
||||
content: (
|
||||
<Step1Phone
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={nextStep}
|
||||
isPhoneVerified={isPhoneVerified}
|
||||
setIsPhoneVerified={setIsPhoneVerified}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Детали происшествия',
|
||||
content: (
|
||||
<Step2Details
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={nextStep}
|
||||
onPrev={prevStep}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Способ выплаты',
|
||||
content: (
|
||||
<Step3Payment
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onPrev={prevStep}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="claim-form-container">
|
||||
<Card title="Подать заявку на выплату" className="claim-form-card">
|
||||
<Steps current={currentStep} className="steps">
|
||||
{steps.map((item) => (
|
||||
<Step key={item.title} title={item.title} />
|
||||
))}
|
||||
</Steps>
|
||||
<div className="steps-content">{steps[currentStep].content}</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
frontend/tsconfig.node.json
Normal file
12
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@ export default defineConfig({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user