Files
aiform_prod/frontend/src/components/form/StepDocumentUpload.tsx

413 lines
14 KiB
TypeScript
Raw Normal View History

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 formDataToSend = new FormData();
fileList.forEach((file) => {
console.log('📎 File:', file.name, file.originFileObj);
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 || '');
console.log('📤 Uploading to n8n:', {
claim_id: claimId,
session_id: sessionId,
file_type: documentConfig.file_type,
voucher: formData.voucher
});
// Показываем модалку обработки
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)
const response = await fetch('https://n8n.clientright.pro/webhook/erv-upload', {
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;