Files
aiform_prod/frontend/src/pages/ClaimForm.tsx

178 lines
4.6 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { Steps, Card, message } from 'antd';
import Step1Policy from '../components/form/Step1Policy';
import Step2Details from '../components/form/Step2Details';
import Step3Payment from '../components/form/Step3Payment';
import './ClaimForm.css';
const { Step } = Steps;
interface FormData {
// Шаг 1
voucher: string; // Полис вида E1000-302538524
email: string; // Email обязателен
// Шаг 2
incidentDate?: string;
incidentDescription?: string;
transportType?: string;
uploadedFiles?: string[];
// Шаг 3
phone: string;
paymentMethod: string;
bankName?: string;
cardNumber?: string;
accountNumber?: string;
}
export default function ClaimForm() {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<FormData>({
voucher: '',
email: '',
phone: '',
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({
voucher: formData.voucher,
email: formData.email,
phone: formData.phone,
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({
voucher: '',
email: '',
phone: '',
paymentMethod: 'sbp',
});
setCurrentStep(0);
setIsPhoneVerified(false);
} else {
message.error('Ошибка при создании заявки');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
console.error(error);
}
};
const steps = [
{
title: 'Проверка полиса',
content: (
<Step1Policy
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
/>
),
},
{
title: 'Детали происшествия',
content: (
<Step2Details
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
onPrev={prevStep}
/>
),
},
{
title: 'Телефон и выплата',
content: (
<Step3Payment
formData={formData}
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
/>
),
},
];
feat: 5 улучшений безопасности и UX 1. ✅ Прогресс бар загрузки: - Upload компонент с showUploadList - Кнопка показывает состояние 'Загрузка...' - Визуальный прогресс для каждого файла 2. ✅ OCR проверка полиса (заготовка): - TODO: проверка что загружен полис, а не шляпа - Если шляпа - помечаем себе в policyValidationWarning - Пользователю не говорим (silent validation) 3. ✅ Лимиты файлов: - Максимум 10 файлов - Каждый файл до 15MB - Валидация на фронте и бэкенде - Счетчик: 'Загружено: X/10 файлов' - Кнопка disabled при 10 файлах 4. ✅ Защита от инъекций и безопасность: Backend (upload.py): - Лимит файлов: if len(files) > 10 - Проверка размера: if len(content) > MAX_FILE_SIZE - Валидация типа: allowed_types = ['image/', 'application/pdf'] - Санитизация folder: allowed_folders whitelist Backend (draft.py): - Валидация session_id (max 255 chars) - Валидация step: only [1, 2, 3] - Параметризованные SQL запросы (защита от SQL injection) Frontend: - beforeUpload валидация размера - maxCount={10} - accept только разрешенные форматы 5. ✅ Кнопка 'Начать заново': - Показывается на шаге 2 и 3 (extra в Card) - Сбрасывает всю форму - Возвращает на шаг 1 - Очищает isPhoneVerified Безопасность: - SQL инъекции: параметризованные запросы ($1, $2) - XSS: Pydantic валидация всех inputs - File upload: type + size validation - Path traversal: folder whitelist - Rate limiting: TODO (Redis) UX: - Прогресс загрузки виден - Понятные лимиты (10 файлов по 15MB) - Возможность начать заново в любой момент
2025-10-24 21:34:50 +03:00
const handleReset = () => {
setFormData({
voucher: '',
email: '',
phone: '',
paymentMethod: 'sbp',
});
setCurrentStep(0);
setIsPhoneVerified(false);
message.info('Форма сброшена');
};
return (
<div className="claim-form-container">
feat: 5 улучшений безопасности и UX 1. ✅ Прогресс бар загрузки: - Upload компонент с showUploadList - Кнопка показывает состояние 'Загрузка...' - Визуальный прогресс для каждого файла 2. ✅ OCR проверка полиса (заготовка): - TODO: проверка что загружен полис, а не шляпа - Если шляпа - помечаем себе в policyValidationWarning - Пользователю не говорим (silent validation) 3. ✅ Лимиты файлов: - Максимум 10 файлов - Каждый файл до 15MB - Валидация на фронте и бэкенде - Счетчик: 'Загружено: X/10 файлов' - Кнопка disabled при 10 файлах 4. ✅ Защита от инъекций и безопасность: Backend (upload.py): - Лимит файлов: if len(files) > 10 - Проверка размера: if len(content) > MAX_FILE_SIZE - Валидация типа: allowed_types = ['image/', 'application/pdf'] - Санитизация folder: allowed_folders whitelist Backend (draft.py): - Валидация session_id (max 255 chars) - Валидация step: only [1, 2, 3] - Параметризованные SQL запросы (защита от SQL injection) Frontend: - beforeUpload валидация размера - maxCount={10} - accept только разрешенные форматы 5. ✅ Кнопка 'Начать заново': - Показывается на шаге 2 и 3 (extra в Card) - Сбрасывает всю форму - Возвращает на шаг 1 - Очищает isPhoneVerified Безопасность: - SQL инъекции: параметризованные запросы ($1, $2) - XSS: Pydantic валидация всех inputs - File upload: type + size validation - Path traversal: folder whitelist - Rate limiting: TODO (Redis) UX: - Прогресс загрузки виден - Понятные лимиты (10 файлов по 15MB) - Возможность начать заново в любой момент
2025-10-24 21:34:50 +03:00
<Card
title="Подать заявку на выплату"
className="claim-form-card"
extra={
currentStep > 0 && (
<button
onClick={handleReset}
style={{
padding: '4px 12px',
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
🔄 Начать заново
</button>
)
}
>
<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>
);
}