feat: Пошаговая загрузка документов с модалкой на Step 2

🎯 Изменения:
- Документы загружаются по очереди (один за другим)
- После загрузки каждого документа открывается модалка с крутилкой
- SSE слушает конкретный event_type: {file_type}_processed
- Модалка показывает результат распознавания с извлечёнными данными
- Кнопка 'Продолжить' → переход к следующему документу
- Опциональные документы можно пропустить
- После обработки всех обязательных → 'Далее на Step 3'

📊 UX флоу:
1. Выбор типа события → показываются нужные документы
2. Документ 1: Выбрать файл → Загрузить → Модалка → Результат → Продолжить
3. Документ 2: Выбрать файл → Загрузить → Модалка → Результат → Продолжить
4. Документ 3 (опц): Загрузить ИЛИ Пропустить
5. Все обязательные обработаны → Далее на Step 3

🔑 Каждый документ получает свой уникальный event_type:
- frontend отправляет file_type
- n8n возвращает event_type = {file_type}_processed
- frontend слушает этот конкретный event_type через SSE
This commit is contained in:
AI Assistant
2025-10-28 12:43:38 +03:00
parent 122af07779
commit 9084d75103
5 changed files with 585 additions and 187 deletions

View File

@@ -622,6 +622,168 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
) : null}
</Modal>
{/* 🔧 Технические кнопки для разработки */}
<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
type="dashed"
onClick={() => {
// Пропускаем валидацию, заполняем минимальные данные
const devData = {
voucher: 'E1000-123456789',
claim_id: `CLM-DEV-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
};
updateFormData(devData);
onNext();
}}
size="small"
style={{ flex: 1 }}
>
Далее (Step 2) [пропустить]
</Button>
</div>
</div>
</Form>
);
}
)}
</div>
</Form.Item>
{/* Прогресс обработки */}
{uploading && uploadProgress && (
<Alert
message={uploadProgress}
type="info"
showIcon
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
style={{ marginBottom: 16 }}
/>
)}
<Form.Item>
<div style={{ display: 'flex', gap: 8 }}>
<Button
onClick={() => {
setPolicyNotFound(false);
setFileList([]);
}}
size="large"
disabled={uploading}
>
Отмена
</Button>
<Button
type="primary"
onClick={handleSubmitWithScan}
loading={uploading}
size="large"
style={{ flex: 1 }}
>
{uploading ? 'Обрабатываем...' : 'Продолжить со сканом'}
</Button>
</div>
</Form.Item>
</>
)}
{!policyNotFound && (
<div style={{ marginTop: 16, padding: 12, background: '#f0f9ff', borderRadius: 8 }}>
<p style={{ margin: 0, fontSize: 13, color: '#666' }}>
💡 Введите номер полиса. Кириллица автоматически заменяется на латиницу, тире вставляется автоматически
</p>
</div>
)}
{/* Модальное окно ожидания OCR результата */}
<Modal
open={ocrModalVisible}
closable={ocrModalContent !== 'loading'}
maskClosable={false}
footer={ocrModalContent === 'loading' ? null :
ocrModalContent?.success ? [
// ✅ Полис распознан - кнопка "Продолжить"
<Button key="next" type="primary" onClick={() => {
setOcrModalVisible(false);
onNext(); // Переход на следующий шаг
}}>
Продолжить
</Button>
] : [
// ❌ Полис не распознан - кнопка "Загрузить другой файл"
<Button key="retry" type="primary" onClick={() => {
setOcrModalVisible(false);
setFileList([]); // Очищаем список файлов
setPolicyNotFound(true); // Показываем форму загрузки снова
}}>
Загрузить другой файл
</Button>
]
}
width={700}
centered
>
{ocrModalContent === 'loading' ? (
<div style={{ textAlign: 'center', padding: '40px 20px' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
<h3 style={{ marginTop: 24, marginBottom: 12 }}> Обрабатываем документ</h3>
<p style={{ color: '#666', marginBottom: 8 }}>OCR распознавание текста...</p>
<p style={{ color: '#666', marginBottom: 8 }}>AI анализ содержимого...</p>
<p style={{ color: '#666' }}>Проверка валидности полиса...</p>
<p style={{ color: '#999', fontSize: 12, marginTop: 20 }}>
Это может занять 20-30 секунд. Пожалуйста, подождите...
</p>
</div>
) : ocrModalContent ? (
<div>
<h3 style={{ marginBottom: 16 }}>
{ocrModalContent.success ? '✅ Результат распознавания' : '❌ Ошибка распознавания'}
</h3>
{ocrModalContent.success ? (
<div>
<p><strong>Номер полиса:</strong> {ocrModalContent.data?.policy_number || 'н/д'}</p>
<p><strong>Владелец:</strong> {ocrModalContent.data?.policyholder_full_name || 'н/д'}</p>
{ocrModalContent.data?.insured_persons?.length > 0 && (
<>
<p><strong>Застрахованные лица:</strong></p>
<ul>
{ocrModalContent.data.insured_persons.map((person: any, i: number) => (
<li key={i}>{person.full_name} (ДР: {person.birth_date || 'н/д'})</li>
))}
</ul>
</>
)}
{ocrModalContent.data?.policy_period && (
<p><strong>Период:</strong> {ocrModalContent.data.policy_period.insured_from} - {ocrModalContent.data.policy_period.insured_to}</p>
)}
<p style={{ marginTop: 16 }}><strong>Полный ответ AI:</strong></p>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
{JSON.stringify(ocrModalContent.data, null, 2)}
</pre>
</div>
) : (
<div>
<p>{ocrModalContent.message || 'Документ не распознан'}</p>
<p style={{ marginTop: 16 }}><strong>Полный ответ:</strong></p>
<pre style={{ background: '#fff3f3', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
{JSON.stringify(ocrModalContent.data, null, 2)}
</pre>
</div>
)}
</div>
) : null}
</Modal>
{/* 🔧 Технические кнопки для разработки */}
<div style={{
marginTop: 24,

View File

@@ -1,5 +1,5 @@
import { Form, Button, Select, Upload, message, Spin, Alert, Card } from 'antd';
import { UploadOutlined, LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { Form, Button, Select, Upload, message, Spin, Card, Modal, Progress } from 'antd';
import { UploadOutlined, LoadingOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { useState, useEffect, useRef } from 'react';
import type { UploadFile } from 'antd/es/upload/interface';
import dayjs from 'dayjs';
@@ -172,125 +172,188 @@ const DOCUMENT_CONFIGS: Record<string, any[]> = {
export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) {
const [form] = Form.useForm();
const [eventType, setEventType] = useState(formData.eventType || '');
const [documentFiles, setDocumentFiles] = useState<Record<string, UploadFile[]>>({});
const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0);
const [processedDocuments, setProcessedDocuments] = useState<Record<string, any>>({});
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState('');
const [waitingForOcr, setWaitingForOcr] = useState(false);
const [ocrResults, setOcrResults] = useState<any>(null);
const [ocrModalVisible, setOcrModalVisible] = useState(false);
const [ocrModalContent, setOcrModalContent] = useState<any>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const handleEventTypeChange = (value: string) => {
setEventType(value);
setDocumentFiles({}); // Очищаем загруженные файлы при смене типа
setCurrentDocumentIndex(0);
setProcessedDocuments({});
setCurrentFile(null);
form.setFieldValue('eventType', value);
};
// Получаем конфигурацию документов для выбранного типа события
const currentDocuments = eventType ? DOCUMENT_CONFIGS[eventType] || [] : [];
const currentDocConfig = currentDocuments[currentDocumentIndex];
// Проверяем все ли обязательные документы обработаны
const allRequiredProcessed = currentDocuments
.filter(doc => doc.required)
.every(doc => processedDocuments[doc.field]);
const handleUploadChange = (field: string, { fileList: newFileList }: any) => {
setDocumentFiles(prev => ({
...prev,
[field]: newFileList
}));
// SSE подключение для получения результатов OCR
useEffect(() => {
const claimId = formData.claim_id;
if (!claimId || !uploading || !currentDocConfig) {
return;
}
console.log('🔌 SSE: Открываю соединение для', currentDocConfig.file_type);
const eventSource = new EventSource(`/events/${claimId}`);
eventSourceRef.current = eventSource;
const expectedEventType = `${currentDocConfig.file_type}_processed`;
console.log('👀 Ожидаю event_type:', expectedEventType);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('📨 SSE event received:', data);
if (data.event_type === expectedEventType) {
console.log('✅ Получил результат для документа:', currentDocConfig.name);
// Сохраняем результат
setProcessedDocuments(prev => ({
...prev,
[currentDocConfig.field]: data.data?.output || data.data
}));
// Показываем результат в модалке
setOcrModalContent({
success: data.status === 'completed',
data: data.data?.output || data.data,
message: data.message,
documentName: currentDocConfig.name
});
setUploading(false);
eventSource.close();
addDebugEvent?.('ocr', 'success', `${currentDocConfig.name} обработан`, {
file_type: currentDocConfig.file_type,
data: data.data?.output || data.data
});
}
} catch (error) {
console.error('SSE parse error:', error);
}
};
eventSource.onerror = (error) => {
console.error('❌ SSE connection error:', error);
setOcrModalContent((prev) => {
if (prev && prev !== 'loading') {
console.log('✅ SSE закрыто после получения результата');
return prev;
}
return { success: false, data: null, message: 'Ошибка подключения к серверу' };
});
setUploading(false);
eventSource.close();
};
eventSource.onopen = () => {
console.log('✅ SSE: Соединение открыто');
};
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
};
}, [formData.claim_id, uploading, currentDocConfig]);
const handleFileSelect = (file: File) => {
setCurrentFile(file);
return false; // Предотвращаем автозагрузку
};
const handleNext = async () => {
const handleUploadAndProcess = async () => {
if (!currentFile || !currentDocConfig) {
message.error('Выберите файл');
return;
}
try {
const values = await form.validateFields();
// Проверяем что все обязательные документы загружены
const missingDocs = currentDocuments.filter(doc =>
doc.required && (!documentFiles[doc.field] || documentFiles[doc.field].length === 0)
);
if (missingDocs.length > 0) {
message.error(`Загрузите обязательные документы: ${missingDocs.map(d => d.name).join(', ')}`);
return;
}
// Загружаем все документы в S3 через n8n
setUploading(true);
setUploadProgress('📤 Загружаем документы...');
setOcrModalVisible(true);
setOcrModalContent('loading');
const claimId = formData.claim_id;
const uploadedFiles: any[] = [];
for (const docConfig of currentDocuments) {
const files = documentFiles[docConfig.field] || [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.originFileObj) continue;
setUploadProgress(`📡 Загружаем: ${docConfig.name} (${i + 1}/${files.length})...`);
const uploadFormData = new FormData();
uploadFormData.append('claim_id', claimId);
uploadFormData.append('file_type', docConfig.file_type); // 🔑 Уникальный file_type для n8n
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);
addDebugEvent?.('upload', 'pending', `📤 Загружаю документ: ${docConfig.name} (${docConfig.file_type})`, {
file_type: docConfig.file_type,
filename: file.name
});
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
method: 'POST',
body: uploadFormData,
});
const uploadResult = await uploadResponse.json();
const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult;
if (resultData?.success) {
uploadedFiles.push({
filename: file.name,
file_type: docConfig.file_type,
field: docConfig.field,
success: true
});
addDebugEvent?.('upload', 'success', `✅ Документ загружен: ${docConfig.name}`, {
file_type: docConfig.file_type,
filename: file.name
});
}
}
}
if (uploadedFiles.length > 0) {
setUploadProgress('🤖 AI анализирует документы...');
updateFormData({
...values,
uploadedDocuments: uploadedFiles
});
// TODO: Здесь будет ожидание SSE события с результатами OCR/AI
// Пока просто переходим дальше
setUploadProgress('');
setUploading(false);
message.success(`Загружено документов: ${uploadedFiles.length}. Переходим дальше...`);
onNext();
} else {
message.error('Не удалось загрузить документы');
setUploading(false);
setUploadProgress('');
}
} catch (error) {
message.error('Заполните все обязательные поля');
addDebugEvent?.('upload', 'pending', `📤 Загружаю: ${currentDocConfig.name}`, {
file_type: currentDocConfig.file_type,
filename: currentFile.name
});
const uploadFormData = new FormData();
uploadFormData.append('claim_id', claimId);
uploadFormData.append('file_type', currentDocConfig.file_type);
uploadFormData.append('filename', currentFile.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', currentFile);
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
method: 'POST',
body: uploadFormData,
});
const uploadResult = await uploadResponse.json();
console.log('📤 Файл загружен, ждём OCR результат...');
addDebugEvent?.('upload', 'success', `✅ Файл загружен, обрабатывается...`, {
file_type: currentDocConfig.file_type
});
// SSE обработчик получит результат и обновит состояние
} catch (error: any) {
console.error('Ошибка загрузки:', error);
message.error('Ошибка загрузки файла');
setUploading(false);
setUploadProgress('');
setOcrModalVisible(false);
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки: ${error.message}`);
}
};
const handleContinueToNextDocument = () => {
setOcrModalVisible(false);
setCurrentFile(null);
setCurrentDocumentIndex(prev => prev + 1);
};
const handleSkipOptionalDocument = () => {
setCurrentFile(null);
setCurrentDocumentIndex(prev => prev + 1);
};
const handleFinishStep = () => {
updateFormData({
eventType,
processedDocuments
});
onNext();
};
// Прогресс загрузки
const totalRequired = currentDocuments.filter(d => d.required).length;
const processedRequired = currentDocuments.filter(d => d.required && processedDocuments[d.field]).length;
const progressPercent = totalRequired > 0 ? Math.round((processedRequired / totalRequired) * 100) : 0;
return (
<Form
form={form}
@@ -316,108 +379,216 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
</Select>
</Form.Item>
{/* Показываем документы только после выбора типа события */}
{/* Прогресс обработки документов */}
{eventType && currentDocuments.length > 0 && (
<Card
title="📋 Загрузите документы для обработки"
style={{ marginTop: 24 }}
headStyle={{ background: '#f0f9ff', borderBottom: '2px solid #91d5ff' }}
>
<div style={{ marginBottom: 16, padding: 12, background: '#e6f7ff', borderRadius: 8 }}>
<p style={{ margin: 0, fontSize: 13, color: '#0050b3' }}>
💡 Просто загрузите документы наш AI автоматически распознает все данные
(номера рейсов, даты, время, причины задержек)
</p>
<Card style={{ marginBottom: 24, background: '#f0f9ff', borderColor: '#91d5ff' }}>
<div style={{ marginBottom: 12 }}>
<strong>Прогресс обработки документов:</strong>
</div>
{currentDocuments.map((doc, index) => (
<div key={doc.field} style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8 }}>
<span style={{ fontSize: 15, fontWeight: 500 }}>
{doc.required ? '✅' : ''} {doc.name}
{doc.required && <span style={{ color: '#ff4d4f', marginLeft: 4 }}>*</span>}
</span>
</div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
💡 {doc.description}
</div>
<Upload
listType="picture"
fileList={documentFiles[doc.field] || []}
onChange={(info) => handleUploadChange(doc.field, info)}
beforeUpload={(file) => {
const isLt15M = file.size / 1024 / 1024 < 15;
if (!isLt15M) {
message.error(`${file.name}: файл больше 15MB`);
return Upload.LIST_IGNORE;
}
const currentFiles = documentFiles[doc.field] || [];
if (currentFiles.length >= doc.maxFiles) {
message.error(`Максимум ${doc.maxFiles} файл(ов) для этого документа`);
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,.webp"
multiple={doc.maxFiles > 1}
maxCount={doc.maxFiles}
showUploadList={{
showPreviewIcon: true,
showRemoveIcon: true,
}}
>
<Button
icon={<UploadOutlined />}
size="large"
block
disabled={(documentFiles[doc.field] || []).length >= doc.maxFiles}
>
Загрузить файл{doc.maxFiles > 1 ? 'ы' : ''} (до {doc.maxFiles} шт, макс 15MB)
</Button>
</Upload>
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
Загружено: {(documentFiles[doc.field] || []).length}/{doc.maxFiles} файл(ов)
</div>
<Progress
percent={progressPercent}
status={allRequiredProcessed ? 'success' : 'active'}
format={() => `${processedRequired}/${totalRequired} обязательных`}
/>
{/* Список обработанных документов */}
{Object.keys(processedDocuments).length > 0 && (
<div style={{ marginTop: 16 }}>
{currentDocuments.map(doc =>
processedDocuments[doc.field] ? (
<div key={doc.field} style={{ marginBottom: 8, color: '#52c41a' }}>
<CheckCircleOutlined /> {doc.name} - Обработан
</div>
) : null
)}
</div>
))}
)}
</Card>
)}
{/* Прогресс обработки */}
{uploading && uploadProgress && (
<Alert
message={uploadProgress}
type="info"
showIcon
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
style={{ marginBottom: 16, marginTop: 16 }}
/>
{/* Текущий документ для загрузки */}
{eventType && currentDocConfig && (
<Card
title={`📋 Шаг ${currentDocumentIndex + 1}/${currentDocuments.length}: ${currentDocConfig.name}`}
style={{ marginTop: 24 }}
headStyle={{ background: currentDocConfig.required ? '#fff7e6' : '#f0f9ff', borderBottom: '2px solid #ffa940' }}
>
<div style={{ marginBottom: 16, padding: 12, background: '#e6f7ff', borderRadius: 8 }}>
<p style={{ margin: 0, fontSize: 13, color: '#0050b3' }}>
💡 {currentDocConfig.description}
</p>
{currentDocConfig.required && (
<p style={{ margin: '8px 0 0 0', fontSize: 12, color: '#d46b08' }}>
Этот документ обязательный
</p>
)}
</div>
<Upload
beforeUpload={handleFileSelect}
accept="image/*,.pdf,.heic,.heif,.webp"
maxCount={1}
showUploadList={true}
fileList={currentFile ? [{
uid: '-1',
name: currentFile.name,
status: 'done',
url: URL.createObjectURL(currentFile),
}] : []}
onRemove={() => setCurrentFile(null)}
>
<Button
icon={<UploadOutlined />}
size="large"
block
disabled={!!currentFile}
>
Выбрать файл (JPG, PNG, PDF, HEIC, макс 15MB)
</Button>
</Upload>
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<Button
type="primary"
size="large"
onClick={handleUploadAndProcess}
disabled={!currentFile || uploading}
loading={uploading}
style={{ flex: 1 }}
>
{uploading ? 'Обрабатываем...' : 'Загрузить и обработать'}
</Button>
{!currentDocConfig.required && (
<Button
size="large"
onClick={handleSkipOptionalDocument}
disabled={uploading}
>
Пропустить
</Button>
)}
</div>
</Card>
)}
{/* Если все документы обработаны или текущий индекс вышел за пределы */}
{eventType && currentDocumentIndex >= currentDocuments.length && (
<Card
style={{ marginTop: 24, background: '#f6ffed', borderColor: '#b7eb8f' }}
>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<CheckCircleOutlined style={{ fontSize: 48, color: '#52c41a', marginBottom: 16 }} />
<h3 style={{ color: '#52c41a' }}> Все документы обработаны!</h3>
<p style={{ color: '#666' }}>
Обработано обязательных документов: {processedRequired}/{totalRequired}
</p>
</div>
</Card>
)}
{/* Модальное окно обработки OCR */}
<Modal
open={ocrModalVisible}
closable={ocrModalContent !== 'loading'}
maskClosable={false}
footer={ocrModalContent === 'loading' ? null :
ocrModalContent?.success ? [
<Button key="next" type="primary" onClick={handleContinueToNextDocument}>
{currentDocumentIndex < currentDocuments.length - 1 ? 'Продолжить к следующему документу →' : 'Завершить загрузку документов'}
</Button>
] : [
<Button key="retry" type="primary" onClick={() => {
setOcrModalVisible(false);
setUploading(false);
}}>
Попробовать другой файл
</Button>
]
}
width={700}
centered
>
{ocrModalContent === 'loading' ? (
<div style={{ textAlign: 'center', padding: '40px 20px' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
<h3 style={{ marginTop: 24, marginBottom: 12 }}> Обрабатываем документ</h3>
<p style={{ color: '#666', marginBottom: 8 }}>📤 Загрузка в облако...</p>
<p style={{ color: '#666', marginBottom: 8 }}>📝 OCR распознавание текста...</p>
<p style={{ color: '#666', marginBottom: 8 }}>🤖 AI анализ документа...</p>
<p style={{ color: '#666' }}> Извлечение данных...</p>
<p style={{ color: '#999', fontSize: 12, marginTop: 20 }}>
Это может занять 20-30 секунд. Пожалуйста, подождите...
</p>
</div>
) : ocrModalContent ? (
<div>
<h3 style={{ marginBottom: 16 }}>
{ocrModalContent.success ? '✅ Результат обработки' : '❌ Ошибка обработки'}
</h3>
{ocrModalContent.success ? (
<div>
<p style={{ marginBottom: 16, fontSize: 15, fontWeight: 500 }}>
📋 {ocrModalContent.documentName}
</p>
<div style={{
padding: 16,
background: '#f6ffed',
borderRadius: 8,
border: '1px solid #b7eb8f',
marginBottom: 16
}}>
<p style={{ margin: '0 0 8px 0', color: '#52c41a', fontWeight: 500 }}>
Документ успешно распознан
</p>
<p style={{ margin: 0, fontSize: 13, color: '#666' }}>
Данные извлечены и сохранены
</p>
</div>
<p style={{ marginTop: 16 }}><strong>Извлечённые данные:</strong></p>
<pre style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
fontSize: 12,
maxHeight: 400,
overflow: 'auto'
}}>
{JSON.stringify(ocrModalContent.data, null, 2)}
</pre>
</div>
) : (
<div>
<p>{ocrModalContent.message || 'Документ не распознан'}</p>
<p style={{ marginTop: 16 }}><strong>Детали:</strong></p>
<pre style={{
background: '#fff3f3',
padding: 12,
borderRadius: 4,
fontSize: 12,
maxHeight: 400,
overflow: 'auto'
}}>
{JSON.stringify(ocrModalContent.data, null, 2)}
</pre>
</div>
)}
</div>
) : null}
</Modal>
{/* Кнопки навигации */}
<Form.Item>
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev} size="large" disabled={uploading}>Назад</Button>
<Button
type="primary"
onClick={handleNext}
loading={uploading}
onClick={handleFinishStep}
disabled={!allRequiredProcessed || uploading}
style={{ flex: 1 }}
size="large"
>
{uploading ? 'Обрабатываем...' : 'Далее'}
{allRequiredProcessed ? 'Далее →' : `Осталось документов: ${totalRequired - processedRequired}`}
</Button>
</div>
</Form.Item>
@@ -444,9 +615,12 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
<Button
type="dashed"
onClick={() => {
// Пропускаем валидацию, заполняем минимальные данные
const devData = {
eventType: 'delay_flight',
processedDocuments: {
boarding_or_ticket: { flight_number: 'DEV123', date: '2025-10-28' },
delay_confirmation: { delay_duration: '4h' }
}
};
updateFormData(devData);
onNext();

View File

@@ -379,3 +379,63 @@ export default function Step3Payment({
</Form>
);
}
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"
>
Назад (Step 2)
</Button>
<Button
type="dashed"
onClick={() => {
// Пропускаем валидацию телефона
setIsPhoneVerified(true);
const devData = {
fullName: 'Тест Тестов',
email: 'test@test.ru',
phone: '+79991234567',
paymentMethod: 'sbp',
bankName: 'sberbank',
};
updateFormData(devData);
message.success('DEV: Телефон автоматически подтверждён');
}}
size="small"
style={{ flex: 1 }}
>
Автоподтверждение телефона [dev]
</Button>
<Button
type="primary"
onClick={() => {
// Автоматически отправляем заявку
setIsPhoneVerified(true);
const devData = {
fullName: 'Тест Тестов',
email: 'test@test.ru',
phone: '+79991234567',
paymentMethod: 'sbp',
bankName: 'sberbank',
};
updateFormData(devData);
onSubmit();
}}
size="small"
>
🚀 Отправить [пропустить]
</Button>
</div>
</div>
</>
)}
</Form>
);
}