import { Form, Button, Select, Upload, message, Spin, Card, Modal, Progress } from 'antd'; import { UploadOutlined, LoadingOutlined, CheckCircleOutlined } from '@ant-design/icons'; import { useState, useEffect, useRef } from 'react'; import type { UploadFile } from 'antd/es/upload/interface'; import dayjs from 'dayjs'; const { Option } = Select; interface Props { formData: any; updateFormData: (data: any) => void; onNext: () => void; onPrev: () => void; addDebugEvent?: (type: string, status: string, message: string, data?: any) => void; } // Типы страховых случаев const EVENT_TYPES = [ { value: 'delay_flight', label: 'Задержка авиарейса (более 3 часов)' }, { value: 'cancel_flight', label: 'Отмена авиарейса' }, { value: 'miss_connection', label: 'Пропуск (задержка прибытия) стыковочного рейса' }, { value: 'emergency_landing', label: 'Посадка воздушного судна на запасной аэродром' }, { value: 'delay_train', label: 'Задержка отправки поезда' }, { value: 'cancel_train', label: 'Отмена поезда' }, { value: 'delay_ferry', label: 'Задержка/отмена отправки парома/круизного судна' }, ]; // Конфигурация документов для каждого типа события с уникальными file_type const DOCUMENT_CONFIGS: Record = { delay_flight: [ { name: "Посадочный талон или Билет", field: "boarding_or_ticket", file_type: "flight_delay_boarding_or_ticket", required: true, maxFiles: 1, description: "Boarding pass или ticket/booking confirmation" }, { name: "Подтверждение задержки", field: "delay_confirmation", file_type: "flight_delay_confirmation", required: true, maxFiles: 3, description: "Справка от АК, email/SMS, или фото табло" } ], cancel_flight: [ { name: "Билет", field: "ticket", file_type: "flight_cancel_ticket", required: true, maxFiles: 1, description: "Ticket/booking confirmation" }, { name: "Уведомление об отмене", field: "cancellation_notice", file_type: "flight_cancel_notice", required: true, maxFiles: 3, description: "Email, SMS или скриншот из приложения АК" } ], miss_connection: [ { name: "Посадочный талон рейса ПРИБЫТИЯ", field: "arrival_boarding", file_type: "connection_arrival_boarding", required: true, maxFiles: 1, description: "Boarding pass рейса, который задержался" }, { name: "Посадочный талон ИЛИ Билет рейса ОТПРАВЛЕНИЯ", field: "departure_boarding_or_ticket", file_type: "connection_departure_boarding_or_ticket", required: true, maxFiles: 1, description: "Boarding pass (если успели) ИЛИ билет (если не успели)" }, { name: "Доказательство задержки (опционально)", field: "delay_proof", file_type: "connection_delay_proof", required: false, maxFiles: 5, description: "Справка, фото табло, email/SMS" } ], delay_train: [ { name: "Билет на поезд", field: "train_ticket", file_type: "train_ticket", required: true, maxFiles: 1, description: "Билет РЖД или другого перевозчика" }, { name: "Подтверждение задержки", field: "delay_proof", file_type: "train_delay_proof", required: true, maxFiles: 3, description: "Справка от РЖД, фото табло, скриншот приложения" } ], cancel_train: [ { name: "Билет на поезд", field: "train_ticket", file_type: "train_ticket", required: true, maxFiles: 1, description: "Билет РЖД или другого перевозчика" }, { name: "Подтверждение отмены", field: "cancel_proof", file_type: "train_cancel_proof", required: true, maxFiles: 3, description: "Справка от РЖД, фото табло, скриншот приложения" } ], delay_ferry: [ { name: "Билет на паром/круиз", field: "ferry_ticket", file_type: "ferry_ticket", required: true, maxFiles: 1, description: "Билет или booking confirmation" }, { name: "Подтверждение задержки/отмены", field: "delay_proof", file_type: "ferry_delay_proof", required: true, maxFiles: 3, description: "Справка от перевозчика, фото расписания, email/SMS" } ], emergency_landing: [ { name: "Посадочный талон или Билет", field: "boarding_or_ticket", file_type: "emergency_boarding_or_ticket", required: true, maxFiles: 1, description: "Boarding pass или ticket" }, { name: "Подтверждение посадки на запасной аэродром", field: "emergency_proof", file_type: "emergency_landing_proof", required: true, maxFiles: 3, description: "Справка от АК, email/SMS, документы" } ] }; export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) { const [form] = Form.useForm(); const [eventType, setEventType] = useState(formData.eventType || ''); const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0); const [processedDocuments, setProcessedDocuments] = useState>({}); const [currentFile, setCurrentFile] = useState(null); const [uploading, setUploading] = useState(false); const [ocrModalVisible, setOcrModalVisible] = useState(false); const [ocrModalContent, setOcrModalContent] = useState(null); const eventSourceRef = useRef(null); const handleEventTypeChange = (value: string) => { setEventType(value); setCurrentDocumentIndex(0); setProcessedDocuments({}); setCurrentFile(null); form.setFieldValue('eventType', value); }; // Получаем конфигурацию документов для выбранного типа события const currentDocuments = eventType ? DOCUMENT_CONFIGS[eventType] || [] : []; const currentDocConfig = currentDocuments[currentDocumentIndex]; // Проверяем все ли обязательные документы обработаны const allRequiredProcessed = currentDocuments .filter(doc => doc.required) .every(doc => processedDocuments[doc.field]); // SSE подключение для получения результатов OCR useEffect(() => { const claimId = formData.claim_id; if (!claimId || !uploading || !currentDocConfig) { return; } console.log('🔌 SSE: Открываю соединение для', currentDocConfig.file_type); const eventSource = new EventSource(`/events/${claimId}`); eventSourceRef.current = eventSource; const expectedEventType = `${currentDocConfig.file_type}_processed`; console.log('👀 Ожидаю event_type:', expectedEventType); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log('📨 SSE event received:', data); if (data.event_type === expectedEventType) { console.log('✅ Получил результат для документа:', currentDocConfig.name); // Сохраняем результат setProcessedDocuments(prev => ({ ...prev, [currentDocConfig.field]: data.data?.output || data.data })); // Показываем результат в модалке setOcrModalContent({ success: data.status === 'completed', data: data.data?.output || data.data, message: data.message, documentName: currentDocConfig.name }); setUploading(false); eventSource.close(); addDebugEvent?.('ocr', 'success', `✅ ${currentDocConfig.name} обработан`, { file_type: currentDocConfig.file_type, data: data.data?.output || data.data }); } } catch (error) { console.error('SSE parse error:', error); } }; eventSource.onerror = (error) => { console.error('❌ SSE connection error:', error); setOcrModalContent((prev) => { if (prev && prev !== 'loading') { console.log('✅ SSE закрыто после получения результата'); return prev; } return { success: false, data: null, message: 'Ошибка подключения к серверу' }; }); setUploading(false); eventSource.close(); }; eventSource.onopen = () => { console.log('✅ SSE: Соединение открыто'); }; return () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } }; }, [formData.claim_id, uploading, currentDocConfig]); const handleFileSelect = (file: File) => { setCurrentFile(file); return false; // Предотвращаем автозагрузку }; const handleUploadAndProcess = async () => { if (!currentFile || !currentDocConfig) { message.error('Выберите файл'); return; } try { setUploading(true); setOcrModalVisible(true); setOcrModalContent('loading'); const claimId = formData.claim_id; addDebugEvent?.('upload', 'pending', `📤 Загружаю: ${currentDocConfig.name}`, { file_type: currentDocConfig.file_type, filename: currentFile.name }); const uploadFormData = new FormData(); uploadFormData.append('claim_id', claimId); uploadFormData.append('file_type', currentDocConfig.file_type); uploadFormData.append('filename', currentFile.name); uploadFormData.append('voucher', formData.voucher || ''); uploadFormData.append('session_id', formData.session_id || 'unknown'); uploadFormData.append('upload_timestamp', new Date().toISOString()); uploadFormData.append('file', currentFile); const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', { method: 'POST', body: uploadFormData, }); const uploadResult = await uploadResponse.json(); console.log('📤 Файл загружен, ждём OCR результат...'); addDebugEvent?.('upload', 'success', `✅ Файл загружен, обрабатывается...`, { file_type: currentDocConfig.file_type }); // SSE обработчик получит результат и обновит состояние } catch (error: any) { console.error('Ошибка загрузки:', error); message.error('Ошибка загрузки файла'); setUploading(false); setOcrModalVisible(false); addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки: ${error.message}`); } }; const handleContinueToNextDocument = () => { setOcrModalVisible(false); setCurrentFile(null); setCurrentDocumentIndex(prev => prev + 1); }; const handleSkipOptionalDocument = () => { setCurrentFile(null); setCurrentDocumentIndex(prev => prev + 1); }; const handleFinishStep = () => { updateFormData({ eventType, processedDocuments }); onNext(); }; // Прогресс загрузки const totalRequired = currentDocuments.filter(d => d.required).length; const processedRequired = currentDocuments.filter(d => d.required && processedDocuments[d.field]).length; const progressPercent = totalRequired > 0 ? Math.round((processedRequired / totalRequired) * 100) : 0; return (
{/* Прогресс обработки документов */} {eventType && currentDocuments.length > 0 && (
Прогресс обработки документов:
`${processedRequired}/${totalRequired} обязательных`} /> {/* Список обработанных документов */} {Object.keys(processedDocuments).length > 0 && (
{currentDocuments.map(doc => processedDocuments[doc.field] ? (
{doc.name} - ✅ Обработан
) : null )}
)}
)} {/* Текущий документ для загрузки */} {eventType && currentDocConfig && (

💡 {currentDocConfig.description}

{currentDocConfig.required && (

⚠️ Этот документ обязательный

)}
setCurrentFile(null)} >
{!currentDocConfig.required && ( )}
)} {/* Если все документы обработаны или текущий индекс вышел за пределы */} {eventType && currentDocumentIndex >= currentDocuments.length && (

✅ Все документы обработаны!

Обработано обязательных документов: {processedRequired}/{totalRequired}

)} {/* Модальное окно обработки OCR */} {currentDocumentIndex < currentDocuments.length - 1 ? 'Продолжить к следующему документу →' : 'Завершить загрузку документов'} ] : [ ] } width={700} centered > {ocrModalContent === 'loading' ? (
} />

⏳ Обрабатываем документ

📤 Загрузка в облако...

📝 OCR распознавание текста...

🤖 AI анализ документа...

✅ Извлечение данных...

Это может занять 20-30 секунд. Пожалуйста, подождите...

) : ocrModalContent ? (

{ocrModalContent.success ? '✅ Результат обработки' : '❌ Ошибка обработки'}

{ocrModalContent.success ? (

📋 {ocrModalContent.documentName}

✅ Документ успешно распознан

Данные извлечены и сохранены

Извлечённые данные:

                  {JSON.stringify(ocrModalContent.data, null, 2)}
                
) : (

{ocrModalContent.message || 'Документ не распознан'}

Детали:

                  {JSON.stringify(ocrModalContent.data, null, 2)}
                
)}
) : null}
{/* Кнопки навигации */}
{/* DEV MODE секция удалена для продакшена */}
); }