feat: Интеграция n8n + Redis Pub/Sub + SSE для real-time обработки заявок
🎯 Основные изменения: Backend: - ✅ Добавлен SSE endpoint для real-time событий (/api/v1/events/{task_id}) - ✅ Redis Pub/Sub для публикации/подписки на события OCR/Vision - ✅ Удален aioboto3 из requirements.txt (конфликт зависимостей) - ✅ Добавлен OCR worker (deprecated, логика перенесена в n8n) Frontend (React): - ✅ Автогенерация claim_id и session_id - ✅ Клиентская конвертация файлов в PDF (JPG/PNG/HEIC/WEBP) - ✅ Сжатие изображений до 2MB перед конвертацией - ✅ SSE подписка на события OCR/Vision в Step1Policy - ✅ Валидация документов (полис vs неподходящий контент) - ✅ Real-time прогресс загрузки и обработки файлов - ✅ Интеграция с n8n webhooks для проверки полиса и загрузки файлов n8n Workflows: - ✅ Проверка полиса в MySQL + запись в PostgreSQL - ✅ Загрузка файлов в S3 + OCR + Vision AI - ✅ Публикация событий в Redis через backend API - ✅ Валидация документов (распознавание полисов ERV) Документация: - 📝 N8N_INTEGRATION.md - интеграция с n8n - 📝 N8N_SQL_QUERIES.md - SQL запросы для workflows - 📝 N8N_PDF_COMPRESS.md - сжатие PDF - 📝 N8N_STIRLING_COMPRESS.md - интеграция Stirling-PDF Утилиты: - 🔧 monitor_redis.py/sh - мониторинг Redis Pub/Sub - 🔧 test_redis_events.sh - тестирование событий - 🔧 pdfConverter.ts - клиентская конвертация в PDF Архитектура: React → n8n webhooks (sync) → MySQL/PostgreSQL/S3 → n8n workflows (async) → OCR/Vision → Redis Pub/Sub → SSE → React
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Form, Input, Button, message, Upload, Progress } from 'antd';
|
||||
import { FileProtectOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Form, Input, Button, message, Upload, Spin, Alert, Modal } from 'antd';
|
||||
import { FileProtectOutlined, UploadOutlined, LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { convertToPDF } from '../../utils/pdfConverter';
|
||||
|
||||
interface Props {
|
||||
formData: any;
|
||||
@@ -56,7 +57,76 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
const [policyNotFound, setPolicyNotFound] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [ocrProgress, setOcrProgress] = useState<string>('');
|
||||
const [uploadProgress, setUploadProgress] = useState('');
|
||||
const [ocrResult, setOcrResult] = useState<any>(null);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
// SSE подключение для получения результатов OCR/Vision
|
||||
useEffect(() => {
|
||||
const claimId = formData.claim_id;
|
||||
if (!claimId || !uploading) return;
|
||||
|
||||
// Подключаемся к SSE для получения результатов OCR
|
||||
const eventSource = new EventSource(`http://147.45.189.234:8000/events/${claimId}`);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📨 SSE event received:', data);
|
||||
|
||||
if (data.event_type === 'ocr_completed') {
|
||||
setUploadProgress(''); // Убираем крутилку
|
||||
setOcrResult(data);
|
||||
|
||||
if (data.status === 'success' && data.data?.is_valid_document) {
|
||||
// ✅ Полис распознан успешно
|
||||
message.success(data.message || '✅ Полис успешно распознан!');
|
||||
addDebugEvent?.('ocr', 'success', data.message, data.data);
|
||||
} else {
|
||||
// ❌ Документ не распознан или это не полис
|
||||
const warnings = data.data?.ai_analysis?.warnings || ['Документ не распознан'];
|
||||
|
||||
Modal.error({
|
||||
title: '❌ Документ не распознан',
|
||||
content: (
|
||||
<div>
|
||||
<p>{data.message}</p>
|
||||
{warnings.length > 0 && (
|
||||
<ul>
|
||||
{warnings.map((w: string, i: number) => (
|
||||
<li key={i}>{w}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p style={{ marginTop: 12, color: '#666' }}>
|
||||
Пожалуйста, загрузите скан страхового полиса ERV.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
addDebugEvent?.('ocr', 'error', data.message, data.data);
|
||||
setFileList([]); // Очищаем список файлов
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SSE parse error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('SSE connection error:', error);
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [formData.claim_id, uploading]);
|
||||
|
||||
// Обработчик изменения поля полиса с автозаменой и маской
|
||||
const handleVoucherChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -81,32 +151,40 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
|
||||
addDebugEvent?.('policy_check', 'pending', `Проверяю полис: ${values.voucher}`, { voucher: values.voucher });
|
||||
|
||||
// Проверка полиса через API
|
||||
const response = await fetch('http://147.45.146.17:8100/api/v1/policy/check', {
|
||||
// Проверка полиса через n8n вебхук + создание записи в БД
|
||||
const response = await fetch('https://n8n.clientright.pro/webhook/9eb7bc5b-645f-477d-a5d8-5a346260a265', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
voucher: values.voucher,
|
||||
email: 'temp@check.com', // Email не требуется на этом шаге
|
||||
claim_id: formData.claim_id, // Передаём claim_id для создания записи
|
||||
policy_number: values.voucher,
|
||||
session_id: sessionStorage.getItem('session_id') || 'unknown'
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.found) {
|
||||
// Новый формат ответа от n8n: {claim: {...}, policy: {...}}
|
||||
const policyFound = result.policy?.found === 1 || result.policy?.found === true;
|
||||
|
||||
if (policyFound) {
|
||||
// Полис найден - переходим дальше
|
||||
addDebugEvent?.('policy_check', 'success', `✅ Полис найден в MySQL БД (33,963 полисов)`, {
|
||||
addDebugEvent?.('policy_check', 'success', `✅ Полис найден в MySQL БД`, {
|
||||
found: true,
|
||||
claim: result.claim,
|
||||
policy: result.policy,
|
||||
voucher: values.voucher
|
||||
});
|
||||
message.success('Полис найден в базе данных');
|
||||
message.success(`Полис найден: ${result.policy.voucher}. Застрахованных: ${result.policy.count} чел.`);
|
||||
updateFormData(values);
|
||||
onNext();
|
||||
} else {
|
||||
// Полис НЕ найден - показываем загрузку скана
|
||||
addDebugEvent?.('policy_check', 'warning', `⚠️ Полис не найден → требуется загрузка скана`, {
|
||||
addDebugEvent?.('policy_check', 'warning', `▲ Полис не найден → требуется загрузка скана`, {
|
||||
found: false,
|
||||
claim: result.claim,
|
||||
message: result.policy?.message || 'Полис не найден',
|
||||
voucher: values.voucher
|
||||
});
|
||||
message.warning('Полис не найден в базе. Загрузите скан полиса');
|
||||
@@ -131,59 +209,8 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
setFileList(newFileList);
|
||||
};
|
||||
|
||||
// Polling для получения OCR результатов
|
||||
const pollOcrResults = async (fileIds: string[]) => {
|
||||
if (fileIds.length === 0) return;
|
||||
|
||||
const maxAttempts = 10;
|
||||
const interval = 3000; // 3 секунды
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
|
||||
setOcrProgress(`🔍 Обработка OCR... (${attempt + 1}/${maxAttempts})`);
|
||||
|
||||
for (const fileId of fileIds) {
|
||||
try {
|
||||
const response = await fetch(`http://147.45.146.17:8100/api/v1/upload/ocr-result/${fileId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.found && result.ocr_result) {
|
||||
const ocr = result.ocr_result;
|
||||
|
||||
addDebugEvent?.('ocr', 'success', `📄 OCR завершен: ${ocr.ocr_text?.length || 0} символов`, {
|
||||
text: ocr.ocr_text?.substring(0, 300)
|
||||
});
|
||||
|
||||
if (ocr.ai_analysis || ocr.document_type) {
|
||||
const isGarbage = ocr.document_type === 'garbage';
|
||||
|
||||
addDebugEvent?.(
|
||||
'ai_analysis',
|
||||
isGarbage ? 'warning' : 'success',
|
||||
isGarbage
|
||||
? `🗑️ ШЛЯПА DETECTED! (пользователю не говорим)`
|
||||
: `🤖 Gemini Vision: ${ocr.document_type}, confidence: ${(ocr.confidence * 100).toFixed(0)}%`,
|
||||
{
|
||||
document_type: ocr.document_type,
|
||||
is_valid: ocr.is_valid,
|
||||
confidence: ocr.confidence,
|
||||
extracted_data: ocr.extracted_data
|
||||
}
|
||||
);
|
||||
|
||||
setOcrProgress(`✅ OCR завершен: ${ocr.document_type}`);
|
||||
return; // Готово
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OCR polling error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOcrProgress('⏱️ OCR обрабатывается в фоне...');
|
||||
};
|
||||
// OCR теперь обрабатывается в n8n (через RabbitMQ + Redis Pub/Sub)
|
||||
// Polling не нужен!
|
||||
|
||||
const handleSubmitWithScan = async () => {
|
||||
if (fileList.length === 0) {
|
||||
@@ -198,27 +225,81 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
setUploadProgress('📤 Подготавливаем документы...');
|
||||
const values = await form.validateFields(['voucher']);
|
||||
|
||||
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} файл(ов) в S3...`, {
|
||||
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} файл(ов) в S3 через n8n...`, {
|
||||
count: fileList.length
|
||||
});
|
||||
|
||||
// Загружаем файлы в S3 с OCR проверкой
|
||||
const formData = new FormData();
|
||||
fileList.forEach((file: any) => {
|
||||
if (file.originFileObj) {
|
||||
formData.append('files', file.originFileObj);
|
||||
// Генерируем claim_id если его нет
|
||||
const claimId = formData.claim_id || `CLM-${new Date().toISOString().split('T')[0]}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||
|
||||
// Загружаем каждый файл через n8n вебхук
|
||||
const uploadedFiles = [];
|
||||
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i];
|
||||
if (!file.originFileObj) continue;
|
||||
|
||||
// 🔄 Конвертируем в PDF перед отправкой
|
||||
let pdfFile: File;
|
||||
try {
|
||||
setUploadProgress(`🔄 Конвертируем ${file.name} в PDF...`);
|
||||
addDebugEvent?.('convert', 'pending', `🔄 Конвертирую ${file.name} в PDF...`, {
|
||||
original_size: `${(file.originFileObj.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
original_type: file.originFileObj.type
|
||||
});
|
||||
|
||||
pdfFile = await convertToPDF(file.originFileObj);
|
||||
|
||||
addDebugEvent?.('convert', 'success', `✅ PDF готов: ${pdfFile.name}`, {
|
||||
pdf_size: `${(pdfFile.size / 1024 / 1024).toFixed(2)} MB`
|
||||
});
|
||||
} catch (error: any) {
|
||||
addDebugEvent?.('convert', 'error', `❌ Ошибка конвертации: ${error.message}`);
|
||||
message.error('Ошибка конвертации файла');
|
||||
continue;
|
||||
}
|
||||
});
|
||||
formData.append('folder', 'policies');
|
||||
|
||||
const uploadResponse = await fetch('http://147.45.146.17:8100/api/v1/upload/files?folder=policies', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('claim_id', claimId);
|
||||
uploadFormData.append('file_type', 'policy_scan');
|
||||
uploadFormData.append('filename', pdfFile.name); // PDF имя
|
||||
uploadFormData.append('voucher', values.voucher);
|
||||
uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown');
|
||||
uploadFormData.append('upload_timestamp', new Date().toISOString());
|
||||
uploadFormData.append('file', pdfFile); // PDF файл!
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
setUploadProgress(`📡 Загружаем ${pdfFile.name} в облако...`);
|
||||
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
|
||||
method: 'POST',
|
||||
body: uploadFormData,
|
||||
});
|
||||
|
||||
setUploadProgress(`🔍 Распознаём текст и проверяем документ...`);
|
||||
const uploadResult = await uploadResponse.json();
|
||||
|
||||
// Логируем ответ от n8n для отладки
|
||||
console.log('n8n upload response:', uploadResult);
|
||||
|
||||
const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult;
|
||||
if (resultData?.success) {
|
||||
uploadedFiles.push({
|
||||
filename: file.name,
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
console.error('Upload failed for file:', file.name, 'Response:', uploadResult);
|
||||
}
|
||||
}
|
||||
|
||||
const uploadResult = {
|
||||
success: uploadedFiles.length > 0,
|
||||
uploaded_count: uploadedFiles.length,
|
||||
total_count: fileList.length,
|
||||
files: uploadedFiles
|
||||
};
|
||||
|
||||
if (uploadResult.success) {
|
||||
addDebugEvent?.('upload', 'success', `✅ Загружено в S3: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
|
||||
@@ -226,27 +307,15 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
files: uploadResult.files
|
||||
});
|
||||
|
||||
// Проверяем OCR результаты
|
||||
if (uploadResult.files && uploadResult.files.length > 0) {
|
||||
const fileIds = uploadResult.files
|
||||
.filter((f: any) => f.file_id)
|
||||
.map((f: any) => f.file_id);
|
||||
|
||||
const firstFile = uploadResult.files[0];
|
||||
|
||||
addDebugEvent?.('ocr', 'pending', `🔍 Запущен OCR для: ${firstFile.filename}`, {
|
||||
file_id: firstFile.file_id,
|
||||
filename: firstFile.filename
|
||||
});
|
||||
|
||||
setOcrProgress('🔄 Запуск OCR...');
|
||||
|
||||
// Запускаем polling в фоне (не блокируем переход)
|
||||
pollOcrResults(fileIds);
|
||||
}
|
||||
// OCR запустится автоматически в n8n workflow (параллельно)
|
||||
addDebugEvent?.('ocr', 'pending', `🔄 OCR запущен в фоне через n8n`, {
|
||||
claim_id: claimId,
|
||||
message: 'Обработка продолжается асинхронно'
|
||||
});
|
||||
|
||||
updateFormData({
|
||||
...values,
|
||||
...values,
|
||||
claim_id: claimId,
|
||||
policyScanUploaded: true,
|
||||
policyScanFiles: uploadResult.files,
|
||||
policyValidationWarning: '' // Silent validation
|
||||
@@ -263,6 +332,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
console.error(error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,57 +406,55 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
fileList={fileList}
|
||||
onChange={handleUploadChange}
|
||||
beforeUpload={(file) => {
|
||||
// Проверка размера (макс 15MB для сырого файла)
|
||||
const isLt15M = file.size / 1024 / 1024 < 15;
|
||||
if (!isLt15M) {
|
||||
message.error(`${file.name}: файл больше 15MB`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
if (fileList.length >= 10) {
|
||||
message.error('Максимум 10 файлов');
|
||||
|
||||
// Проверка формата
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
|
||||
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
|
||||
|
||||
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
|
||||
message.error(`${file.name}: неподдерживаемый формат. Используйте JPG, PNG, PDF, HEIC или WEBP`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return false;
|
||||
|
||||
return false; // Не загружать автоматически
|
||||
}}
|
||||
accept="image/*,.pdf,.heic,.heif"
|
||||
multiple
|
||||
maxCount={10}
|
||||
accept="image/*,.pdf,.heic,.heif,.webp"
|
||||
multiple={false}
|
||||
maxCount={1}
|
||||
showUploadList={{
|
||||
showPreviewIcon: true,
|
||||
showRemoveIcon: true,
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} size="large" block disabled={fileList.length >= 10}>
|
||||
Выбрать файлы (до 10 шт, макс 15MB каждый)
|
||||
<Button icon={<UploadOutlined />} size="large" block disabled={fileList.length >= 1}>
|
||||
Загрузить скан полиса (JPG, PNG, HEIC, PDF)
|
||||
</Button>
|
||||
</Upload>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
||||
Загружено: {fileList.length}/10 файлов
|
||||
Поддерживаются: JPG, PNG, HEIC, WEBP, PDF (макс 15MB)
|
||||
{fileList.length > 0 && (
|
||||
<span style={{ marginLeft: 8, color: '#52c41a' }}>
|
||||
(автоконвертация в PDF)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* OCR Progress */}
|
||||
{ocrProgress && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: '#f0f9ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
{ocrProgress.includes('🔍') || ocrProgress.includes('🔄') ? (
|
||||
<LoadingOutlined style={{ fontSize: 16, color: '#1890ff' }} />
|
||||
) : null}
|
||||
<span style={{ fontSize: 13, fontWeight: 500 }}>{ocrProgress}</span>
|
||||
</div>
|
||||
{ocrProgress.includes('Обработка') && (
|
||||
<Progress
|
||||
percent={Math.min(((ocrProgress.match(/(\d+)\/\d+/)?.[1] || 0) as any) * 10, 90)}
|
||||
status="active"
|
||||
showInfo={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Прогресс обработки */}
|
||||
{uploading && uploadProgress && (
|
||||
<Alert
|
||||
message={uploadProgress}
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
@@ -397,6 +465,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
setFileList([]);
|
||||
}}
|
||||
size="large"
|
||||
disabled={uploading}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
@@ -407,7 +476,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
size="large"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{uploading ? 'Загрузка...' : 'Продолжить со сканом'}
|
||||
{uploading ? 'Обрабатываем...' : 'Продолжить со сканом'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form, Input, Button, Select, DatePicker, Upload, message } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Button, Select, DatePicker, Upload, message, Spin, Alert } from 'antd';
|
||||
import { UploadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -29,6 +29,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState('');
|
||||
|
||||
const handleNext = async () => {
|
||||
try {
|
||||
@@ -37,28 +38,61 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
// Если есть файлы - загружаем
|
||||
if (fileList.length > 0) {
|
||||
setUploading(true);
|
||||
setUploadProgress('📤 Подготавливаем документы...');
|
||||
|
||||
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} документ(ов) в S3...`, {
|
||||
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} документ(ов) в S3 через n8n...`, {
|
||||
count: fileList.length
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
fileList.forEach((file: any) => {
|
||||
if (file.originFileObj) {
|
||||
formData.append('files', file.originFileObj);
|
||||
// Используем claim_id из formData (уже сгенерирован в Step1)
|
||||
const claimId = formData.claim_id;
|
||||
|
||||
// Загружаем каждый документ через n8n вебхук
|
||||
const uploadedFiles = [];
|
||||
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i];
|
||||
if (!file.originFileObj) continue;
|
||||
|
||||
setUploadProgress(`📡 Загружаем документ ${i + 1} из ${fileList.length}: ${file.name}...`);
|
||||
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('claim_id', claimId);
|
||||
uploadFormData.append('file_type', `document_${i + 1}`); // document_1, document_2, etc
|
||||
uploadFormData.append('filename', file.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', file.originFileObj);
|
||||
|
||||
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
|
||||
method: 'POST',
|
||||
body: uploadFormData,
|
||||
});
|
||||
|
||||
setUploadProgress(`🔍 Обрабатываем документ ${i + 1} из ${fileList.length}...`);
|
||||
const uploadResult = await uploadResponse.json();
|
||||
|
||||
const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult;
|
||||
if (resultData?.success) {
|
||||
uploadedFiles.push({
|
||||
filename: file.name,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const uploadResponse = await fetch('http://147.45.146.17:8100/api/v1/upload/files?folder=documents', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
const uploadResult = {
|
||||
success: uploadedFiles.length > 0,
|
||||
uploaded_count: uploadedFiles.length,
|
||||
total_count: fileList.length,
|
||||
files: uploadedFiles
|
||||
};
|
||||
|
||||
if (uploadResult.success) {
|
||||
addDebugEvent?.('upload', 'success', `✅ Документы загружены: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
|
||||
files: uploadResult.files
|
||||
addDebugEvent?.('upload', 'success', `✅ Документы загружены через n8n: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
|
||||
files: uploadResult.files,
|
||||
claim_id: claimId
|
||||
});
|
||||
|
||||
updateFormData({
|
||||
@@ -68,10 +102,12 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
} else {
|
||||
message.error('Ошибка загрузки документов');
|
||||
setUploading(false);
|
||||
setUploadProgress('');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(false);
|
||||
setUploadProgress('');
|
||||
} else {
|
||||
updateFormData(values);
|
||||
}
|
||||
@@ -80,6 +116,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
} catch (error) {
|
||||
message.error('Заполните все обязательные поля');
|
||||
setUploading(false);
|
||||
setUploadProgress('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,9 +263,18 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
message.error(`${file.name}: файл больше 15MB`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
|
||||
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
|
||||
|
||||
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
|
||||
message.error(`${file.name}: неподдерживаемый формат`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
accept="image/*,.pdf,.heic,.heif"
|
||||
accept="image/*,.pdf,.heic,.heif,.webp"
|
||||
multiple
|
||||
maxCount={5}
|
||||
>
|
||||
@@ -254,13 +300,23 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
message.error(`${file.name}: файл больше 15MB`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
if (fileList.length >= 10) {
|
||||
message.error('Максимум 10 файлов');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
|
||||
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
|
||||
|
||||
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
|
||||
message.error(`${file.name}: неподдерживаемый формат`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
accept="image/*,.pdf,.heic,.heif"
|
||||
accept="image/*,.pdf,.heic,.heif,.webp"
|
||||
multiple
|
||||
maxCount={10}
|
||||
showUploadList={{
|
||||
@@ -277,9 +333,20 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* Прогресс обработки */}
|
||||
{uploading && uploadProgress && (
|
||||
<Alert
|
||||
message={uploadProgress}
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
|
||||
style={{ marginBottom: 16, marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
||||
<Button onClick={onPrev} size="large">Назад</Button>
|
||||
<Button onClick={onPrev} size="large" disabled={uploading}>Назад</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
@@ -287,7 +354,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
style={{ flex: 1 }}
|
||||
size="large"
|
||||
>
|
||||
{uploading ? 'Загрузка документов...' : 'Далее'}
|
||||
{uploading ? 'Обрабатываем...' : 'Далее'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user