- Изменён console.error на console.log для нормального закрытия SSE
- Теперь показывается '✅ SSE закрыто после получения результата - всё ОК'
- Реальная ошибка выводится только если данные не получены
- Консоль больше не пугает красными ошибками при успешной работе
424 lines
15 KiB
TypeScript
424 lines
15 KiB
TypeScript
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 () => {
|
||
console.log('🚀 handleUpload called', { fileListLength: fileList.length });
|
||
|
||
if (fileList.length === 0) {
|
||
message.error('Пожалуйста, выберите файл для загрузки');
|
||
return;
|
||
}
|
||
|
||
setUploading(true);
|
||
|
||
try {
|
||
// Берём первый файл (у нас только один файл на шаг)
|
||
const file = fileList[0];
|
||
if (!file.originFileObj) {
|
||
message.error('Ошибка: файл не найден');
|
||
setUploading(false);
|
||
return;
|
||
}
|
||
|
||
console.log('📎 File:', file.name, file.originFileObj);
|
||
|
||
const formDataToSend = new FormData();
|
||
formDataToSend.append('claim_id', claimId);
|
||
formDataToSend.append('file_type', documentConfig.file_type);
|
||
formDataToSend.append('filename', file.name); // Оригинальное имя файла
|
||
formDataToSend.append('voucher', formData.voucher || '');
|
||
formDataToSend.append('session_id', sessionId);
|
||
formDataToSend.append('upload_timestamp', new Date().toISOString());
|
||
formDataToSend.append('file', file.originFileObj); // 'file' - единственное число!
|
||
|
||
console.log('📤 Uploading to n8n:', {
|
||
claim_id: claimId,
|
||
session_id: sessionId,
|
||
file_type: documentConfig.file_type,
|
||
filename: file.name,
|
||
voucher: formData.voucher,
|
||
upload_timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Показываем модалку обработки
|
||
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.log('🔌 SSE connection closed');
|
||
|
||
setProcessingModalContent((prev: any) => {
|
||
if (prev && prev !== 'loading') {
|
||
console.log('✅ SSE закрыто после получения результата - всё ОК');
|
||
return prev; // Оставляем полученный результат
|
||
}
|
||
// Реальная ошибка - не получили данные
|
||
console.error('❌ SSE ошибка: не получили данные', error);
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка подключения к серверу',
|
||
data: null
|
||
};
|
||
});
|
||
|
||
setUploading(false);
|
||
eventSource.close();
|
||
};
|
||
|
||
// Отправляем файл на сервер (n8n webhook)
|
||
const response = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
|
||
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}
|
||
onChange={({ fileList: newFileList }) => {
|
||
console.log('📁 Upload onChange:', newFileList?.length, 'files');
|
||
setFileList(newFileList || []);
|
||
}}
|
||
beforeUpload={() => false}
|
||
maxCount={documentConfig.maxFiles}
|
||
accept="image/*,application/pdf"
|
||
listType="picture"
|
||
disabled={uploading}
|
||
>
|
||
<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 }}>
|
||
<Button onClick={onPrev} size="large">
|
||
← Назад
|
||
</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 }}>
|
||
<Button onClick={onPrev} size="small">
|
||
← Назад
|
||
</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;
|
||
|