Files
aiform_prod/frontend/src/components/form/Step1Policy.tsx
AI Assistant 9084d75103 feat: Пошаговая загрузка документов с модалкой на Step 2
🎯 Изменения:
- Документы загружаются по очереди (один за другим)
- После загрузки каждого документа открывается модалка с крутилкой
- SSE слушает конкретный event_type: {file_type}_processed
- Модалка показывает результат распознавания с извлечёнными данными
- Кнопка 'Продолжить' → переход к следующему документу
- Опциональные документы можно пропустить
- После обработки всех обязательных → 'Далее на Step 3'

📊 UX флоу:
1. Выбор типа события → показываются нужные документы
2. Документ 1: Выбрать файл → Загрузить → Модалка → Результат → Продолжить
3. Документ 2: Выбрать файл → Загрузить → Модалка → Результат → Продолжить
4. Документ 3 (опц): Загрузить ИЛИ Пропустить
5. Все обязательные обработаны → Далее на Step 3

🔑 Каждый документ получает свой уникальный event_type:
- frontend отправляет file_type
- n8n возвращает event_type = {file_type}_processed
- frontend слушает этот конкретный event_type через SSE
2025-10-28 12:43:38 +03:00

820 lines
34 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, 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>
);
}
)}
</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>
);
}