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:
@@ -174,3 +174,4 @@ class OCRService:
|
|||||||
# Глобальный экземпляр
|
# Глобальный экземпляр
|
||||||
ocr_service = OCRService()
|
ocr_service = OCRService()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -102,3 +102,4 @@ class S3Service:
|
|||||||
# Глобальный экземпляр
|
# Глобальный экземпляр
|
||||||
s3_service = S3Service()
|
s3_service = S3Service()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.current_step IS 'Номер шага где пользователь остановился';
|
||||||
COMMENT ON COLUMN claims_draft.form_data IS 'Все данные формы в JSON формате';
|
COMMENT ON COLUMN claims_draft.form_data IS 'Все данные формы в JSON формате';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -186,34 +186,67 @@ export default function DebugPanel({ events, formData }: Props) {
|
|||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{event.type === 'upload' && (
|
{event.type === 'upload' && event.data.files && (
|
||||||
<Descriptions size="small" column={1} bordered>
|
<div>
|
||||||
<Descriptions.Item
|
{event.data.files.map((file: any, idx: number) => (
|
||||||
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
|
<Descriptions key={idx} size="small" column={1} bordered style={{ marginBottom: 8 }}>
|
||||||
labelStyle={{ background: '#252526' }}
|
<Descriptions.Item
|
||||||
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 10 }}
|
label={<span style={{ color: '#9cdcfe' }}>File</span>}
|
||||||
>
|
labelStyle={{ background: '#252526' }}
|
||||||
{event.data.file_id}
|
contentStyle={{ background: '#1e1e1e', color: '#ce9178', fontSize: 10 }}
|
||||||
</Descriptions.Item>
|
>
|
||||||
<Descriptions.Item
|
{file.filename}
|
||||||
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
|
</Descriptions.Item>
|
||||||
labelStyle={{ background: '#252526' }}
|
<Descriptions.Item
|
||||||
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
|
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
|
||||||
>
|
labelStyle={{ background: '#252526' }}
|
||||||
{(event.data.size / 1024).toFixed(1)} KB
|
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 9, fontFamily: 'monospace' }}
|
||||||
</Descriptions.Item>
|
>
|
||||||
{event.data.url && (
|
{file.file_id}
|
||||||
<Descriptions.Item
|
</Descriptions.Item>
|
||||||
label={<span style={{ color: '#9cdcfe' }}>S3 URL</span>}
|
<Descriptions.Item
|
||||||
labelStyle={{ background: '#252526' }}
|
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
|
||||||
contentStyle={{ background: '#1e1e1e', fontSize: 9 }}
|
labelStyle={{ background: '#252526' }}
|
||||||
>
|
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
|
||||||
<a href={event.data.url} target="_blank" rel="noopener noreferrer" style={{ color: '#4ec9b0' }}>
|
>
|
||||||
{event.data.url.substring(0, 50)}...
|
{(file.size / 1024).toFixed(1)} KB
|
||||||
</a>
|
</Descriptions.Item>
|
||||||
</Descriptions.Item>
|
{file.url && (
|
||||||
)}
|
<Descriptions.Item
|
||||||
</Descriptions>
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -234,3 +267,4 @@ export default function DebugPanel({ events, formData }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -11,53 +11,80 @@ interface Props {
|
|||||||
updateFormData: (data: any) => void;
|
updateFormData: (data: any) => void;
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onPrev: () => 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 [form] = Form.useForm();
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
updateFormData({
|
|
||||||
...values,
|
// Если есть файлы - загружаем
|
||||||
incidentDate: values.incidentDate?.format('YYYY-MM-DD'),
|
if (fileList.length > 0) {
|
||||||
uploadedFiles: fileList.map(f => f.uid),
|
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();
|
onNext();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Заполните все обязательные поля');
|
message.error('Заполните все обязательные поля');
|
||||||
|
setUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadProps = {
|
const handleUploadChange = ({ fileList: newFileList }: any) => {
|
||||||
fileList,
|
setFileList(newFileList);
|
||||||
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));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,56 +94,115 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev
|
|||||||
initialValues={formData}
|
initialValues={formData}
|
||||||
style={{ marginTop: 24 }}
|
style={{ marginTop: 24 }}
|
||||||
>
|
>
|
||||||
<Form.Item
|
{/* Индикатор что полис найден */}
|
||||||
label="Дата происшествия"
|
{formData.voucher && (
|
||||||
name="incidentDate"
|
<div style={{
|
||||||
>
|
padding: 12,
|
||||||
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
background: '#f6ffed',
|
||||||
</Form.Item>
|
border: '1px solid #b7eb8f',
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 24,
|
||||||
|
color: '#52c41a',
|
||||||
|
fontWeight: 500
|
||||||
|
}}>
|
||||||
|
✅ Полис найден
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Тип транспорта"
|
label="Выберите тип события"
|
||||||
name="transportType"
|
name="eventType"
|
||||||
|
rules={[{ required: true, message: 'Выберите тип события' }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="Выберите тип транспорта">
|
<Select
|
||||||
<Option value="air">Авиа</Option>
|
placeholder="Выберите тип события"
|
||||||
<Option value="train">Поезд</Option>
|
size="large"
|
||||||
<Option value="bus">Автобус</Option>
|
>
|
||||||
<Option value="ship">Водный транспорт</Option>
|
{EVENT_TYPES.map(type => (
|
||||||
<Option value="other">Другое</Option>
|
<Option key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Описание происшествия"
|
label="Дата наступления страхового случая"
|
||||||
name="incidentDescription"
|
name="incidentDate"
|
||||||
|
rules={[{ required: true, message: 'Укажите дату' }]}
|
||||||
>
|
>
|
||||||
<TextArea
|
<DatePicker
|
||||||
rows={4}
|
placeholder="Выберите дату"
|
||||||
placeholder="Опишите что произошло..."
|
size="large"
|
||||||
maxLength={1000}
|
style={{ width: '100%' }}
|
||||||
showCount
|
format="DD.MM.YYYY"
|
||||||
|
disabledDate={(current) => current && current > dayjs().endOf('day')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Документы (билеты, справки, чеки)">
|
<Form.Item
|
||||||
<Upload {...uploadProps} listType="picture">
|
label="Номер рейса/поезда/парома"
|
||||||
<Button icon={<UploadOutlined />}>Загрузить файлы</Button>
|
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>
|
</Upload>
|
||||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
||||||
Максимум 10 MB на файл. Форматы: JPG, PNG, PDF, HEIC
|
Загружено: {fileList.length}/10 файлов
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
||||||
<Button onClick={onPrev}>Назад</Button>
|
<Button onClick={onPrev} size="large">Назад</Button>
|
||||||
<Button type="primary" onClick={handleNext} style={{ flex: 1 }}>
|
<Button
|
||||||
Далее
|
type="primary"
|
||||||
|
onClick={handleNext}
|
||||||
|
loading={uploading}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{uploading ? 'Загрузка документов...' : 'Далее'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export default function ClaimForm() {
|
|||||||
updateFormData={updateFormData}
|
updateFormData={updateFormData}
|
||||||
onNext={nextStep}
|
onNext={nextStep}
|
||||||
onPrev={prevStep}
|
onPrev={prevStep}
|
||||||
|
addDebugEvent={addDebugEvent}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user