feat: Step2 переделан + улучшен Debug Panel с полными S3 URL

Step2Details (по скриншоту):
 Индикатор ' Полис найден' вверху
 Select с типами событий из erv_ticket:
   - Задержка авиарейса (более 3 часов)
   - Отмена авиарейса
   - Пропуск стыковочного рейса
   - Посадка на запасной аэродром
   - Задержка отправки поезда
   - Отмена поезда
   - Задержка/отмена парома/круизного судна

 Дата наступления страхового случая (DatePicker)
 Номер рейса/поезда/парома
 Загрузка подтверждающих документов:
   - Посадочный талон, билет, справка и т.д.
   - До 10 файлов по 15MB
   - HEIC, PDF, фото

Debug Panel улучшения:
 Полные S3 URL (не обрезанные)
 Кнопка '🔗 Открыть в новой вкладке'
 word-break: break-all для длинных URL
 Показывает все файлы из массива
 Для каждого файла:
   - Filename
   - File ID (UUID)
   - Size (KB)
   - Полный S3 URL (кликабельный)

Теперь в Debug видно КУДА загрузилось:
https://s3.twcstorage.ru/f9825c87-.../policies/20251024_213045_abc123_file.jpg

Можно кликнуть и посмотреть глазами! 👀
This commit is contained in:
AI Assistant
2025-10-25 09:27:56 +03:00
parent 720d4ebdd9
commit d2777aeabf
6 changed files with 219 additions and 95 deletions

View File

@@ -174,3 +174,4 @@ class OCRService:
# Глобальный экземпляр
ocr_service = OCRService()

View File

@@ -102,3 +102,4 @@ class S3Service:
# Глобальный экземпляр
s3_service = S3Service()

View File

@@ -24,3 +24,4 @@ COMMENT ON COLUMN claims_draft.session_id IS 'Уникальный ID сесси
COMMENT ON COLUMN claims_draft.current_step IS 'Номер шага где пользователь остановился';
COMMENT ON COLUMN claims_draft.form_data IS 'Все данные формы в JSON формате';

View File

@@ -186,34 +186,67 @@ export default function DebugPanel({ events, formData }: Props) {
</Descriptions>
)}
{event.type === 'upload' && (
<Descriptions size="small" column={1} bordered>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 10 }}
>
{event.data.file_id}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
>
{(event.data.size / 1024).toFixed(1)} KB
</Descriptions.Item>
{event.data.url && (
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>S3 URL</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', fontSize: 9 }}
>
<a href={event.data.url} target="_blank" rel="noopener noreferrer" style={{ color: '#4ec9b0' }}>
{event.data.url.substring(0, 50)}...
</a>
</Descriptions.Item>
)}
</Descriptions>
{event.type === 'upload' && event.data.files && (
<div>
{event.data.files.map((file: any, idx: number) => (
<Descriptions key={idx} size="small" column={1} bordered style={{ marginBottom: 8 }}>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>File</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#ce9178', fontSize: 10 }}
>
{file.filename}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 9, fontFamily: 'monospace' }}
>
{file.file_id}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
>
{(file.size / 1024).toFixed(1)} KB
</Descriptions.Item>
{file.url && (
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>S3 URL</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', fontSize: 9, wordBreak: 'break-all' }}
>
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#4ec9b0', textDecoration: 'underline' }}
>
{file.url}
</a>
<div style={{ marginTop: 4 }}>
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: 10,
color: '#569cd6',
background: '#1e1e1e',
padding: '2px 6px',
borderRadius: 3,
border: '1px solid #333'
}}
>
🔗 Открыть в новой вкладке
</a>
</div>
</Descriptions.Item>
)}
</Descriptions>
))}
</div>
)}
</div>
)}
@@ -234,3 +267,4 @@ export default function DebugPanel({ events, formData }: Props) {
);
}

View File

