/** * 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, Row, Col, Typography, Space, Empty, Popconfirm, message, Spin, Tooltip } from 'antd'; import { FileTextOutlined, DeleteOutlined, ReloadOutlined, ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined, UploadOutlined, FileSearchOutlined, MobileOutlined, ExclamationCircleOutlined, ArrowLeftOutlined, FolderOpenOutlined } from '@ant-design/icons'; import { Package, Wrench, Wallet, ShoppingCart, Truck, Plane, GraduationCap, Wifi, Home, Hammer, HeartPulse, Car, Building, Shield, Ticket, type LucideIcon, } from 'lucide-react'; const { Title, Text } = Typography; // Иконки по направлениям (категориям) для плиток const DIRECTION_ICONS: Record = { 'товары': Package, 'услуги': Wrench, 'финансы и платежи': Wallet, 'интернет-торговля и маркетплейсы': ShoppingCart, 'доставка и логистика': Truck, 'туризм и путешествия': Plane, 'образование и онлайн-курсы': GraduationCap, 'связь и интернет': Wifi, 'жкх и коммунальные услуги': Home, 'строительство и ремонт': Hammer, 'медицина и платные клиники': HeartPulse, 'транспорт и перевозки': Car, 'недвижимость и аренда': Building, 'страхование': Shield, 'развлечения и мероприятия': Ticket, }; function getDirectionIcon(directionOrCategory: string | undefined): LucideIcon | null { if (!directionOrCategory || typeof directionOrCategory !== 'string') return null; const key = directionOrCategory.trim().toLowerCase(); return DIRECTION_ICONS[key] || null; } // Форматирование даты 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 DocumentStatus { name: string; required: boolean; uploaded: boolean; } interface Draft { id: string; claim_id: string; session_token: string; status_code: string; channel: string; created_at: string; updated_at: string; problem_title?: string; // Краткое описание (заголовок) problem_description?: string; category?: string; // Категория проблемы direction?: string; // Направление (для иконки плитки) facts_short?: string; // Краткие факты от AI — заголовок плитки wizard_plan: boolean; wizard_answers: boolean; has_documents: boolean; // Новые поля для нового флоу documents_total?: number; documents_uploaded?: number; documents_skipped?: number; documents_list?: DocumentStatus[]; // Список документов со статусами wizard_ready?: boolean; claim_ready?: boolean; is_legacy?: boolean; // Старый формат без documents_required } interface Props { phone?: string; session_id?: string; unified_id?: string; isTelegramMiniApp?: boolean; // ✅ Флаг Telegram Mini App 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, isTelegramMiniApp, onSelectDraft, onNewClaim, onRestartDraft, }: Props) { const [drafts, setDrafts] = useState([]); const [loading, setLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); /** Черновик, открытый для просмотра полного описания (по клику на карточку) */ const [selectedDraft, setSelectedDraft] = 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) let 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, }; }); // ✅ В Telegram Mini App скрываем заявки "В работе" if (isTelegramMiniApp) { processedDrafts = processedDrafts.filter((draft: Draft) => draft.status_code !== 'in_work'); console.log('🔍 Telegram Mini App: заявки "В работе" скрыты'); } 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 { // ✅ Разрешаем переход на любом этапе до апрува по SMS onSelectDraft(draftId); } }; // Кнопка действия const getActionButton = (draft: Draft) => { // Для заявок "В работе" if (draft.status_code === 'in_work') { // ✅ В веб-версии показываем кнопку "Просмотреть в Telegram" if (!isTelegramMiniApp) { return ( ); } // ✅ В Telegram Mini App не показываем (но этот код не выполнится, т.к. заявки отфильтрованы) return null; } const config = getStatusConfig(draft); return ( ); }; // Экран полного описания черновика (по клику на карточку) if (selectedDraft) { const fullText = selectedDraft.problem_description || selectedDraft.facts_short || selectedDraft.problem_title || '—'; const draftId = selectedDraft.claim_id || selectedDraft.id; return (
Обращение
{fullText}
{selectedDraft.is_legacy && onRestartDraft ? ( ) : ( )}
); } return (
📋 Мои обращения
{loading ? (
) : drafts.length === 0 ? ( ) : ( <> {drafts.map((draft) => { const config = getStatusConfig(draft); const directionOrCategory = draft.direction || draft.category; const DirectionIcon = getDirectionIcon(directionOrCategory); const tileTitle = draft.facts_short || draft.problem_title || (draft.problem_description ? (draft.problem_description.length > 60 ? draft.problem_description.slice(0, 60).trim() + '…' : draft.problem_description) : 'Обращение'); const borderColor = draft.is_legacy ? '#faad14' : '#e8e8e8'; const bgColor = draft.is_legacy ? '#fffbe6' : '#fff'; const iconBg = draft.is_legacy ? '#fff7e6' : '#f8fafc'; const iconColor = draft.is_legacy ? '#faad14' : '#6366f1'; return ( setSelectedDraft(draft)} >
{DirectionIcon ? ( ) : ( {config.icon} )}
{tileTitle}
{config.label} {(draft.documents_total != null && draft.documents_total > 0) && ( {draft.documents_uploaded ?? 0}/{draft.documents_total} )} {getRelativeTime(draft.updated_at)}
e.stopPropagation()}> {getActionButton(draft)} {draft.status_code !== 'in_work' && ( handleDelete(draft.claim_id || draft.id)} okText="Да, удалить" cancelText="Отмена" > )}
); })}
)}
); }