Files
aiform_prod/frontend/src/components/form/Step1Policy.tsx
AI Assistant f2cfa54c9d feat: Улучшена форма полиса - маска ввода и загрузка скана
Изменения в UX (Step1Policy):
 Автоматическая маска ввода E1000-302538524
   - Тире вставляется автоматически
   - Не нужно вводить вручную

 Расширенная автозамена кириллицы:
   - А→A, а→A, С→C, с→C, Е→E, е→E и т.д.
   - Поддержка строчных и заглавных

 Автоматический uppercase
   - Все буквы автоматически заглавные

 Логика при ненайденном полисе:
   - НЕ переходит на следующий шаг
   - Показывает поле загрузки скана прямо на месте
   - Кнопка "Продолжить со сканом"
   - Поддержка изображений и PDF

 Обработка paste:
   - Корректная обработка вставки текста
   - Применяются все правила форматирования

Backend (policy.py):
 Убран вывод holder_name (для продакшна)
   - API не возвращает персональные данные
   - Только found: true/false

Формат полиса:
Ввод: k78486489849494 или К7848-6489849494
Результат: K7848-648984949
2025-10-24 21:12:30 +03:00

265 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Form, Input, Button, message, Upload } from 'antd';
import { FileProtectOutlined, MailOutlined, UploadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
}
// Расширенная функция автозамены кириллицы на латиницу
const cyrillicToLatin = (text: string): string => {
const map: Record<string, string> = {
'А': 'A', 'а': 'A',
'В': 'B', 'в': 'B',
'С': 'C', 'с': 'C',
'Е': 'E', 'е': 'E',
'Н': 'H', 'н': 'H',
'К': 'K', 'к': 'K',
'М': 'M', 'м': 'M',
'О': 'O', 'о': 'O',
'Р': 'P', 'р': 'P',
'Т': 'T', 'т': 'T',
'Х': 'X', 'х': 'X',
'У': 'Y', 'у': 'Y'
};
return text.split('').map(char => map[char] || char).join('');
};
// Функция форматирования полиса с маской E1000-302538524
const formatVoucher = (value: string): string => {
// Удаляем все кроме букв и цифр
const cleaned = value.replace(/[^A-Za-z0-9]/g, '');
// Применяем автозамену кириллицы и uppercase
const latinUpper = cyrillicToLatin(cleaned).toUpperCase();
// Применяем маску: буква + 4 цифры + тире + 9 цифр
if (latinUpper.length <= 1) {
return latinUpper;
} else if (latinUpper.length <= 5) {
return latinUpper;
} else if (latinUpper.length <= 14) {
return latinUpper.slice(0, 5) + '-' + latinUpper.slice(5);
} else {
return latinUpper.slice(0, 5) + '-' + latinUpper.slice(5, 14);
}
};
export default function Step1Policy({ formData, updateFormData, onNext }: Props) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [policyNotFound, setPolicyNotFound] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
// Обработчик изменения поля полиса с автозаменой и маской
const handleVoucherChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatVoucher(e.target.value);
form.setFieldValue('voucher', formatted);
};
// Обработчик paste для корректной обработки вставки
const handleVoucherPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pastedText = e.clipboardData.getData('text');
const formatted = formatVoucher(pastedText);
form.setFieldValue('voucher', formatted);
};
const checkPolicy = async () => {
try {
const values = await form.validateFields(['voucher', 'email']);
setLoading(true);
setPolicyNotFound(false);
// Проверка полиса через 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({
voucher: values.voucher,
email: values.email,
}),
});
const result = await response.json();
if (response.ok) {
if (result.found) {
// Полис найден - переходим дальше
message.success('Полис найден в базе данных');
updateFormData(values);
onNext();
} else {
// Полис НЕ найден - показываем загрузку скана
message.warning('Полис не найден в базе. Загрузите скан полиса');
setPolicyNotFound(true);
}
} else {
message.error(result.detail || 'Ошибка проверки полиса');
}
} catch (error: any) {
if (error.errorFields) {
message.error('Заполните все обязательные поля');
} else {
message.error('Ошибка соединения с сервером');
}
} finally {
setLoading(false);
}
};
const handleUploadChange = ({ fileList: newFileList }: any) => {
setFileList(newFileList);
};
const handleSubmitWithScan = async () => {
if (fileList.length === 0) {
message.error('Загрузите скан полиса');
return;
}
try {
const values = await form.validateFields();
updateFormData({ ...values, policyScanUploaded: true, policyScanFiles: fileList });
message.success('Данные сохранены');
onNext();
} catch (error) {
message.error('Заполните все обязательные поля');
}
};
return (
<Form
form={form}
layout="vertical"
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Номер полиса"
name="voucher"
rules={[
{ required: true, message: 'Введите номер полиса' },
{
pattern: /^[A-Z]\d{4}-\d{9}$/,
message: 'Формат: E1000-302538524'
}
]}
tooltip="Формат: E1000-302538524. Тире вставляется автоматически"
>
<Input
prefix={<FileProtectOutlined />}
placeholder="E1000302538524"
size="large"
onChange={handleVoucherChange}
onPaste={handleVoucherPaste}
maxLength={15}
/>
</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"
/>
</Form.Item>
{!policyNotFound && (
<Form.Item>
<Button
type="primary"
onClick={checkPolicy}
loading={loading}
size="large"
block
>
Проверить полис и продолжить
</Button>
</Form.Item>
)}
{policyNotFound && (
<>
<div style={{
marginBottom: 16,
padding: 16,
background: '#fff7e6',
borderRadius: 8,
border: '1px solid #ffa940'
}}>
<p style={{ margin: 0, color: '#d46b08', fontWeight: 500 }}>
Полис не найден в базе данных
</p>
<p style={{ margin: '8px 0 0 0', fontSize: 13, color: '#666' }}>
Загрузите скан/фото полиса для продолжения
</p>
</div>
<Form.Item
label="Скан полиса"
name="policyScan"
rules={[{ required: true, message: 'Загрузите скан полиса' }]}
>
<Upload
listType="picture"
fileList={fileList}
onChange={handleUploadChange}
beforeUpload={() => false}
accept="image/*,.pdf"
maxCount={3}
>
<Button icon={<UploadOutlined />} size="large" block>
Выбрать файл (фото или PDF)
</Button>
</Upload>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', gap: 8 }}>
<Button
onClick={() => {
setPolicyNotFound(false);
setFileList([]);
}}
size="large"
>
Отмена
</Button>
<Button
type="primary"
onClick={handleSubmitWithScan}
size="large"
style={{ flex: 1 }}
>
Продолжить со сканом
</Button>
</div>
</Form.Item>
</>
)}
{!policyNotFound && (
<div style={{ marginTop: 16, padding: 12, background: '#f0f9ff', borderRadius: 8 }}>
<p style={{ margin: 0, fontSize: 13, color: '#666' }}>
💡 Введите номер полиса. Кириллица автоматически заменяется на латиницу, тире вставляется автоматически
</p>
</div>
)}
</Form>
);
}