2025-10-28 12:43:38 +03:00
|
|
|
|
import { Form, Button, Select, Upload, message, Spin, Card, Modal, Progress } from 'antd';
|
|
|
|
|
|
import { UploadOutlined, LoadingOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
2025-10-28 12:03:12 +03:00
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
2025-10-25 09:27:56 +03:00
|
|
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
formData: any;
|
|
|
|
|
|
updateFormData: (data: any) => void;
|
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
|
onPrev: () => void;
|
2025-10-25 09:27:56 +03:00
|
|
|
|
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
|
2025-10-24 16:19:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Типы страховых случаев
|
2025-10-25 09:27:56 +03:00
|
|
|
|
const EVENT_TYPES = [
|
|
|
|
|
|
{ value: 'delay_flight', label: 'Задержка авиарейса (более 3 часов)' },
|
|
|
|
|
|
{ value: 'cancel_flight', label: 'Отмена авиарейса' },
|
2025-10-28 12:03:12 +03:00
|
|
|
|
{ value: 'miss_connection', label: 'Пропуск (задержка прибытия) стыковочного рейса' },
|
2025-10-25 09:27:56 +03:00
|
|
|
|
{ value: 'emergency_landing', label: 'Посадка воздушного судна на запасной аэродром' },
|
|
|
|
|
|
{ value: 'delay_train', label: 'Задержка отправки поезда' },
|
|
|
|
|
|
{ value: 'cancel_train', label: 'Отмена поезда' },
|
|
|
|
|
|
{ value: 'delay_ferry', label: 'Задержка/отмена отправки парома/круизного судна' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Конфигурация документов для каждого типа события с уникальными file_type
|
|
|
|
|
|
const DOCUMENT_CONFIGS: Record<string, any[]> = {
|
|
|
|
|
|
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, документы"
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-25 09:27:56 +03:00
|
|
|
|
export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) {
|
2025-10-24 16:19:58 +03:00
|
|
|
|
const [form] = Form.useForm();
|
2025-10-28 12:03:12 +03:00
|
|
|
|
const [eventType, setEventType] = useState(formData.eventType || '');
|
2025-10-28 12:43:38 +03:00
|
|
|
|
const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0);
|
|
|
|
|
|
const [processedDocuments, setProcessedDocuments] = useState<Record<string, any>>({});
|
|
|
|
|
|
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
2025-10-25 09:27:56 +03:00
|
|
|
|
const [uploading, setUploading] = useState(false);
|
2025-10-28 12:43:38 +03:00
|
|
|
|
const [ocrModalVisible, setOcrModalVisible] = useState(false);
|
|
|
|
|
|
const [ocrModalContent, setOcrModalContent] = useState<any>(null);
|
2025-10-28 12:03:12 +03:00
|
|
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const handleEventTypeChange = (value: string) => {
|
|
|
|
|
|
setEventType(value);
|
2025-10-28 12:43:38 +03:00
|
|
|
|
setCurrentDocumentIndex(0);
|
|
|
|
|
|
setProcessedDocuments({});
|
|
|
|
|
|
setCurrentFile(null);
|
2025-10-28 12:03:12 +03:00
|
|
|
|
form.setFieldValue('eventType', value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем конфигурацию документов для выбранного типа события
|
|
|
|
|
|
const currentDocuments = eventType ? DOCUMENT_CONFIGS[eventType] || [] : [];
|
2025-10-28 12:43:38 +03:00
|
|
|
|
const currentDocConfig = currentDocuments[currentDocumentIndex];
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем все ли обязательные документы обработаны
|
|
|
|
|
|
const allRequiredProcessed = currentDocuments
|
|
|
|
|
|
.filter(doc => doc.required)
|
|
|
|
|
|
.every(doc => processedDocuments[doc.field]);
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
// SSE подключение для получения результатов OCR
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const claimId = formData.claim_id;
|
|
|
|
|
|
if (!claimId || !uploading || !currentDocConfig) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
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);
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
setOcrModalContent((prev) => {
|
|
|
|
|
|
if (prev && prev !== 'loading') {
|
|
|
|
|
|
console.log('✅ SSE закрыто после получения результата');
|
|
|
|
|
|
return prev;
|
|
|
|
|
|
}
|
|
|
|
|
|
return { success: false, data: null, message: 'Ошибка подключения к серверу' };
|
|
|
|
|
|
});
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
setUploading(false);
|
|
|
|
|
|
eventSource.close();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
eventSource.onopen = () => {
|
|
|
|
|
|
console.log('✅ SSE: Соединение открыто');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (eventSourceRef.current) {
|
|
|
|
|
|
eventSourceRef.current.close();
|
|
|
|
|
|
eventSourceRef.current = null;
|
2025-10-28 12:03:12 +03:00
|
|
|
|
}
|
2025-10-28 12:43:38 +03:00
|
|
|
|
};
|
|
|
|
|
|
}, [formData.claim_id, uploading, currentDocConfig]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleFileSelect = (file: File) => {
|
|
|
|
|
|
setCurrentFile(file);
|
|
|
|
|
|
return false; // Предотвращаем автозагрузку
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUploadAndProcess = async () => {
|
|
|
|
|
|
if (!currentFile || !currentDocConfig) {
|
|
|
|
|
|
message.error('Выберите файл');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
try {
|
2025-10-28 12:03:12 +03:00
|
|
|
|
setUploading(true);
|
2025-10-28 12:43:38 +03:00
|
|
|
|
setOcrModalVisible(true);
|
|
|
|
|
|
setOcrModalContent('loading');
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
const claimId = formData.claim_id;
|
2025-10-28 12:43:38 +03:00
|
|
|
|
|
|
|
|
|
|
addDebugEvent?.('upload', 'pending', `📤 Загружаю: ${currentDocConfig.name}`, {
|
|
|
|
|
|
file_type: currentDocConfig.file_type,
|
|
|
|
|
|
filename: currentFile.name
|
|
|
|
|
|
});
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
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', sessionStorage.getItem('session_id') || 'unknown');
|
|
|
|
|
|
uploadFormData.append('upload_timestamp', new Date().toISOString());
|
|
|
|
|
|
uploadFormData.append('file', currentFile);
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
body: uploadFormData,
|
|
|
|
|
|
});
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
const uploadResult = await uploadResponse.json();
|
|
|
|
|
|
console.log('📤 Файл загружен, ждём OCR результат...');
|
|
|
|
|
|
|
|
|
|
|
|
addDebugEvent?.('upload', 'success', `✅ Файл загружен, обрабатывается...`, {
|
|
|
|
|
|
file_type: currentDocConfig.file_type
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// SSE обработчик получит результат и обновит состояние
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('Ошибка загрузки:', error);
|
|
|
|
|
|
message.error('Ошибка загрузки файла');
|
2025-10-25 09:27:56 +03:00
|
|
|
|
setUploading(false);
|
2025-10-28 12:43:38 +03:00
|
|
|
|
setOcrModalVisible(false);
|
|
|
|
|
|
|
|
|
|
|
|
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки: ${error.message}`);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
return (
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
initialValues={formData}
|
|
|
|
|
|
style={{ marginTop: 24 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form.Item
|
2025-10-25 09:27:56 +03:00
|
|
|
|
label="Выберите тип события"
|
|
|
|
|
|
name="eventType"
|
|
|
|
|
|
rules={[{ required: true, message: 'Выберите тип события' }]}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="Выберите тип события"
|
|
|
|
|
|
size="large"
|
2025-10-25 10:12:41 +03:00
|
|
|
|
onChange={handleEventTypeChange}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
>
|
|
|
|
|
|
{EVENT_TYPES.map(type => (
|
|
|
|
|
|
<Option key={type.value} value={type.value}>
|
|
|
|
|
|
{type.label}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
{/* Прогресс обработки документов */}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
{eventType && currentDocuments.length > 0 && (
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<Card style={{ marginBottom: 24, background: '#fafafa', borderColor: '#d9d9d9' }}>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
<div style={{ marginBottom: 12 }}>
|
|
|
|
|
|
<strong>Прогресс обработки документов:</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={progressPercent}
|
|
|
|
|
|
status={allRequiredProcessed ? 'success' : 'active'}
|
|
|
|
|
|
format={() => `${processedRequired}/${totalRequired} обязательных`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Список обработанных документов */}
|
|
|
|
|
|
{Object.keys(processedDocuments).length > 0 && (
|
|
|
|
|
|
<div style={{ marginTop: 16 }}>
|
|
|
|
|
|
{currentDocuments.map(doc =>
|
|
|
|
|
|
processedDocuments[doc.field] ? (
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<div key={doc.field} style={{ marginBottom: 8, color: '#595959' }}>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
<CheckCircleOutlined /> {doc.name} - ✅ Обработан
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Текущий документ для загрузки */}
|
|
|
|
|
|
{eventType && currentDocConfig && (
|
2025-10-28 12:03:12 +03:00
|
|
|
|
<Card
|
2025-10-28 12:43:38 +03:00
|
|
|
|
title={`📋 Шаг ${currentDocumentIndex + 1}/${currentDocuments.length}: ${currentDocConfig.name}`}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
style={{ marginTop: 24 }}
|
2025-11-19 18:46:48 +03:00
|
|
|
|
headStyle={{ background: currentDocConfig.required ? '#fafafa' : '#fafafa', borderBottom: '2px solid #d9d9d9' }}
|
2025-10-25 10:12:41 +03:00
|
|
|
|
>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<div style={{ marginBottom: 16, padding: 12, background: '#fafafa', borderRadius: 8 }}>
|
|
|
|
|
|
<p style={{ margin: 0, fontSize: 13, color: '#000000' }}>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
💡 {currentDocConfig.description}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
</p>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
{currentDocConfig.required && (
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<p style={{ margin: '8px 0 0 0', fontSize: 12, color: '#595959' }}>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
⚠️ Этот документ обязательный
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
</div>
|
2025-10-25 10:12:41 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
<Upload
|
|
|
|
|
|
beforeUpload={handleFileSelect}
|
|
|
|
|
|
accept="image/*,.pdf,.heic,.heif,.webp"
|
|
|
|
|
|
maxCount={1}
|
|
|
|
|
|
showUploadList={true}
|
|
|
|
|
|
fileList={currentFile ? [{
|
|
|
|
|
|
uid: '-1',
|
|
|
|
|
|
name: currentFile.name,
|
|
|
|
|
|
status: 'done',
|
|
|
|
|
|
url: URL.createObjectURL(currentFile),
|
|
|
|
|
|
}] : []}
|
|
|
|
|
|
onRemove={() => setCurrentFile(null)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<UploadOutlined />}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
block
|
|
|
|
|
|
disabled={!!currentFile}
|
|
|
|
|
|
>
|
|
|
|
|
|
Выбрать файл (JPG, PNG, PDF, HEIC, макс 15MB)
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Upload>
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={handleUploadAndProcess}
|
|
|
|
|
|
disabled={!currentFile || uploading}
|
|
|
|
|
|
loading={uploading}
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{uploading ? 'Обрабатываем...' : 'Загрузить и обработать'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{!currentDocConfig.required && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={handleSkipOptionalDocument}
|
|
|
|
|
|
disabled={uploading}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
Пропустить
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-28 12:03:12 +03:00
|
|
|
|
</Card>
|
2025-10-25 10:12:41 +03:00
|
|
|
|
)}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
{/* Если все документы обработаны или текущий индекс вышел за пределы */}
|
|
|
|
|
|
{eventType && currentDocumentIndex >= currentDocuments.length && (
|
|
|
|
|
|
<Card
|
2025-11-19 18:46:48 +03:00
|
|
|
|
style={{ marginTop: 24, background: '#fafafa', borderColor: '#d9d9d9' }}
|
2025-10-28 12:43:38 +03:00
|
|
|
|
>
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<CheckCircleOutlined style={{ fontSize: 48, color: '#595959', marginBottom: 16 }} />
|
|
|
|
|
|
<h3 style={{ color: '#000000' }}>✅ Все документы обработаны!</h3>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
<p style={{ color: '#666' }}>
|
|
|
|
|
|
Обработано обязательных документов: {processedRequired}/{totalRequired}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-28 12:43:38 +03:00
|
|
|
|
{/* Модальное окно обработки OCR */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
open={ocrModalVisible}
|
|
|
|
|
|
closable={ocrModalContent !== 'loading'}
|
|
|
|
|
|
maskClosable={false}
|
|
|
|
|
|
footer={ocrModalContent === 'loading' ? null :
|
|
|
|
|
|
ocrModalContent?.success ? [
|
|
|
|
|
|
<Button key="next" type="primary" onClick={handleContinueToNextDocument}>
|
|
|
|
|
|
{currentDocumentIndex < currentDocuments.length - 1 ? 'Продолжить к следующему документу →' : 'Завершить загрузку документов'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
] : [
|
|
|
|
|
|
<Button key="retry" type="primary" onClick={() => {
|
|
|
|
|
|
setOcrModalVisible(false);
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
}}>
|
|
|
|
|
|
Попробовать другой файл
|
|
|
|
|
|
</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 }}>📤 Загрузка в облако...</p>
|
|
|
|
|
|
<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 style={{ marginBottom: 16, fontSize: 15, fontWeight: 500 }}>
|
|
|
|
|
|
📋 {ocrModalContent.documentName}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: 16,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
background: '#fafafa',
|
2025-10-28 12:43:38 +03:00
|
|
|
|
borderRadius: 8,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
border: '1px solid #d9d9d9',
|
2025-10-28 12:43:38 +03:00
|
|
|
|
marginBottom: 16
|
|
|
|
|
|
}}>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<p style={{ margin: '0 0 8px 0', color: '#000000', fontWeight: 500 }}>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
✅ Документ успешно распознан
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p style={{ margin: 0, fontSize: 13, color: '#666' }}>
|
|
|
|
|
|
Данные извлечены и сохранены
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p style={{ marginTop: 16 }}><strong>Извлечённые данные:</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={{
|
2025-11-19 18:46:48 +03:00
|
|
|
|
background: '#fafafa',
|
2025-10-28 12:43:38 +03:00
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
maxHeight: 400,
|
|
|
|
|
|
overflow: 'auto'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{JSON.stringify(ocrModalContent.data, null, 2)}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Кнопки навигации */}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
<Form.Item>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
<Button onClick={onPrev} size="large" disabled={uploading}>Назад</Button>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2025-10-28 12:43:38 +03:00
|
|
|
|
onClick={handleFinishStep}
|
|
|
|
|
|
disabled={!allRequiredProcessed || uploading}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
>
|
2025-10-28 12:43:38 +03:00
|
|
|
|
{allRequiredProcessed ? 'Далее →' : `Осталось документов: ${totalRequired - processedRequired}`}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form.Item>
|
2025-10-28 10:13:18 +03:00
|
|
|
|
|
|
|
|
|
|
{/* 🔧 Технические кнопки для разработки */}
|
|
|
|
|
|
<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
|
|
|
|
|
|
onClick={onPrev}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
disabled={uploading}
|
|
|
|
|
|
>
|
|
|
|
|
|
← Назад (Step 1)
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="dashed"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const devData = {
|
|
|
|
|
|
eventType: 'delay_flight',
|
2025-10-28 12:43:38 +03:00
|
|
|
|
processedDocuments: {
|
|
|
|
|
|
boarding_or_ticket: { flight_number: 'DEV123', date: '2025-10-28' },
|
|
|
|
|
|
delay_confirmation: { delay_duration: '4h' }
|
|
|
|
|
|
}
|
2025-10-28 10:13:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
updateFormData(devData);
|
|
|
|
|
|
onNext();
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Далее → (Step 3) [пропустить]
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|