🔧 Технические панели на всех шагах:
- Step 1: Кнопка пропуска валидации полиса → Step 2
- Step 2: Кнопки навигации Назад/Вперёд без валидации полей
- Step 3: Автоподтверждение телефона + быстрая отправка заявки
Теперь можно тестировать весь флоу без заполнения обязательных полей.
658 lines
27 KiB
TypeScript
658 lines
27 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
||
import { Form, Input, Button, message, Upload, Spin, Alert, Modal } from 'antd';
|
||
import { FileProtectOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||
import type { UploadFile } from 'antd/es/upload/interface';
|
||
import { convertToPDF } from '../../utils/pdfConverter';
|
||
|
||
interface Props {
|
||
formData: any;
|
||
updateFormData: (data: any) => void;
|
||
onNext: () => void;
|
||
addDebugEvent?: (type: string, status: string, message: string, data?: any) => 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, addDebugEvent }: Props) {
|
||
const [form] = Form.useForm();
|
||
const [loading, setLoading] = useState(false);
|
||
const [policyNotFound, setPolicyNotFound] = useState(false);
|
||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||
const [uploading, setUploading] = useState(false);
|
||
const [waitingForOcr, setWaitingForOcr] = useState(false); // ⬅️ НОВЫЙ state для ожидания SSE!
|
||
const [uploadProgress, setUploadProgress] = useState('');
|
||
const [, setOcrResult] = useState<any>(null);
|
||
const [ocrModalVisible, setOcrModalVisible] = useState(false); // ⬅️ Видимость модалки
|
||
const [ocrModalContent, setOcrModalContent] = useState<any>(null); // ⬅️ Контент модалки
|
||
const eventSourceRef = useRef<EventSource | null>(null);
|
||
|
||
// SSE подключение для получения результатов OCR/Vision
|
||
useEffect(() => {
|
||
const claimId = formData.claim_id;
|
||
if (!claimId || !waitingForOcr) {
|
||
console.log('🔍 SSE useEffect: условие не выполнено', { claimId, waitingForOcr });
|
||
return;
|
||
}
|
||
|
||
console.log('🔌 SSE: Открываю соединение к', `/events/${claimId}`);
|
||
|
||
// Открываем модалку с крутилкой
|
||
setOcrModalVisible(true);
|
||
setOcrModalContent('loading');
|
||
|
||
// Подключаемся к SSE для получения результатов OCR (через Vite proxy)
|
||
const eventSource = new EventSource(`/events/${claimId}`);
|
||
eventSourceRef.current = eventSource;
|
||
|
||
console.log('✅ SSE: EventSource создан');
|
||
|
||
eventSource.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
console.log('📨 SSE event received:', data);
|
||
|
||
if (data.event_type === 'ocr_completed') {
|
||
console.log('✅ SSE: Получил событие ocr_completed!', data);
|
||
|
||
setUploadProgress('');
|
||
setUploading(false);
|
||
setWaitingForOcr(false); // Останавливаем ожидание
|
||
setOcrResult(data);
|
||
|
||
// Обрабатываем формат от n8n: data.output.is_policy или data.is_valid_document
|
||
const aiOutput = data.data?.output || data.data;
|
||
const isValidPolicy = aiOutput?.is_policy === 'yes' || data.data?.is_valid_document === true;
|
||
|
||
// Обновляем содержимое модалки на результат (вместо крутилки)
|
||
setOcrModalContent({ success: isValidPolicy, data: aiOutput, message: data.message });
|
||
|
||
if (data.status === 'completed' || data.status === 'success') {
|
||
const policyNumber = aiOutput?.policy_number || 'неизвестно';
|
||
const holderName = aiOutput?.policyholder_full_name || '';
|
||
const insuredPersons = aiOutput?.insured_persons || [];
|
||
|
||
if (isValidPolicy) {
|
||
// ✅ Полис распознан - логируем в Debug Panel
|
||
addDebugEvent?.('ocr_ai_result', 'success', `✅ AI анализ завершён`, {
|
||
policy_number: policyNumber,
|
||
holder: holderName,
|
||
insured_persons: insuredPersons,
|
||
policy_period: aiOutput?.policy_period,
|
||
program_name: aiOutput?.program_name,
|
||
full_ai_output: aiOutput
|
||
});
|
||
|
||
// Сохраняем извлечённые AI данные
|
||
updateFormData({
|
||
policyAiData: aiOutput,
|
||
policyNumber: policyNumber,
|
||
holderName: holderName
|
||
});
|
||
} else {
|
||
// ❌ Не полис
|
||
addDebugEvent?.('ocr', 'error', '❌ Документ не является полисом ERV', aiOutput);
|
||
setFileList([]);
|
||
setPolicyNotFound(true);
|
||
}
|
||
} else {
|
||
// Ошибка обработки
|
||
addDebugEvent?.('ocr', 'error', data.message || 'Ошибка OCR', data.data);
|
||
setFileList([]);
|
||
setPolicyNotFound(true);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('SSE parse error:', error);
|
||
}
|
||
};
|
||
|
||
eventSource.onerror = (error) => {
|
||
console.error('❌ SSE connection error:', error);
|
||
console.error('SSE readyState:', eventSource.readyState);
|
||
|
||
// Не показываем ошибку если уже получили результат (backend закрыл SSE после успешной отправки)
|
||
setOcrModalContent((prev) => {
|
||
if (prev && prev !== 'loading') {
|
||
console.log('✅ SSE закрыто после получения результата, не показываем ошибку');
|
||
return prev; // Оставляем текущий результат
|
||
}
|
||
return { success: false, data: null, message: 'Ошибка подключения к серверу' };
|
||
});
|
||
|
||
setWaitingForOcr(false);
|
||
eventSource.close();
|
||
};
|
||
|
||
eventSource.onopen = () => {
|
||
console.log('✅ SSE: Соединение открыто!');
|
||
};
|
||
|
||
return () => {
|
||
if (eventSourceRef.current) {
|
||
eventSourceRef.current.close();
|
||
eventSourceRef.current = null;
|
||
}
|
||
};
|
||
}, [formData.claim_id, waitingForOcr]);
|
||
|
||
// Обработчик изменения поля полиса с автозаменой и маской
|
||
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']);
|
||
|
||
setLoading(true);
|
||
setPolicyNotFound(false);
|
||
|
||
addDebugEvent?.('policy_check', 'pending', `Проверяю полис: ${values.voucher}`, { voucher: values.voucher });
|
||
|
||
// Проверка полиса через n8n вебхук + создание записи в БД
|
||
const response = await fetch('https://n8n.clientright.pro/webhook/9eb7bc5b-645f-477d-a5d8-5a346260a265', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
claim_id: formData.claim_id, // Передаём claim_id для создания записи
|
||
policy_number: values.voucher,
|
||
session_id: sessionStorage.getItem('session_id') || 'unknown'
|
||
}),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
// Новый формат ответа от n8n: {claim: {...}, policy: {...}}
|
||
const policyFound = result.policy?.found === 1 || result.policy?.found === true;
|
||
|
||
if (policyFound) {
|
||
// Полис найден - переходим дальше
|
||
addDebugEvent?.('policy_check', 'success', `✅ Полис найден в MySQL БД`, {
|
||
found: true,
|
||
claim: result.claim,
|
||
policy: result.policy,
|
||
voucher: values.voucher
|
||
});
|
||
message.success(`Полис найден: ${result.policy.voucher}. Застрахованных: ${result.policy.count} чел.`);
|
||
updateFormData(values);
|
||
onNext();
|
||
} else {
|
||
// Полис НЕ найден - показываем загрузку скана
|
||
addDebugEvent?.('policy_check', 'warning', `▲ Полис не найден → требуется загрузка скана`, {
|
||
found: false,
|
||
claim: result.claim,
|
||
message: result.policy?.message || 'Полис не найден',
|
||
voucher: values.voucher
|
||
});
|
||
message.warning('Полис не найден в базе. Загрузите скан полиса');
|
||
setPolicyNotFound(true);
|
||
}
|
||
} else {
|
||
addDebugEvent?.('policy_check', 'error', `❌ Ошибка API: ${result.detail}`, { error: result.detail });
|
||
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);
|
||
};
|
||
|
||
// OCR теперь обрабатывается в n8n (через RabbitMQ + Redis Pub/Sub)
|
||
// Polling не нужен!
|
||
|
||
const handleSubmitWithScan = async () => {
|
||
if (fileList.length === 0) {
|
||
message.error('Загрузите скан полиса');
|
||
return;
|
||
}
|
||
|
||
if (fileList.length > 10) {
|
||
message.error('Максимум 10 файлов');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setUploading(true);
|
||
setUploadProgress('📤 Подготавливаем документы...');
|
||
const values = await form.validateFields(['voucher']);
|
||
|
||
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} файл(ов) в S3 через n8n...`, {
|
||
count: fileList.length
|
||
});
|
||
|
||
// Генерируем claim_id если его нет
|
||
const claimId = formData.claim_id || `CLM-${new Date().toISOString().split('T')[0]}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||
|
||
// Загружаем каждый файл через n8n вебхук
|
||
const uploadedFiles = [];
|
||
|
||
for (let i = 0; i < fileList.length; i++) {
|
||
const file = fileList[i];
|
||
if (!file.originFileObj) continue;
|
||
|
||
// 🔄 Конвертируем в PDF перед отправкой
|
||
let pdfFile: File;
|
||
try {
|
||
setUploadProgress(`🔄 Конвертируем ${file.name} в PDF...`);
|
||
addDebugEvent?.('convert', 'pending', `🔄 Конвертирую ${file.name} в PDF...`, {
|
||
original_size: `${(file.originFileObj.size / 1024 / 1024).toFixed(2)} MB`,
|
||
original_type: file.originFileObj.type
|
||
});
|
||
|
||
pdfFile = await convertToPDF(file.originFileObj);
|
||
|
||
addDebugEvent?.('convert', 'success', `✅ PDF готов: ${pdfFile.name}`, {
|
||
pdf_size: `${(pdfFile.size / 1024 / 1024).toFixed(2)} MB`
|
||
});
|
||
} catch (error: any) {
|
||
addDebugEvent?.('convert', 'error', `❌ Ошибка конвертации: ${error.message}`);
|
||
message.error('Ошибка конвертации файла');
|
||
continue;
|
||
}
|
||
|
||
const uploadFormData = new FormData();
|
||
uploadFormData.append('claim_id', claimId);
|
||
uploadFormData.append('file_type', 'policy_scan');
|
||
uploadFormData.append('filename', pdfFile.name); // PDF имя
|
||
uploadFormData.append('voucher', values.voucher);
|
||
uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown');
|
||
uploadFormData.append('upload_timestamp', new Date().toISOString());
|
||
uploadFormData.append('file', pdfFile); // PDF файл!
|
||
|
||
setUploadProgress(`📡 Загружаем ${pdfFile.name} в облако...`);
|
||
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
|
||
method: 'POST',
|
||
body: uploadFormData,
|
||
});
|
||
|
||
setUploadProgress(`🔍 Распознаём текст и проверяем документ...`);
|
||
const uploadResult = await uploadResponse.json();
|
||
|
||
// Логируем ответ от n8n для отладки
|
||
console.log('n8n upload response:', uploadResult);
|
||
|
||
const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult;
|
||
if (resultData?.success) {
|
||
uploadedFiles.push({
|
||
filename: file.name,
|
||
success: true
|
||
});
|
||
} else {
|
||
console.error('Upload failed for file:', file.name, 'Response:', uploadResult);
|
||
}
|
||
}
|
||
|
||
const uploadResult = {
|
||
success: uploadedFiles.length > 0,
|
||
uploaded_count: uploadedFiles.length,
|
||
total_count: fileList.length,
|
||
files: uploadedFiles
|
||
};
|
||
|
||
if (uploadResult.success) {
|
||
addDebugEvent?.('upload', 'success', `✅ Загружено в S3: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
|
||
uploaded_count: uploadResult.uploaded_count,
|
||
files: uploadResult.files
|
||
});
|
||
|
||
// OCR запустится автоматически в n8n workflow (параллельно)
|
||
addDebugEvent?.('ocr', 'pending', `🔄 OCR запущен в фоне через n8n`, {
|
||
claim_id: claimId,
|
||
message: 'Обработка продолжается асинхронно'
|
||
});
|
||
|
||
updateFormData({
|
||
...values,
|
||
claim_id: claimId,
|
||
policyScanUploaded: true,
|
||
policyScanFiles: uploadResult.files,
|
||
policyValidationWarning: '' // Silent validation
|
||
});
|
||
|
||
// ⏳ Включаем режим ожидания SSE результата!
|
||
console.log('🔄 Устанавливаю waitingForOcr=true для claim_id:', claimId);
|
||
setWaitingForOcr(true); // ⬅️ Это откроет SSE соединение в useEffect!
|
||
setUploadProgress('⏳ Ждём результат распознавания полиса...');
|
||
message.info('Файл загружен. Ожидаем результат OCR и AI анализа...');
|
||
console.log('📡 waitingForOcr установлен в true, useEffect должен сработать!');
|
||
|
||
// SSE событие обработается в useEffect и покажет модалку
|
||
// НЕ вызываем onNext() здесь!
|
||
} else {
|
||
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки файлов`, { error: 'Upload failed' });
|
||
message.error('Ошибка загрузки файлов');
|
||
}
|
||
} catch (error) {
|
||
message.error('Ошибка загрузки файлов');
|
||
console.error(error);
|
||
} finally {
|
||
setUploading(false);
|
||
setUploadProgress('');
|
||
}
|
||
};
|
||
|
||
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="E1000-302538524"
|
||
size="large"
|
||
onChange={handleVoucherChange}
|
||
onPaste={handleVoucherPaste}
|
||
maxLength={15}
|
||
/>
|
||
</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={(file) => {
|
||
// Проверка размера (макс 15MB для сырого файла)
|
||
const isLt15M = file.size / 1024 / 1024 < 15;
|
||
if (!isLt15M) {
|
||
message.error(`${file.name}: файл больше 15MB`);
|
||
return Upload.LIST_IGNORE;
|
||
}
|
||
|
||
// Проверка формата
|
||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
|
||
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
|
||
|
||
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
|
||
message.error(`${file.name}: неподдерживаемый формат. Используйте JPG, PNG, PDF, HEIC или WEBP`);
|
||
return Upload.LIST_IGNORE;
|
||
}
|
||
|
||
return false; // Не загружать автоматически
|
||
}}
|
||
accept="image/*,.pdf,.heic,.heif,.webp"
|
||
multiple={false}
|
||
maxCount={1}
|
||
showUploadList={{
|
||
showPreviewIcon: true,
|
||
showRemoveIcon: true,
|
||
}}
|
||
>
|
||
<Button icon={<UploadOutlined />} size="large" block disabled={fileList.length >= 1}>
|
||
Загрузить скан полиса (JPG, PNG, HEIC, PDF)
|
||
</Button>
|
||
</Upload>
|
||
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
||
Поддерживаются: JPG, PNG, HEIC, WEBP, PDF (макс 15MB)
|
||
{fileList.length > 0 && (
|
||
<span style={{ marginLeft: 8, color: '#52c41a' }}>
|
||
(автоконвертация в PDF)
|
||
</span>
|
||
)}
|
||
</div>
|
||
</Form.Item>
|
||
|
||
{/* Прогресс обработки */}
|
||
{uploading && uploadProgress && (
|
||
<Alert
|
||
message={uploadProgress}
|
||
type="info"
|
||
showIcon
|
||
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
)}
|
||
|
||
<Form.Item>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<Button
|
||
onClick={() => {
|
||
setPolicyNotFound(false);
|
||
setFileList([]);
|
||
}}
|
||
size="large"
|
||
disabled={uploading}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
onClick={handleSubmitWithScan}
|
||
loading={uploading}
|
||
size="large"
|
||
style={{ flex: 1 }}
|
||
>
|
||
{uploading ? 'Обрабатываем...' : 'Продолжить со сканом'}
|
||
</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>
|
||
)}
|
||
|
||
{/* Модальное окно ожидания OCR результата */}
|
||
<Modal
|
||
open={ocrModalVisible}
|
||
closable={ocrModalContent !== 'loading'}
|
||
maskClosable={false}
|
||
footer={ocrModalContent === 'loading' ? null :
|
||
ocrModalContent?.success ? [
|
||
// ✅ Полис распознан - кнопка "Продолжить"
|
||
<Button key="next" type="primary" onClick={() => {
|
||
setOcrModalVisible(false);
|
||
onNext(); // Переход на следующий шаг
|
||
}}>
|
||
Продолжить →
|
||
</Button>
|
||
] : [
|
||
// ❌ Полис не распознан - кнопка "Загрузить другой файл"
|
||
<Button key="retry" type="primary" onClick={() => {
|
||
setOcrModalVisible(false);
|
||
setFileList([]); // Очищаем список файлов
|
||
setPolicyNotFound(true); // Показываем форму загрузки снова
|
||
}}>
|
||
Загрузить другой файл
|
||
</Button>
|
||
]
|
||
}
|
||
width={700}
|
||
centered
|
||
>
|
||
{ocrModalContent === 'loading' ? (
|
||
<div style={{ textAlign: 'center', padding: '40px 20px' }}>
|
||
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
||
<h3 style={{ marginTop: 24, marginBottom: 12 }}>⏳ Обрабатываем документ</h3>
|
||
<p style={{ color: '#666', marginBottom: 8 }}>OCR распознавание текста...</p>
|
||
<p style={{ color: '#666', marginBottom: 8 }}>AI анализ содержимого...</p>
|
||
<p style={{ color: '#666' }}>Проверка валидности полиса...</p>
|
||
<p style={{ color: '#999', fontSize: 12, marginTop: 20 }}>
|
||
Это может занять 20-30 секунд. Пожалуйста, подождите...
|
||
</p>
|
||
</div>
|
||
) : ocrModalContent ? (
|
||
<div>
|
||
<h3 style={{ marginBottom: 16 }}>
|
||
{ocrModalContent.success ? '✅ Результат распознавания' : '❌ Ошибка распознавания'}
|
||
</h3>
|
||
{ocrModalContent.success ? (
|
||
<div>
|
||
<p><strong>Номер полиса:</strong> {ocrModalContent.data?.policy_number || 'н/д'}</p>
|
||
<p><strong>Владелец:</strong> {ocrModalContent.data?.policyholder_full_name || 'н/д'}</p>
|
||
{ocrModalContent.data?.insured_persons?.length > 0 && (
|
||
<>
|
||
<p><strong>Застрахованные лица:</strong></p>
|
||
<ul>
|
||
{ocrModalContent.data.insured_persons.map((person: any, i: number) => (
|
||
<li key={i}>{person.full_name} (ДР: {person.birth_date || 'н/д'})</li>
|
||
))}
|
||
</ul>
|
||
</>
|
||
)}
|
||
{ocrModalContent.data?.policy_period && (
|
||
<p><strong>Период:</strong> {ocrModalContent.data.policy_period.insured_from} - {ocrModalContent.data.policy_period.insured_to}</p>
|
||
)}
|
||
<p style={{ marginTop: 16 }}><strong>Полный ответ AI:</strong></p>
|
||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
|
||
{JSON.stringify(ocrModalContent.data, null, 2)}
|
||
</pre>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<p>{ocrModalContent.message || 'Документ не распознан'}</p>
|
||
<p style={{ marginTop: 16 }}><strong>Полный ответ:</strong></p>
|
||
<pre style={{ background: '#fff3f3', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
|
||
{JSON.stringify(ocrModalContent.data, null, 2)}
|
||
</pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</Modal>
|
||
|
||
{/* 🔧 Технические кнопки для разработки */}
|
||
<div style={{
|
||
marginTop: 24,
|
||
padding: 16,
|
||
background: '#f0f0f0',
|
||
borderRadius: 8,
|
||
border: '2px dashed #999'
|
||
}}>
|
||
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
|
||
🔧 DEV MODE - Быстрая навигация (без валидации)
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<Button
|
||
type="dashed"
|
||
onClick={() => {
|
||
// Пропускаем валидацию, заполняем минимальные данные
|
||
const devData = {
|
||
voucher: 'E1000-123456789',
|
||
claim_id: `CLM-DEV-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
||
};
|
||
updateFormData(devData);
|
||
onNext();
|
||
}}
|
||
size="small"
|
||
style={{ flex: 1 }}
|
||
>
|
||
Далее → (Step 2) [пропустить]
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Form>
|
||
);
|
||
}
|