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
This commit is contained in:
AI Assistant
2025-10-24 21:12:30 +03:00
parent 3b08916c22
commit f2cfa54c9d
2 changed files with 163 additions and 37 deletions

View File

@@ -32,8 +32,8 @@ async def check_policy(request: PolicyCheckRequest):
return {
"success": True,
"found": True,
"message": "Полис найден в базе",
"policy_data": policy
"message": "Полис найден в базе"
# policy_data не отдаем (для продакшна)
}
else:
return {

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { Form, Input, Button, message } from 'antd';
import { FileProtectOutlined, MailOutlined } from '@ant-design/icons';
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;
@@ -8,34 +9,73 @@ interface Props {
onNext: () => void;
}
// Функция автозамены кириллицы на латиницу
// Расширенная функция автозамены кириллицы на латиницу
const cyrillicToLatin = (text: string): string => {
const map: Record<string, string> = {
'А': 'A', 'В': 'B', 'С': 'C', 'Е': 'E', 'Н': 'H', 'К': 'K',
'М': 'M', 'О': 'O', 'Р': 'P', 'Т': 'T', 'Х': 'X',
'а': 'a', 'в': 'b', 'с': 'c', 'е': 'e', 'н': 'h', 'к': 'k',
'м': 'm', 'о': 'o', 'р': 'p', 'т': 't', 'х': 'x'
'А': '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 value = e.target.value;
const converted = cyrillicToLatin(value.toUpperCase());
form.setFieldValue('voucher', converted);
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();
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',
@@ -50,13 +90,14 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
if (response.ok) {
if (result.found) {
message.success(`Полис найден! Владелец: ${result.policy_data?.holder_name || 'не указан'}`);
// Полис найден - переходим дальше
message.success('Полис найден в базе данных');
updateFormData(values);
onNext();
} else {
message.warning('Полис не найден в базе. Продолжайте — на следующем шаге загрузите скан.');
updateFormData(values);
onNext();
// Полис НЕ найден - показываем загрузку скана
message.warning('Полис не найден в базе. Загрузите скан полиса');
setPolicyNotFound(true);
}
} else {
message.error(result.detail || 'Ошибка проверки полиса');
@@ -72,6 +113,26 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
}
};
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}
@@ -86,16 +147,17 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
{ required: true, message: 'Введите номер полиса' },
{
pattern: /^[A-Z]\d{4}-\d{9}$/,
message: 'Формат: E1000-302538524 (буква, 4 цифры, тире, 9 цифр)'
message: 'Формат: E1000-302538524'
}
]}
tooltip="Формат: E1000-302538524. Кириллица автоматически заменяется на латиницу"
tooltip="Формат: E1000-302538524. Тире вставляется автоматически"
>
<Input
prefix={<FileProtectOutlined />}
placeholder="E1000-302538524"
placeholder="E1000302538524"
size="large"
onChange={handleVoucherChange}
onPaste={handleVoucherPaste}
maxLength={15}
/>
</Form.Item>
@@ -116,23 +178,87 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
onClick={checkPolicy}
loading={loading}
size="large"
block
>
Проверить полис и продолжить
</Button>
</Form.Item>
{!policyNotFound && (
<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>
{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>
);
}