2025-10-29 12:36:30 +03:00
|
|
|
|
import { Upload, Button, Card, Alert, Modal, Spin, Progress, message } from 'antd';
|
|
|
|
|
|
import { UploadOutlined, FileTextOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
|
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
|
|
|
|
|
|
|
|
|
|
interface DocumentConfig {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
field: string;
|
|
|
|
|
|
file_type: string;
|
|
|
|
|
|
required: boolean;
|
|
|
|
|
|
maxFiles: number;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
documentConfig: DocumentConfig;
|
|
|
|
|
|
formData: any;
|
|
|
|
|
|
updateFormData: (data: any) => void;
|
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
|
onPrev: () => void;
|
|
|
|
|
|
isLastDocument: boolean;
|
|
|
|
|
|
currentDocNumber: number;
|
|
|
|
|
|
totalDocs: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://147.45.146.17:8100';
|
|
|
|
|
|
|
|
|
|
|
|
const StepDocumentUpload: React.FC<Props> = ({
|
|
|
|
|
|
documentConfig,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
updateFormData,
|
|
|
|
|
|
onNext,
|
|
|
|
|
|
onPrev,
|
|
|
|
|
|
isLastDocument,
|
|
|
|
|
|
currentDocNumber,
|
|
|
|
|
|
totalDocs
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
|
|
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
|
|
const [processingModalVisible, setProcessingModalVisible] = useState(false);
|
|
|
|
|
|
const [processingModalContent, setProcessingModalContent] = useState<any>('loading');
|
|
|
|
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const claimId = formData.claim_id;
|
|
|
|
|
|
const sessionId = formData.session_id || `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем, загружен ли уже документ
|
|
|
|
|
|
const documentData = formData.documents?.[documentConfig.file_type];
|
|
|
|
|
|
const isAlreadyUploaded = documentData?.uploaded;
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// Если документ уже загружен, можно сразу показать кнопку "Продолжить"
|
|
|
|
|
|
if (isAlreadyUploaded) {
|
|
|
|
|
|
console.log(`✅ Документ ${documentConfig.file_type} уже загружен`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isAlreadyUploaded, documentConfig.file_type]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpload = async () => {
|
2025-10-29 12:44:17 +03:00
|
|
|
|
console.log('🚀 handleUpload called', { fileListLength: fileList.length });
|
|
|
|
|
|
|
2025-10-29 12:36:30 +03:00
|
|
|
|
if (fileList.length === 0) {
|
|
|
|
|
|
message.error('Пожалуйста, выберите файл для загрузки');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setUploading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formDataToSend = new FormData();
|
|
|
|
|
|
fileList.forEach((file) => {
|
2025-10-29 12:44:17 +03:00
|
|
|
|
console.log('📎 File:', file.name, file.originFileObj);
|
2025-10-29 12:36:30 +03:00
|
|
|
|
if (file.originFileObj) {
|
|
|
|
|
|
formDataToSend.append('files', file.originFileObj);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
formDataToSend.append('claim_id', claimId);
|
|
|
|
|
|
formDataToSend.append('session_id', sessionId);
|
|
|
|
|
|
formDataToSend.append('file_type', documentConfig.file_type);
|
|
|
|
|
|
formDataToSend.append('voucher', formData.voucher || '');
|
2025-10-29 12:44:17 +03:00
|
|
|
|
|
|
|
|
|
|
console.log('📤 Uploading to n8n:', {
|
|
|
|
|
|
claim_id: claimId,
|
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
|
file_type: documentConfig.file_type,
|
|
|
|
|
|
voucher: formData.voucher
|
|
|
|
|
|
});
|
2025-10-29 12:36:30 +03:00
|
|
|
|
|
|
|
|
|
|
// Показываем модалку обработки
|
|
|
|
|
|
setProcessingModalVisible(true);
|
|
|
|
|
|
setProcessingModalContent('loading');
|
|
|
|
|
|
|
|
|
|
|
|
// Подключаемся к SSE для получения результата
|
|
|
|
|
|
const event_type = `${documentConfig.file_type}_processed`;
|
|
|
|
|
|
const eventSource = new EventSource(
|
|
|
|
|
|
`${API_BASE_URL}/events/${claimId}?event_type=${event_type}`
|
|
|
|
|
|
);
|
|
|
|
|
|
eventSourceRef.current = eventSource;
|
|
|
|
|
|
|
|
|
|
|
|
eventSource.onmessage = (event) => {
|
|
|
|
|
|
console.log('📨 SSE message received:', event.data);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = JSON.parse(event.data);
|
|
|
|
|
|
console.log('📦 Parsed result:', result);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.event_type === event_type) {
|
|
|
|
|
|
setProcessingModalContent(result);
|
|
|
|
|
|
|
|
|
|
|
|
// Сохраняем данные документа в formData
|
|
|
|
|
|
const updatedDocuments = {
|
|
|
|
|
|
...(formData.documents || {}),
|
|
|
|
|
|
[documentConfig.file_type]: {
|
|
|
|
|
|
uploaded: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
file_type: documentConfig.file_type
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
updateFormData({ documents: updatedDocuments, session_id: sessionId });
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ Error parsing SSE message:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
eventSource.onerror = (error) => {
|
|
|
|
|
|
console.error('❌ SSE connection error:', error);
|
|
|
|
|
|
|
|
|
|
|
|
setProcessingModalContent((prev: any) => {
|
|
|
|
|
|
if (prev && prev !== 'loading') {
|
|
|
|
|
|
console.log('✅ SSE закрыто после получения результата');
|
|
|
|
|
|
return prev;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: 'Ошибка подключения к серверу',
|
|
|
|
|
|
data: null
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
eventSource.close();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Отправляем файл на сервер (n8n webhook)
|
2025-10-29 12:47:35 +03:00
|
|
|
|
const response = await fetch('https://n8n.clientright.pro/webhook/erv-upload', {
|
2025-10-29 12:36:30 +03:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
body: formDataToSend,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Upload failed: ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ File uploaded successfully');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ Upload error:', error);
|
|
|
|
|
|
message.error('Ошибка загрузки файла');
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
setProcessingModalVisible(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (eventSourceRef.current) {
|
|
|
|
|
|
eventSourceRef.current.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleContinue = () => {
|
|
|
|
|
|
setProcessingModalVisible(false);
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (eventSourceRef.current) {
|
|
|
|
|
|
eventSourceRef.current.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onNext();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSkipDocument = () => {
|
|
|
|
|
|
if (documentConfig.required) {
|
|
|
|
|
|
message.warning('Этот документ обязателен для загрузки');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Пропускаем необязательный документ
|
|
|
|
|
|
const updatedDocuments = {
|
|
|
|
|
|
...(formData.documents || {}),
|
|
|
|
|
|
[documentConfig.file_type]: {
|
|
|
|
|
|
uploaded: false,
|
|
|
|
|
|
skipped: true,
|
|
|
|
|
|
data: null
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
updateFormData({ documents: updatedDocuments });
|
|
|
|
|
|
onNext();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ maxWidth: 700, margin: '0 auto' }}>
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
{/* Прогресс */}
|
|
|
|
|
|
<div style={{ marginBottom: 24 }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
|
|
|
|
<span style={{ fontSize: 12, color: '#666' }}>
|
|
|
|
|
|
Документ {currentDocNumber} из {totalDocs}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span style={{ fontSize: 12, color: '#666' }}>
|
|
|
|
|
|
{Math.round((currentDocNumber / totalDocs) * 100)}% завершено
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round((currentDocNumber / totalDocs) * 100)}
|
|
|
|
|
|
showInfo={false}
|
|
|
|
|
|
strokeColor="#1890ff"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Заголовок */}
|
|
|
|
|
|
<div style={{ marginBottom: 24 }}>
|
|
|
|
|
|
<h2 style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>
|
|
|
|
|
|
<FileTextOutlined style={{ marginRight: 8, color: '#1890ff' }} />
|
|
|
|
|
|
{documentConfig.name}
|
|
|
|
|
|
{documentConfig.required && <span style={{ color: '#ff4d4f', marginLeft: 8 }}>*</span>}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p style={{ color: '#666', margin: 0 }}>
|
|
|
|
|
|
{documentConfig.description}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{!documentConfig.required && (
|
|
|
|
|
|
<p style={{ color: '#faad14', fontSize: 12, marginTop: 4 }}>
|
|
|
|
|
|
⚠️ Этот документ необязателен, можно пропустить
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Если документ уже загружен */}
|
|
|
|
|
|
{isAlreadyUploaded && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="✅ Документ уже загружен"
|
|
|
|
|
|
description="Вы можете продолжить к следующему документу или загрузить другой файл"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: 24 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Загрузка файла */}
|
|
|
|
|
|
<Upload
|
|
|
|
|
|
fileList={fileList}
|
2025-10-29 12:44:17 +03:00
|
|
|
|
onChange={({ fileList: newFileList }) => {
|
|
|
|
|
|
console.log('📁 Upload onChange:', newFileList?.length, 'files');
|
|
|
|
|
|
setFileList(newFileList || []);
|
|
|
|
|
|
}}
|
2025-10-29 12:36:30 +03:00
|
|
|
|
beforeUpload={() => false}
|
|
|
|
|
|
maxCount={documentConfig.maxFiles}
|
|
|
|
|
|
accept="image/*,application/pdf"
|
|
|
|
|
|
listType="picture"
|
2025-10-29 12:44:17 +03:00
|
|
|
|
disabled={uploading}
|
2025-10-29 12:36:30 +03:00
|
|
|
|
>
|
|
|
|
|
|
<Button icon={<UploadOutlined />} size="large" block disabled={uploading}>
|
|
|
|
|
|
{documentConfig.maxFiles > 1
|
|
|
|
|
|
? `Выберите файлы (до ${documentConfig.maxFiles})`
|
|
|
|
|
|
: 'Выберите файл'
|
|
|
|
|
|
}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
|
|
|
|
|
|
<p style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
|
|
|
|
|
|
Поддерживаются: JPG, PNG, PDF (до 10 МБ)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Кнопки */}
|
|
|
|
|
|
<div style={{ marginTop: 24, display: 'flex', gap: 12 }}>
|
2025-10-29 12:44:17 +03:00
|
|
|
|
<Button onClick={onPrev} size="large">
|
2025-10-29 12:36:30 +03:00
|
|
|
|
← Назад
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{isAlreadyUploaded ? (
|
|
|
|
|
|
<Button type="primary" onClick={handleContinue} size="large" style={{ flex: 1 }}>
|
|
|
|
|
|
{isLastDocument ? 'Далее: Оплата →' : 'Продолжить →'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={handleUpload}
|
|
|
|
|
|
loading={uploading}
|
|
|
|
|
|
disabled={fileList.length === 0}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Загрузить и обработать
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{!documentConfig.required && (
|
|
|
|
|
|
<Button onClick={handleSkipDocument} size="large" disabled={uploading}>
|
|
|
|
|
|
Пропустить
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 🔧 DEV MODE */}
|
|
|
|
|
|
<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 }}>
|
2025-10-29 12:44:17 +03:00
|
|
|
|
<Button onClick={onPrev} size="small">
|
2025-10-29 12:36:30 +03:00
|
|
|
|
← Назад
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="dashed"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
// Эмулируем загрузку документа
|
|
|
|
|
|
const updatedDocuments = {
|
|
|
|
|
|
...(formData.documents || {}),
|
|
|
|
|
|
[documentConfig.file_type]: {
|
|
|
|
|
|
uploaded: true,
|
|
|
|
|
|
data: { test: 'dev_mode_skip' },
|
|
|
|
|
|
file_type: documentConfig.file_type
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
updateFormData({ documents: updatedDocuments });
|
|
|
|
|
|
message.success('DEV: Документ пропущен');
|
|
|
|
|
|
onNext();
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Пропустить [dev] →
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Модалка обработки */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="Обработка документа"
|
|
|
|
|
|
open={processingModalVisible}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
if (processingModalContent !== 'loading') {
|
|
|
|
|
|
setProcessingModalVisible(false);
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
if (eventSourceRef.current) {
|
|
|
|
|
|
eventSourceRef.current.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={processingModalContent === 'loading' ? null : [
|
|
|
|
|
|
<Button key="continue" type="primary" onClick={handleContinue}>
|
|
|
|
|
|
{isLastDocument ? 'Далее: Оплата →' : 'Продолжить к следующему документу →'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
]}
|
|
|
|
|
|
closable={processingModalContent !== 'loading'}
|
|
|
|
|
|
maskClosable={false}
|
|
|
|
|
|
>
|
|
|
|
|
|
{processingModalContent === 'loading' ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: 24 }}>
|
|
|
|
|
|
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
|
|
|
|
|
<p style={{ marginTop: 16, fontSize: 16 }}>
|
|
|
|
|
|
Обрабатываем документ...
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p style={{ color: '#999', fontSize: 12 }}>
|
|
|
|
|
|
Извлекаем данные с помощью AI
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<CheckCircleOutlined style={{ marginRight: 8 }} />
|
|
|
|
|
|
Документ обработан
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={processingModalContent.message || 'Данные успешно извлечены'}
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
showIcon={false}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
maxHeight: 300,
|
|
|
|
|
|
overflow: 'auto'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<pre style={{
|
|
|
|
|
|
margin: 0,
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
|
wordBreak: 'break-word'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{JSON.stringify(processingModalContent.data?.output || processingModalContent.data, null, 2)}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default StepDocumentUpload;
|
|
|
|
|
|
|