/** * 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, 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); const day = date.getDate(); const month = date.toLocaleDateString('ru-RU', { month: 'long' }); const year = date.getFullYear(); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${day} ${month} ${year}, ${hours}:${minutes}`; } catch { return dateStr; } }; // Относительное время 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; onSelectDraft: (claimId: string) => void; onNewClaim: () => void; onRestartDraft?: (claimId: string, description: string) => void; // Для legacy черновиков } // === Конфиг статусов === const STATUS_CONFIG: Record = { draft: { color: 'default', icon: , label: 'Черновик', description: 'Начато заполнение', action: 'Продолжить', }, draft_new: { color: 'blue', icon: , label: 'Новый', description: 'Только описание проблемы', action: 'Загрузить документы', }, draft_docs_progress: { color: 'processing', icon: , label: 'Загрузка документов', description: 'Часть документов загружена', action: 'Продолжить загрузку', }, draft_docs_complete: { color: 'orange', icon: , label: 'Обработка', description: 'Формируется заявление...', action: 'Ожидайте', }, draft_claim_ready: { color: 'green', icon: , label: 'Готово к отправке', description: 'Заявление готово', action: 'Просмотреть и отправить', }, awaiting_sms: { color: 'volcano', icon: , label: 'Ожидает подтверждения', description: 'Введите SMS код', action: 'Подтвердить', }, in_work: { color: 'cyan', icon: , label: 'В работе', description: 'Заявка на рассмотрении', action: 'Просмотреть', }, legacy: { color: 'warning', icon: , label: 'Устаревший формат', description: 'Требуется обновление', action: 'Начать заново', }, }; export default function StepDraftSelection({ phone, session_id, unified_id, onSelectDraft, onNewClaim, onRestartDraft, }: Props) { const [drafts, setDrafts] = useState([]); const [loading, setLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); const loadDrafts = async () => { try { setLoading(true); const params = new URLSearchParams(); if (unified_id) { params.append('unified_id', unified_id); console.log('🔍 StepDraftSelection: загружаем черновики по unified_id:', unified_id); } else if (phone) { params.append('phone', phone); console.log('🔍 StepDraftSelection: загружаем черновики по phone:', phone); } else if (session_id) { params.append('session_id', session_id); console.log('🔍 StepDraftSelection: загружаем черновики по session_id:', session_id); } const url = `/api/v1/claims/drafts/list?${params.toString()}`; console.log('🔍 StepDraftSelection: запрос:', url); const response = await fetch(url); if (!response.ok) { throw new Error('Не удалось загрузить черновики'); } const data = await response.json(); console.log('🔍 StepDraftSelection: ответ API:', data); // Определяем 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('Не удалось загрузить список черновиков'); } finally { setLoading(false); } }; useEffect(() => { loadDrafts(); }, [phone, session_id, unified_id]); const handleDelete = async (claimId: string) => { try { setDeletingId(claimId); const response = await fetch(`/api/v1/claims/drafts/${claimId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Не удалось удалить черновик'); } message.success('Черновик удален'); await loadDrafts(); } catch (error) { console.error('Ошибка удаления черновика:', error); message.error('Не удалось удалить черновик'); } finally { setDeletingId(null); } }; // Получение конфига статуса const getStatusConfig = (draft: Draft) => { if (draft.is_legacy) { return STATUS_CONFIG.legacy; } return STATUS_CONFIG[draft.status_code] || STATUS_CONFIG.draft; }; // Прогресс документов 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 ( ); }; return (
📋 Ваши заявки Выберите заявку для продолжения или создайте новую.
{loading ? (
) : drafts.length === 0 ? ( ) : ( <> { const config = getStatusConfig(draft); const docsProgress = getDocsProgress(draft); return ( handleDelete(draft.claim_id || draft.id)} okText="Да, удалить" cancelText="Отмена" > , ]} > {config.icon}
} title={
{config.label}
} description={ {/* Описание проблемы */} {draft.problem_description && ( {draft.problem_description.length > 60 ? draft.problem_description.substring(0, 60) + '...' : draft.problem_description } )} {/* Время обновления */} {getRelativeTime(draft.updated_at)} {/* Legacy предупреждение */} {draft.is_legacy && ( )} {/* Прогресс документов */} {docsProgress && (
📎 Документы: {docsProgress.uploaded} из {docsProgress.total} загружено {docsProgress.skipped > 0 && ` (${docsProgress.skipped} пропущено)`}
)} {/* Старые теги прогресса (для обратной совместимости) */} {!docsProgress && !draft.is_legacy && ( {draft.problem_description ? '✓ Описание' : 'Описание'} {draft.wizard_plan ? '✓ План' : 'План'} {draft.has_documents ? '✓ Документы' : 'Документы'} )} {/* Описание статуса */} {config.description}
} /> ); }} />
)} ); }