fix: Исправление загрузки документов и SQL запросов
- Исправлена потеря документов при обновлении черновика (SQL объединяет вместо перезаписи) - Исправлено определение типа документа (приоритет field_label над field_name) - Исправлены дубликаты в documents_meta и documents_uploaded - Добавлена передача group_index с фронтенда для правильного field_name - Исправлены все документы в таблице clpr_claim_documents с правильными field_name - Обновлены SQL запросы: claimsave и claimsave_final для нового флоу - Добавлена поддержка multi-file upload для одного документа - Исправлены дубликаты в списке загруженных документов на фронтенде Файлы: - SQL: SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql, SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql - n8n: N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js (поддержка group_index) - Backend: documents.py (передача group_index в n8n) - Frontend: StepWizardPlan.tsx (передача group_index, исправление дубликатов) - Скрипты: fix_claim_documents_field_names.py, fix_documents_meta_duplicates.py Результат: документы больше не теряются, имеют правильные типы и field_name
This commit is contained in:
@@ -1,7 +1,37 @@
|
||||
/**
|
||||
* StepDraftSelection.tsx
|
||||
*
|
||||
* Выбор черновика с поддержкой разных статусов:
|
||||
* - draft_new: только описание
|
||||
* - draft_docs_progress: часть документов загружена
|
||||
* - draft_docs_complete: все документы, ждём заявление
|
||||
* - draft_claim_ready: заявление готово
|
||||
* - awaiting_sms: ждёт SMS подтверждения
|
||||
* - legacy: старый формат (без documents_required)
|
||||
*
|
||||
* @version 2.0
|
||||
* @date 2025-11-26
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Card, List, Typography, Space, Empty, Popconfirm, message, Spin, Tag } from 'antd';
|
||||
import { FileTextOutlined, DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
// Форматирование даты без date-fns (если библиотека не установлена)
|
||||
import { Button, Card, List, Typography, Space, Empty, Popconfirm, message, Spin, Tag, Alert, Progress, Tooltip } from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
ClockCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
LoadingOutlined,
|
||||
UploadOutlined,
|
||||
FileSearchOutlined,
|
||||
MobileOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
// Форматирование даты
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
@@ -16,35 +46,129 @@ const formatDate = (dateStr: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
// Относительное время
|
||||
const getRelativeTime = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'только что';
|
||||
if (diffMins < 60) return `${diffMins} мин. назад`;
|
||||
if (diffHours < 24) return `${diffHours} ч. назад`;
|
||||
if (diffDays < 7) return `${diffDays} дн. назад`;
|
||||
return formatDate(dateStr);
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
interface Draft {
|
||||
id: string;
|
||||
claim_id: string;
|
||||
session_token: string;
|
||||
status_code: string;
|
||||
channel: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
problem_description?: string;
|
||||
wizard_plan: boolean;
|
||||
wizard_answers: boolean;
|
||||
has_documents: boolean;
|
||||
// Новые поля для нового флоу
|
||||
documents_total?: number;
|
||||
documents_uploaded?: number;
|
||||
documents_skipped?: number;
|
||||
wizard_ready?: boolean;
|
||||
claim_ready?: boolean;
|
||||
is_legacy?: boolean; // Старый формат без documents_required
|
||||
}
|
||||
|
||||
interface Props {
|
||||
phone?: string;
|
||||
session_id?: string;
|
||||
unified_id?: string; // ✅ Добавляем unified_id
|
||||
unified_id?: string;
|
||||
onSelectDraft: (claimId: string) => void;
|
||||
onNewClaim: () => void;
|
||||
onRestartDraft?: (claimId: string, description: string) => void; // Для legacy черновиков
|
||||
}
|
||||
|
||||
// === Конфиг статусов ===
|
||||
const STATUS_CONFIG: Record<string, {
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
description: string;
|
||||
action: string;
|
||||
}> = {
|
||||
draft: {
|
||||
color: 'default',
|
||||
icon: <FileTextOutlined />,
|
||||
label: 'Черновик',
|
||||
description: 'Начато заполнение',
|
||||
action: 'Продолжить',
|
||||
},
|
||||
draft_new: {
|
||||
color: 'blue',
|
||||
icon: <FileTextOutlined />,
|
||||
label: 'Новый',
|
||||
description: 'Только описание проблемы',
|
||||
action: 'Загрузить документы',
|
||||
},
|
||||
draft_docs_progress: {
|
||||
color: 'processing',
|
||||
icon: <UploadOutlined />,
|
||||
label: 'Загрузка документов',
|
||||
description: 'Часть документов загружена',
|
||||
action: 'Продолжить загрузку',
|
||||
},
|
||||
draft_docs_complete: {
|
||||
color: 'orange',
|
||||
icon: <LoadingOutlined />,
|
||||
label: 'Обработка',
|
||||
description: 'Формируется заявление...',
|
||||
action: 'Ожидайте',
|
||||
},
|
||||
draft_claim_ready: {
|
||||
color: 'green',
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: 'Готово к отправке',
|
||||
description: 'Заявление готово',
|
||||
action: 'Просмотреть и отправить',
|
||||
},
|
||||
awaiting_sms: {
|
||||
color: 'volcano',
|
||||
icon: <MobileOutlined />,
|
||||
label: 'Ожидает подтверждения',
|
||||
description: 'Введите SMS код',
|
||||
action: 'Подтвердить',
|
||||
},
|
||||
in_work: {
|
||||
color: 'cyan',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: 'В работе',
|
||||
description: 'Заявка на рассмотрении',
|
||||
action: 'Просмотреть',
|
||||
},
|
||||
legacy: {
|
||||
color: 'warning',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
label: 'Устаревший формат',
|
||||
description: 'Требуется обновление',
|
||||
action: 'Начать заново',
|
||||
},
|
||||
};
|
||||
|
||||
export default function StepDraftSelection({
|
||||
phone,
|
||||
session_id,
|
||||
unified_id, // ✅ Добавляем unified_id
|
||||
unified_id,
|
||||
onSelectDraft,
|
||||
onNewClaim,
|
||||
onRestartDraft,
|
||||
}: Props) {
|
||||
const [drafts, setDrafts] = useState<Draft[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -54,7 +178,7 @@ export default function StepDraftSelection({
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
// ✅ Приоритет: unified_id > phone > session_id
|
||||
|
||||
if (unified_id) {
|
||||
params.append('unified_id', unified_id);
|
||||
console.log('🔍 StepDraftSelection: загружаем черновики по unified_id:', unified_id);
|
||||
@@ -76,8 +200,22 @@ export default function StepDraftSelection({
|
||||
|
||||
const data = await response.json();
|
||||
console.log('🔍 StepDraftSelection: ответ API:', data);
|
||||
console.log('🔍 StepDraftSelection: количество черновиков:', data.count);
|
||||
setDrafts(data.drafts || []);
|
||||
|
||||
// Определяем legacy черновики (без documents_required в payload)
|
||||
const processedDrafts = (data.drafts || []).map((draft: Draft) => {
|
||||
// Legacy только если:
|
||||
// 1. Статус 'draft' (старый формат) ИЛИ
|
||||
// 2. Нет новых статусов (draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready)
|
||||
// И есть wizard_plan (старый формат)
|
||||
const isNewFlowStatus = ['draft_new', 'draft_docs_progress', 'draft_docs_complete', 'draft_claim_ready'].includes(draft.status_code || '');
|
||||
const isLegacy = !isNewFlowStatus && draft.wizard_plan && draft.status_code === 'draft';
|
||||
return {
|
||||
...draft,
|
||||
is_legacy: isLegacy,
|
||||
};
|
||||
});
|
||||
|
||||
setDrafts(processedDrafts);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки черновиков:', error);
|
||||
message.error('Не удалось загрузить список черновиков');
|
||||
@@ -88,7 +226,7 @@ export default function StepDraftSelection({
|
||||
|
||||
useEffect(() => {
|
||||
loadDrafts();
|
||||
}, [phone, session_id, unified_id]); // ✅ Добавляем unified_id в зависимости
|
||||
}, [phone, session_id, unified_id]);
|
||||
|
||||
const handleDelete = async (claimId: string) => {
|
||||
try {
|
||||
@@ -111,14 +249,56 @@ export default function StepDraftSelection({
|
||||
}
|
||||
};
|
||||
|
||||
// Получение конфига статуса
|
||||
const getStatusConfig = (draft: Draft) => {
|
||||
if (draft.is_legacy) {
|
||||
return STATUS_CONFIG.legacy;
|
||||
}
|
||||
return STATUS_CONFIG[draft.status_code] || STATUS_CONFIG.draft;
|
||||
};
|
||||
|
||||
const getProgressInfo = (draft: Draft) => {
|
||||
const parts: string[] = [];
|
||||
if (draft.problem_description) parts.push('Описание');
|
||||
if (draft.wizard_plan) parts.push('План вопросов');
|
||||
if (draft.wizard_answers) parts.push('Ответы');
|
||||
if (draft.has_documents) parts.push('Документы');
|
||||
return parts.length > 0 ? parts.join(', ') : 'Начато';
|
||||
// Прогресс документов
|
||||
const getDocsProgress = (draft: Draft) => {
|
||||
if (!draft.documents_total) return null;
|
||||
const uploaded = draft.documents_uploaded || 0;
|
||||
const skipped = draft.documents_skipped || 0;
|
||||
const total = draft.documents_total;
|
||||
const percent = Math.round(((uploaded + skipped) / total) * 100);
|
||||
return { uploaded, skipped, total, percent };
|
||||
};
|
||||
|
||||
// Обработка клика на черновик
|
||||
const handleDraftAction = (draft: Draft) => {
|
||||
const draftId = draft.claim_id || draft.id;
|
||||
|
||||
if (draft.is_legacy && onRestartDraft) {
|
||||
// Legacy черновик - предлагаем начать заново с тем же описанием
|
||||
onRestartDraft(draftId, draft.problem_description || '');
|
||||
} else if (draft.status_code === 'draft_docs_complete') {
|
||||
// Всё ещё обрабатывается - показываем сообщение
|
||||
message.info('Заявление формируется. Пожалуйста, подождите.');
|
||||
} else {
|
||||
// Обычный переход
|
||||
onSelectDraft(draftId);
|
||||
}
|
||||
};
|
||||
|
||||
// Кнопка действия
|
||||
const getActionButton = (draft: Draft) => {
|
||||
const config = getStatusConfig(draft);
|
||||
const isProcessing = draft.status_code === 'draft_docs_complete';
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={isProcessing ? 'default' : 'primary'}
|
||||
onClick={() => handleDraftAction(draft)}
|
||||
icon={config.icon}
|
||||
disabled={isProcessing}
|
||||
loading={isProcessing}
|
||||
>
|
||||
{config.action}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -133,10 +313,10 @@ export default function StepDraftSelection({
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={2} style={{ marginBottom: 8, color: '#1890ff' }}>
|
||||
📋 Ваши черновики заявок
|
||||
📋 Ваши заявки
|
||||
</Title>
|
||||
<Paragraph type="secondary" style={{ fontSize: 14, marginBottom: 16 }}>
|
||||
Выберите черновик, чтобы продолжить заполнение, или создайте новую заявку.
|
||||
Выберите заявку для продолжения или создайте новую.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +326,7 @@ export default function StepDraftSelection({
|
||||
</div>
|
||||
) : drafts.length === 0 ? (
|
||||
<Empty
|
||||
description="У вас нет незавершенных черновиков"
|
||||
description="У вас нет незавершенных заявок"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onNewClaim} size="large">
|
||||
@@ -157,89 +337,146 @@ export default function StepDraftSelection({
|
||||
<>
|
||||
<List
|
||||
dataSource={drafts}
|
||||
renderItem={(draft) => (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
background: '#fff',
|
||||
}}
|
||||
actions={[
|
||||
<Button
|
||||
key="continue"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
console.log('🔍 Выбран черновик:', draft.claim_id, 'id:', draft.id);
|
||||
// Используем id (UUID) если claim_id отсутствует
|
||||
const draftId = draft.claim_id || draft.id;
|
||||
console.log('🔍 Загружаем черновик с ID:', draftId);
|
||||
onSelectDraft(draftId);
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
Продолжить
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="Удалить черновик?"
|
||||
description="Это действие нельзя отменить"
|
||||
onConfirm={() => handleDelete(draft.claim_id!)}
|
||||
okText="Да, удалить"
|
||||
cancelText="Отмена"
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === draft.claim_id}
|
||||
disabled={deletingId === draft.claim_id}
|
||||
renderItem={(draft) => {
|
||||
const config = getStatusConfig(draft);
|
||||
const docsProgress = getDocsProgress(draft);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '16px',
|
||||
border: `1px solid ${draft.is_legacy ? '#faad14' : '#d9d9d9'}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
background: draft.is_legacy ? '#fffbe6' : '#fff',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
actions={[
|
||||
getActionButton(draft),
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="Удалить заявку?"
|
||||
description="Это действие нельзя отменить"
|
||||
onConfirm={() => handleDelete(draft.claim_id || draft.id)}
|
||||
okText="Да, удалить"
|
||||
cancelText="Отмена"
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<FileTextOutlined style={{ fontSize: 24, color: '#595959' }} />}
|
||||
title={
|
||||
<Space>
|
||||
<Text strong>Черновик</Text>
|
||||
<Tag color="default">Черновик</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Обновлен: {formatDate(draft.updated_at)}
|
||||
</Text>
|
||||
{draft.problem_description && (
|
||||
<Text
|
||||
ellipsis={{ tooltip: draft.problem_description }}
|
||||
style={{ fontSize: 13 }}
|
||||
>
|
||||
{draft.problem_description}
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === (draft.claim_id || draft.id)}
|
||||
disabled={deletingId === (draft.claim_id || draft.id)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
background: draft.is_legacy ? '#fff7e6' : '#f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
color: draft.is_legacy ? '#faad14' : '#595959',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{config.icon}
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Tag color={config.color} style={{ margin: 0 }}>{config.label}</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{/* Описание проблемы */}
|
||||
{draft.problem_description && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
title={draft.problem_description}
|
||||
>
|
||||
{draft.problem_description.length > 60
|
||||
? draft.problem_description.substring(0, 60) + '...'
|
||||
: draft.problem_description
|
||||
}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Время обновления */}
|
||||
<Space size="small">
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Tooltip title={formatDate(draft.updated_at)}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{getRelativeTime(draft.updated_at)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{/* Legacy предупреждение */}
|
||||
{draft.is_legacy && (
|
||||
<Alert
|
||||
message="Черновик в старом формате. Нажмите 'Начать заново'."
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ fontSize: 12, padding: '4px 8px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Прогресс документов */}
|
||||
{docsProgress && (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
📎 Документы: {docsProgress.uploaded} из {docsProgress.total} загружено
|
||||
{docsProgress.skipped > 0 && ` (${docsProgress.skipped} пропущено)`}
|
||||
</Text>
|
||||
<Progress
|
||||
percent={docsProgress.percent}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
strokeColor="#52c41a"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Старые теги прогресса (для обратной совместимости) */}
|
||||
{!docsProgress && !draft.is_legacy && (
|
||||
<Space size="small" wrap>
|
||||
<Tag color={draft.problem_description ? 'green' : 'default'}>
|
||||
{draft.problem_description ? '✓ Описание' : 'Описание'}
|
||||
</Tag>
|
||||
<Tag color={draft.wizard_plan ? 'green' : 'default'}>
|
||||
{draft.wizard_plan ? '✓ План' : 'План'}
|
||||
</Tag>
|
||||
<Tag color={draft.has_documents ? 'green' : 'default'}>
|
||||
{draft.has_documents ? '✓ Документы' : 'Документы'}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{/* Описание статуса */}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{config.description}
|
||||
</Text>
|
||||
)}
|
||||
<Space size="small">
|
||||
<Tag color={draft.wizard_plan ? 'green' : 'default'}>
|
||||
{draft.wizard_plan ? '✓ План' : 'План'}
|
||||
</Tag>
|
||||
<Tag color={draft.wizard_answers ? 'green' : 'default'}>
|
||||
{draft.wizard_answers ? '✓ Ответы' : 'Ответы'}
|
||||
</Tag>
|
||||
<Tag color={draft.has_documents ? 'green' : 'default'}>
|
||||
{draft.has_documents ? '✓ Документы' : 'Документы'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Прогресс: {getProgressInfo(draft)}
|
||||
</Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 24 }}>
|
||||
@@ -271,4 +508,3 @@ export default function StepDraftSelection({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user