diff --git a/backend/app/api/policy.py b/backend/app/api/policy.py index d498ba7..f1e8117 100644 --- a/backend/app/api/policy.py +++ b/backend/app/api/policy.py @@ -32,8 +32,8 @@ async def check_policy(request: PolicyCheckRequest): return { "success": True, "found": True, - "message": "Полис найден в базе", - "policy_data": policy + "message": "Полис найден в базе" + # policy_data не отдаем (для продакшна) } else: return { diff --git a/frontend/src/components/form/Step1Policy.tsx b/frontend/src/components/form/Step1Policy.tsx index 7a161a5..4fb00eb 100644 --- a/frontend/src/components/form/Step1Policy.tsx +++ b/frontend/src/components/form/Step1Policy.tsx @@ -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 = { - 'А': '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([]); - // Обработчик изменения поля полиса с автозаменой + // Обработчик изменения поля полиса с автозаменой и маской const handleVoucherChange = (e: React.ChangeEvent) => { - 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) => { + 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 (
} - placeholder="E1000-302538524" + placeholder="E1000302538524" size="large" onChange={handleVoucherChange} + onPaste={handleVoucherPaste} maxLength={15} /> @@ -116,23 +178,87 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props) /> - - - + {!policyNotFound && ( + + + + )} -
-

- 💡 Если полис не найден в базе, на следующем шаге вы сможете загрузить его скан -

-
+ {policyNotFound && ( + <> +
+

+ ⚠️ Полис не найден в базе данных +

+

+ Загрузите скан/фото полиса для продолжения +

+
+ + + false} + accept="image/*,.pdf" + maxCount={3} + > + + + + + +
+ + +
+
+ + )} + + {!policyNotFound && ( +
+

+ 💡 Введите номер полиса. Кириллица автоматически заменяется на латиницу, тире вставляется автоматически +

+
+ )}
); }