@@ -1,9 +1,9 @@
import { Form, Input, DatePicker, Select, Button, Upload, message } from 'antd';
import { Form, Input, Button, Select, DatePicker, Upload, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import { useState } from 'react';
import type { UploadFile } from 'antd/es/upload/interface';
import dayjs from 'dayjs';
const { TextArea } = Input;
const { Option } = Select;
interface Props {
@@ -11,53 +11,80 @@ interface Props {
updateFormData: (data: any) => void;
onNext: () => void;
onPrev: () => void;
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
export default function Step2Details({ formData, updateFormData, onNext, onPrev }: Props) {
// Типы страховых случаев из erv_ticket
const EVENT_TYPES = [
{ value: 'delay_flight', label: 'Задержка авиарейса (более 3 часов)' },
{ value: 'cancel_flight', label: 'Отмена авиарейса' },
{ value: 'miss_connection', label: 'Пропуск (задержка прибытия) стыковочного рейса (авиа/жд/паром и тд)' },
{ value: 'emergency_landing', label: 'Посадка воздушного судна на запасной аэродром' },
{ value: 'delay_train', label: 'Задержка отправки поезда' },
{ value: 'cancel_train', label: 'Отмена поезда' },
{ value: 'delay_ferry', label: 'Задержка/отмена отправки парома/круизного судна' },
];
export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) {
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const handleNext = async () => {
try {
const values = await form.validateFields();
updateFormData({
...values,
incidentDate: values.incidentDate?.format('YYYY-MM-DD'),
uploadedFiles: fileList.map(f => f.uid),
});
// Если есть файлы - загружаем
if (fileList.length > 0) {
setUploading(true);
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} документ(ов) в S3...`, {
count: fileList.length
});
const formData = new FormData();
fileList.forEach((file: any) => {
if (file.originFileObj) {
formData.append('files', file.originFileObj);
}
});
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();
if (uploadResult.success) {
addDebugEvent?.('upload', 'success', `✅ Документы загружены: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
files: uploadResult.files
});
updateFormData({
...values,
uploadedFiles: uploadResult.files
});
} else {
message.error('Ошибка загрузки документов');
setUploading(false);
return;
}
setUploading(false);
} else {
updateFormData(values);
}
onNext();
} catch (error) {
message.error('Заполните все обязательные поля');
setUploading(false);
}
};
const uploadProps = {
fileList,
beforeUpload: (file: File) => {
const isImage = file.type.startsWith('image/');
const isPDF = file.type === 'application/pdf';
if (!isImage && !isPDF) {
message.error('Можно загружать только изображения и PDF');
return false;
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('Файл должен быть меньше 10MB');
return false;
}
setFileList([...fileList, {
uid: Math.random().toString(),
name: file.name,
status: 'done',
url: URL.createObjectURL(file),
} as UploadFile]);
return false; // Отключаем автозагрузку
},
onRemove: (file: UploadFile) => {
setFileList(fileList.filter(f => f.uid !== file.uid));
},
const handleUploadChange = ({ fileList: newFileList }: any) => {
setFileList(newFileList);
};
return (
@@ -67,56 +94,115 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Дата происшествия"
name="incidentDate"
>
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
</Form.Item>
{/* Индикатор что полис найден */}
{formData.voucher && (
<div style={{
padding: 12,
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: 8,
marginBottom: 24,
color: '#52c41a',
fontWeight: 500
}}>
Полис найден
</div>
)}
<Form.Item
label="Тип транспорта"
name="transportType"
label="Выберите тип события"
name="eventType"
rules={[{ required: true, message: 'Выберите тип события' }]}
>
<Select placeholder="Выберите тип транспорта">
<Option value="air">Авиа</Option>
<Option value="train">Поезд</Option>
<Option value="bus">Автобус</Option>
<Option value="ship">Водный транспорт</Option>
<Option value="other">Другое</Option>
<Select
placeholder="Выберите тип события"
size="large"
>
{EVENT_TYPES.map(type => (
<Option key={type.value} value={type.value}>
{type.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
label="Описание происшествия"
name="incidentDescription"
label="Дата наступления страхового случая"
name="incidentDate"
rules={[{ required: true, message: 'Укажите дату' }]}
>
<TextArea
rows={4}
placeholder="Опишите что произошло..."
maxLength={1000}
showCount
<DatePicker
placeholder="Выберите дату"
size="large"
style={{ width: '100%' }}
format="DD.MM.YYYY"
disabledDate={(current) => current && current > dayjs().endOf('day')}
/>
</Form.Item>
<Form.Item label="Документы (билеты, справки, чеки)">
<Upload {...uploadProps} listType="picture">
<Button icon={<UploadOutlined />}>Загрузить файлы</Button>
<Form.Item
label="Номер рейса/поезда/парома"
name="transportNumber"
rules={[{ required: true, message: 'Введите номер' }]}
>
<Input
placeholder="Введите номер"
size="large"
/>
</Form.Item>
<Form.Item
label="Подтверждающие документы"
name="documents"
tooltip="Посадочный талон, билет, справка о задержке и т.д."
>
<Upload
listType="picture"
fileList={fileList}
onChange={handleUploadChange}
beforeUpload={(file) => {
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 файлов');
return Upload.LIST_IGNORE;
}
return false;
}}
accept="image/*,.pdf,.heic,.heif"
multiple
maxCount={10}
showUploadList={{
showPreviewIcon: true,
showRemoveIcon: true,
}}
>
<Button icon={<UploadOutlined />} size="large" block disabled={fileList.length >= 10}>
Загрузить файлы (до 10 шт, макс 15MB каждый)
</Button>
</Upload>
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
Максимум 10 MB на файл. Форматы: JPG, PNG, PDF, HEIC
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
Загружено: {fileList.length}/10 файлов
</div>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', gap: 8 }}>
<Button onClick={onPrev}>Назад</Button>
<Button type="primary" onClick={handleNext} style={{ flex: 1 }}>
Далее
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev} size="large">Назад</Button>
<Button
type="primary"
onClick={handleNext}
loading={uploading}
style={{ flex: 1 }}
size="large"
>
{uploading ? 'Загрузка документов...' : 'Далее'}
</Button>
</div>
</Form.Item>
</Form>
);
}

View File

@@ -125,6 +125,7 @@ export default function ClaimForm() {
updateFormData={updateFormData}
onNext={nextStep}
onPrev={prevStep}
addDebugEvent={addDebugEvent}
/>
),
},