Files
aiform_prod/frontend/src/components/form/StepDocumentUpload.tsx
AI Assistant 4c8fda5f55 Добавлено логирование для отладки черновиков
- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API
- Добавлены логи в backend (claims.py) для отладки SQL запросов
- Создан лог сессии с описанием проблемы и текущего состояния
- Проблема: API возвращает 0 черновиков, хотя в БД есть данные
2025-11-19 18:46:48 +03:00

441 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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://localhost:8200';
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();
};
// Отправляем файл на сервер через backend API (proxy к n8n)
// Используем относительный путь - Vite proxy перенаправит на backend
const response = await fetch('/api/n8n/upload/file', {
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 - 1) / totalDocs) * 100)}% завершено
</span>
</div>
<Progress
percent={Math.round(((currentDocNumber - 1) / totalDocs) * 100)}
showInfo={false}
strokeColor="#595959"
/>
</div>
{/* Заголовок */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>
<FileTextOutlined style={{ marginRight: 8, color: '#595959' }} />
{documentConfig.name}
{documentConfig.required && <span style={{ color: '#000000', marginLeft: 8 }}>*</span>}
</h2>
<p style={{ color: '#666', margin: 0 }}>
{documentConfig.description}
</p>
{!documentConfig.required && (
<p style={{ color: '#595959', 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={() => {
console.log('🔙 Кнопка Назад нажата');
onPrev();
}}
size="large"
disabled={false}
>
Назад
</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={() => {
console.log('🔙 DEV Кнопка Назад нажата');
onPrev();
}}
size="small"
disabled={false}
>
Назад
</Button>
<Button
type="dashed"
onClick={() => {
console.log('⏭️ DEV Пропустить нажата');
// Эмулируем загрузку документа
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 }}
disabled={false}
>
Пропустить [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;