2025-10-28 12:03:12 +03:00
|
|
|
|
import { Form, Button, Select, Upload, message, Spin, Alert, Card } from 'antd';
|
|
|
|
|
|
import { UploadOutlined, LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
2025-10-25 09:27:56 +03:00
|
|
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
formData: any;
|
|
|
|
|
|
updateFormData: (data: any) => void;
|
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
|
onPrev: () => void;
|
2025-10-25 09:27:56 +03:00
|
|
|
|
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
|
2025-10-24 16:19:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Типы страховых случаев
|
2025-10-25 09:27:56 +03:00
|
|
|
|
const EVENT_TYPES = [
|
|
|
|
|
|
{ value: 'delay_flight', label: 'Задержка авиарейса (более 3 часов)' },
|
|
|
|
|
|
{ value: 'cancel_flight', label: 'Отмена авиарейса' },
|
2025-10-28 12:03:12 +03:00
|
|
|
|
{ value: 'miss_connection', label: 'Пропуск (задержка прибытия) стыковочного рейса' },
|
2025-10-25 09:27:56 +03:00
|
|
|
|
{ value: 'emergency_landing', label: 'Посадка воздушного судна на запасной аэродром' },
|
|
|
|
|
|
{ value: 'delay_train', label: 'Задержка отправки поезда' },
|
|
|
|
|
|
{ value: 'cancel_train', label: 'Отмена поезда' },
|
|
|
|
|
|
{ value: 'delay_ferry', label: 'Задержка/отмена отправки парома/круизного судна' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Конфигурация документов для каждого типа события с уникальными file_type
|
|
|
|
|
|
const DOCUMENT_CONFIGS: Record<string, any[]> = {
|
|
|
|
|
|
delay_flight: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Посадочный талон или Билет",
|
|
|
|
|
|
field: "boarding_or_ticket",
|
|
|
|
|
|
file_type: "flight_delay_boarding_or_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Boarding pass или ticket/booking confirmation"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Подтверждение задержки",
|
|
|
|
|
|
field: "delay_confirmation",
|
|
|
|
|
|
file_type: "flight_delay_confirmation",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Справка от АК, email/SMS, или фото табло"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
cancel_flight: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Билет",
|
|
|
|
|
|
field: "ticket",
|
|
|
|
|
|
file_type: "flight_cancel_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Ticket/booking confirmation"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Уведомление об отмене",
|
|
|
|
|
|
field: "cancellation_notice",
|
|
|
|
|
|
file_type: "flight_cancel_notice",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Email, SMS или скриншот из приложения АК"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
miss_connection: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Посадочный талон рейса ПРИБЫТИЯ",
|
|
|
|
|
|
field: "arrival_boarding",
|
|
|
|
|
|
file_type: "connection_arrival_boarding",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Boarding pass рейса, который задержался"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Посадочный талон ИЛИ Билет рейса ОТПРАВЛЕНИЯ",
|
|
|
|
|
|
field: "departure_boarding_or_ticket",
|
|
|
|
|
|
file_type: "connection_departure_boarding_or_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Boarding pass (если успели) ИЛИ билет (если не успели)"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Доказательство задержки (опционально)",
|
|
|
|
|
|
field: "delay_proof",
|
|
|
|
|
|
file_type: "connection_delay_proof",
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
maxFiles: 5,
|
|
|
|
|
|
description: "Справка, фото табло, email/SMS"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
delay_train: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Билет на поезд",
|
|
|
|
|
|
field: "train_ticket",
|
|
|
|
|
|
file_type: "train_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Билет РЖД или другого перевозчика"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Подтверждение задержки",
|
|
|
|
|
|
field: "delay_proof",
|
|
|
|
|
|
file_type: "train_delay_proof",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Справка от РЖД, фото табло, скриншот приложения"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
cancel_train: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Билет на поезд",
|
|
|
|
|
|
field: "train_ticket",
|
|
|
|
|
|
file_type: "train_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Билет РЖД или другого перевозчика"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Подтверждение отмены",
|
|
|
|
|
|
field: "cancel_proof",
|
|
|
|
|
|
file_type: "train_cancel_proof",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Справка от РЖД, фото табло, скриншот приложения"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
delay_ferry: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Билет на паром/круиз",
|
|
|
|
|
|
field: "ferry_ticket",
|
|
|
|
|
|
file_type: "ferry_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Билет или booking confirmation"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Подтверждение задержки/отмены",
|
|
|
|
|
|
field: "delay_proof",
|
|
|
|
|
|
file_type: "ferry_delay_proof",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Справка от перевозчика, фото расписания, email/SMS"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
emergency_landing: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Посадочный талон или Билет",
|
|
|
|
|
|
field: "boarding_or_ticket",
|
|
|
|
|
|
file_type: "emergency_boarding_or_ticket",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
description: "Boarding pass или ticket"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Подтверждение посадки на запасной аэродром",
|
|
|
|
|
|
field: "emergency_proof",
|
|
|
|
|
|
file_type: "emergency_landing_proof",
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
maxFiles: 3,
|
|
|
|
|
|
description: "Справка от АК, email/SMS, документы"
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-25 09:27:56 +03:00
|
|
|
|
export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) {
|
2025-10-24 16:19:58 +03:00
|
|
|
|
const [form] = Form.useForm();
|
2025-10-28 12:03:12 +03:00
|
|
|
|
const [eventType, setEventType] = useState(formData.eventType || '');
|
|
|
|
|
|
const [documentFiles, setDocumentFiles] = useState<Record<string, UploadFile[]>>({});
|
2025-10-25 09:27:56 +03:00
|
|
|
|
const [uploading, setUploading] = useState(false);
|
2025-10-27 08:33:16 +03:00
|
|
|
|
const [uploadProgress, setUploadProgress] = useState('');
|
2025-10-28 12:03:12 +03:00
|
|
|
|
const [waitingForOcr, setWaitingForOcr] = useState(false);
|
|
|
|
|
|
const [ocrResults, setOcrResults] = useState<any>(null);
|
|
|
|
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const handleEventTypeChange = (value: string) => {
|
|
|
|
|
|
setEventType(value);
|
|
|
|
|
|
setDocumentFiles({}); // Очищаем загруженные файлы при смене типа
|
|
|
|
|
|
form.setFieldValue('eventType', value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем конфигурацию документов для выбранного типа события
|
|
|
|
|
|
const currentDocuments = eventType ? DOCUMENT_CONFIGS[eventType] || [] : [];
|
|
|
|
|
|
|
|
|
|
|
|
const handleUploadChange = (field: string, { fileList: newFileList }: any) => {
|
|
|
|
|
|
setDocumentFiles(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[field]: newFileList
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
|
|
|
|
|
const handleNext = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields();
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Проверяем что все обязательные документы загружены
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// Загружаем все документы в S3 через n8n
|
|
|
|
|
|
setUploading(true);
|
|
|
|
|
|
setUploadProgress('📤 Загружаем документы...');
|
|
|
|
|
|
|
|
|
|
|
|
const claimId = formData.claim_id;
|
|
|
|
|
|
const uploadedFiles: any[] = [];
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
for (const docConfig of currentDocuments) {
|
|
|
|
|
|
const files = documentFiles[docConfig.field] || [];
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
|
|
const file = files[i];
|
2025-10-27 08:33:16 +03:00
|
|
|
|
if (!file.originFileObj) continue;
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
setUploadProgress(`📡 Загружаем: ${docConfig.name} (${i + 1}/${files.length})...`);
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
|
|
|
|
|
const uploadFormData = new FormData();
|
|
|
|
|
|
uploadFormData.append('claim_id', claimId);
|
2025-10-28 12:03:12 +03:00
|
|
|
|
uploadFormData.append('file_type', docConfig.file_type); // 🔑 Уникальный file_type для n8n
|
2025-10-27 08:33:16 +03:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
addDebugEvent?.('upload', 'pending', `📤 Загружаю документ: ${docConfig.name} (${docConfig.file_type})`, {
|
|
|
|
|
|
file_type: docConfig.file_type,
|
|
|
|
|
|
filename: file.name
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-27 08:33:16 +03:00
|
|
|
|
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;
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
2025-10-27 08:33:16 +03:00
|
|
|
|
if (resultData?.success) {
|
|
|
|
|
|
uploadedFiles.push({
|
|
|
|
|
|
filename: file.name,
|
2025-10-28 12:03:12 +03:00
|
|
|
|
file_type: docConfig.file_type,
|
|
|
|
|
|
field: docConfig.field,
|
2025-10-27 08:33:16 +03:00
|
|
|
|
success: true
|
|
|
|
|
|
});
|
2025-10-28 12:03:12 +03:00
|
|
|
|
|
|
|
|
|
|
addDebugEvent?.('upload', 'success', `✅ Документ загружен: ${docConfig.name}`, {
|
|
|
|
|
|
file_type: docConfig.file_type,
|
|
|
|
|
|
filename: file.name
|
|
|
|
|
|
});
|
2025-10-27 08:33:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-28 12:03:12 +03:00
|
|
|
|
}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
if (uploadedFiles.length > 0) {
|
|
|
|
|
|
setUploadProgress('🤖 AI анализирует документы...');
|
|
|
|
|
|
|
|
|
|
|
|
updateFormData({
|
|
|
|
|
|
...values,
|
|
|
|
|
|
uploadedDocuments: uploadedFiles
|
|
|
|
|
|
});
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
// TODO: Здесь будет ожидание SSE события с результатами OCR/AI
|
|
|
|
|
|
// Пока просто переходим дальше
|
2025-10-27 08:33:16 +03:00
|
|
|
|
setUploadProgress('');
|
2025-10-28 12:03:12 +03:00
|
|
|
|
setUploading(false);
|
|
|
|
|
|
|
|
|
|
|
|
message.success(`Загружено документов: ${uploadedFiles.length}. Переходим дальше...`);
|
|
|
|
|
|
onNext();
|
2025-10-25 09:27:56 +03:00
|
|
|
|
} else {
|
2025-10-28 12:03:12 +03:00
|
|
|
|
message.error('Не удалось загрузить документы');
|
|
|
|
|
|
setUploading(false);
|
|
|
|
|
|
setUploadProgress('');
|
2025-10-25 09:27:56 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('Заполните все обязательные поля');
|
2025-10-25 09:27:56 +03:00
|
|
|
|
setUploading(false);
|
2025-10-27 08:33:16 +03:00
|
|
|
|
setUploadProgress('');
|
2025-10-24 16:19:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
initialValues={formData}
|
|
|
|
|
|
style={{ marginTop: 24 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form.Item
|
2025-10-25 09:27:56 +03:00
|
|
|
|
label="Выберите тип события"
|
|
|
|
|
|
name="eventType"
|
|
|
|
|
|
rules={[{ required: true, message: 'Выберите тип события' }]}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="Выберите тип события"
|
|
|
|
|
|
size="large"
|
2025-10-25 10:12:41 +03:00
|
|
|
|
onChange={handleEventTypeChange}
|
2025-10-25 09:27:56 +03:00
|
|
|
|
>
|
|
|
|
|
|
{EVENT_TYPES.map(type => (
|
|
|
|
|
|
<Option key={type.value} value={type.value}>
|
|
|
|
|
|
{type.label}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
{/* Показываем документы только после выбора типа события */}
|
|
|
|
|
|
{eventType && currentDocuments.length > 0 && (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
title="📋 Загрузите документы для обработки"
|
|
|
|
|
|
style={{ marginTop: 24 }}
|
|
|
|
|
|
headStyle={{ background: '#f0f9ff', borderBottom: '2px solid #91d5ff' }}
|
2025-10-25 10:12:41 +03:00
|
|
|
|
>
|
2025-10-28 12:03:12 +03:00
|
|
|
|
<div style={{ marginBottom: 16, padding: 12, background: '#e6f7ff', borderRadius: 8 }}>
|
|
|
|
|
|
<p style={{ margin: 0, fontSize: 13, color: '#0050b3' }}>
|
|
|
|
|
|
💡 Просто загрузите документы — наш AI автоматически распознает все данные
|
|
|
|
|
|
(номера рейсов, даты, время, причины задержек)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2025-10-25 10:12:41 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
{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>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
<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>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-28 12:03:12 +03:00
|
|
|
|
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
|
|
|
|
|
Загружено: {(documentFiles[doc.field] || []).length}/{doc.maxFiles} файл(ов)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Card>
|
2025-10-25 10:12:41 +03:00
|
|
|
|
)}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-27 08:33:16 +03:00
|
|
|
|
{/* Прогресс обработки */}
|
|
|
|
|
|
{uploading && uploadProgress && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={uploadProgress}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
|
|
|
|
|
|
style={{ marginBottom: 16, marginTop: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
<Form.Item>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
<Button onClick={onPrev} size="large" disabled={uploading}>Назад</Button>
|
2025-10-25 09:27:56 +03:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
|
loading={uploading}
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
>
|
2025-10-27 08:33:16 +03:00
|
|
|
|
{uploading ? 'Обрабатываем...' : 'Далее'}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form.Item>
|
2025-10-28 10:13:18 +03:00
|
|
|
|
|
|
|
|
|
|
{/* 🔧 Технические кнопки для разработки */}
|
|
|
|
|
|
<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"
|
|
|
|
|
|
disabled={uploading}
|
|
|
|
|
|
>
|
|
|
|
|
|
← Назад (Step 1)
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="dashed"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
// Пропускаем валидацию, заполняем минимальные данные
|
|
|
|
|
|
const devData = {
|
|
|
|
|
|
eventType: 'delay_flight',
|
|
|
|
|
|
};
|
|
|
|
|
|
updateFormData(devData);
|
|
|
|
|
|
onNext();
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Далее → (Step 3) [пропустить]
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|