2025-11-26 19:54:51 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
import { useEffect, useState } from 'react';
|
2026-02-21 22:08:30 +03:00
|
|
|
|
import { Button, Card, Row, Col, Typography, Space, Empty, Popconfirm, message, Spin, Tooltip } from 'antd';
|
2025-11-26 19:54:51 +03:00
|
|
|
|
import {
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
LoadingOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
FileSearchOutlined,
|
|
|
|
|
|
MobileOutlined,
|
2026-02-21 22:08:30 +03:00
|
|
|
|
ExclamationCircleOutlined,
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
FolderOpenOutlined
|
2025-11-26 19:54:51 +03:00
|
|
|
|
} from '@ant-design/icons';
|
2026-02-21 22:08:30 +03:00
|
|
|
|
import {
|
|
|
|
|
|
Package,
|
|
|
|
|
|
Wrench,
|
|
|
|
|
|
Wallet,
|
|
|
|
|
|
ShoppingCart,
|
|
|
|
|
|
Truck,
|
|
|
|
|
|
Plane,
|
|
|
|
|
|
GraduationCap,
|
|
|
|
|
|
Wifi,
|
|
|
|
|
|
Home,
|
|
|
|
|
|
Hammer,
|
|
|
|
|
|
HeartPulse,
|
|
|
|
|
|
Car,
|
|
|
|
|
|
Building,
|
|
|
|
|
|
Shield,
|
|
|
|
|
|
Ticket,
|
|
|
|
|
|
type LucideIcon,
|
|
|
|
|
|
} from 'lucide-react';
|
2025-11-26 19:54:51 +03:00
|
|
|
|
|
2026-02-21 22:08:30 +03:00
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
// Иконки по направлениям (категориям) для плиток
|
|
|
|
|
|
const DIRECTION_ICONS: Record<string, LucideIcon> = {
|
|
|
|
|
|
'товары': 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;
|
|
|
|
|
|
}
|
2025-11-26 19:54:51 +03:00
|
|
|
|
|
|
|
|
|
|
// Форматирование даты
|
2025-11-19 18:46:48 +03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// Относительное время
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
2025-12-04 12:22:23 +03:00
|
|
|
|
interface DocumentStatus {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
required: boolean;
|
|
|
|
|
|
uploaded: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
interface Draft {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
claim_id: string;
|
|
|
|
|
|
session_token: string;
|
|
|
|
|
|
status_code: string;
|
2025-11-26 19:54:51 +03:00
|
|
|
|
channel: string;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
created_at: string;
|
|
|
|
|
|
updated_at: string;
|
2025-12-04 12:22:23 +03:00
|
|
|
|
problem_title?: string; // Краткое описание (заголовок)
|
2025-11-19 18:46:48 +03:00
|
|
|
|
problem_description?: string;
|
2025-12-04 12:22:23 +03:00
|
|
|
|
category?: string; // Категория проблемы
|
2026-02-21 22:08:30 +03:00
|
|
|
|
direction?: string; // Направление (для иконки плитки)
|
|
|
|
|
|
facts_short?: string; // Краткие факты от AI — заголовок плитки
|
2025-11-19 18:46:48 +03:00
|
|
|
|
wizard_plan: boolean;
|
|
|
|
|
|
wizard_answers: boolean;
|
|
|
|
|
|
has_documents: boolean;
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// Новые поля для нового флоу
|
|
|
|
|
|
documents_total?: number;
|
|
|
|
|
|
documents_uploaded?: number;
|
|
|
|
|
|
documents_skipped?: number;
|
2025-12-04 12:22:23 +03:00
|
|
|
|
documents_list?: DocumentStatus[]; // Список документов со статусами
|
2025-11-26 19:54:51 +03:00
|
|
|
|
wizard_ready?: boolean;
|
|
|
|
|
|
claim_ready?: boolean;
|
|
|
|
|
|
is_legacy?: boolean; // Старый формат без documents_required
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
phone?: string;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
session_id?: string;
|
2025-11-26 19:54:51 +03:00
|
|
|
|
unified_id?: string;
|
2026-01-29 16:12:48 +03:00
|
|
|
|
isTelegramMiniApp?: boolean; // ✅ Флаг Telegram Mini App
|
2025-11-19 18:46:48 +03:00
|
|
|
|
onSelectDraft: (claimId: string) => void;
|
|
|
|
|
|
onNewClaim: () => void;
|
2025-11-26 19:54:51 +03:00
|
|
|
|
onRestartDraft?: (claimId: string, description: string) => void; // Для legacy черновиков
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// === Конфиг статусов ===
|
|
|
|
|
|
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',
|
2025-12-04 12:22:23 +03:00
|
|
|
|
icon: <CheckCircleOutlined />,
|
|
|
|
|
|
label: 'Документы загружены',
|
|
|
|
|
|
description: 'Все документы обработаны',
|
|
|
|
|
|
action: 'Продолжить',
|
2025-11-26 19:54:51 +03:00
|
|
|
|
},
|
|
|
|
|
|
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: 'Начать заново',
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
export default function StepDraftSelection({
|
|
|
|
|
|
phone,
|
|
|
|
|
|
session_id,
|
2025-11-26 19:54:51 +03:00
|
|
|
|
unified_id,
|
2026-01-29 16:12:48 +03:00
|
|
|
|
isTelegramMiniApp,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
onSelectDraft,
|
|
|
|
|
|
onNewClaim,
|
2025-11-26 19:54:51 +03:00
|
|
|
|
onRestartDraft,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}: Props) {
|
|
|
|
|
|
const [drafts, setDrafts] = useState<Draft[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
2026-02-21 22:08:30 +03:00
|
|
|
|
/** Черновик, открытый для просмотра полного описания (по клику на карточку) */
|
|
|
|
|
|
const [selectedDraft, setSelectedDraft] = useState<Draft | null>(null);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
const loadDrafts = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const params = new URLSearchParams();
|
2025-11-26 19:54:51 +03:00
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
if (unified_id) {
|
|
|
|
|
|
params.append('unified_id', unified_id);
|
|
|
|
|
|
console.log('🔍 StepDraftSelection: загружаем черновики по unified_id:', unified_id);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else if (phone) {
|
|
|
|
|
|
params.append('phone', phone);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 StepDraftSelection: загружаем черновики по phone:', phone);
|
|
|
|
|
|
} else if (session_id) {
|
|
|
|
|
|
params.append('session_id', session_id);
|
|
|
|
|
|
console.log('🔍 StepDraftSelection: загружаем черновики по session_id:', session_id);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
const url = `/api/v1/claims/drafts/list?${params.toString()}`;
|
|
|
|
|
|
console.log('🔍 StepDraftSelection: запрос:', url);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('Не удалось загрузить черновики');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 StepDraftSelection: ответ API:', data);
|
2025-11-26 19:54:51 +03:00
|
|
|
|
|
|
|
|
|
|
// Определяем legacy черновики (без documents_required в payload)
|
2026-01-29 16:12:48 +03:00
|
|
|
|
let processedDrafts = (data.drafts || []).map((draft: Draft) => {
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// 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,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-29 16:12:48 +03:00
|
|
|
|
// ✅ В Telegram Mini App скрываем заявки "В работе"
|
|
|
|
|
|
if (isTelegramMiniApp) {
|
|
|
|
|
|
processedDrafts = processedDrafts.filter((draft: Draft) => draft.status_code !== 'in_work');
|
|
|
|
|
|
console.log('🔍 Telegram Mini App: заявки "В работе" скрыты');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
setDrafts(processedDrafts);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Ошибка загрузки черновиков:', error);
|
|
|
|
|
|
message.error('Не удалось загрузить список черновиков');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadDrafts();
|
2025-11-26 19:54:51 +03:00
|
|
|
|
}, [phone, session_id, unified_id]);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// Получение конфига статуса
|
|
|
|
|
|
const getStatusConfig = (draft: Draft) => {
|
|
|
|
|
|
if (draft.is_legacy) {
|
|
|
|
|
|
return STATUS_CONFIG.legacy;
|
|
|
|
|
|
}
|
|
|
|
|
|
return STATUS_CONFIG[draft.status_code] || STATUS_CONFIG.draft;
|
|
|
|
|
|
};
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
// Прогресс документов
|
|
|
|
|
|
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 {
|
2025-12-04 12:22:23 +03:00
|
|
|
|
// ✅ Разрешаем переход на любом этапе до апрува по SMS
|
2025-11-26 19:54:51 +03:00
|
|
|
|
onSelectDraft(draftId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Кнопка действия
|
|
|
|
|
|
const getActionButton = (draft: Draft) => {
|
2026-01-29 16:12:48 +03:00
|
|
|
|
// Для заявок "В работе"
|
|
|
|
|
|
if (draft.status_code === 'in_work') {
|
|
|
|
|
|
// ✅ В веб-версии показываем кнопку "Просмотреть в Telegram"
|
|
|
|
|
|
if (!isTelegramMiniApp) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<FileSearchOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
// Открываем Telegram бота
|
|
|
|
|
|
window.open('https://t.me/klientprav_bot', '_blank');
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Просмотреть в Telegram
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
// ✅ В Telegram Mini App не показываем (но этот код не выполнится, т.к. заявки отфильтрованы)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
const config = getStatusConfig(draft);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
2025-12-04 12:22:23 +03:00
|
|
|
|
type="primary"
|
2025-11-26 19:54:51 +03:00
|
|
|
|
onClick={() => handleDraftAction(draft)}
|
|
|
|
|
|
icon={config.icon}
|
|
|
|
|
|
>
|
|
|
|
|
|
{config.action}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-21 22:08:30 +03:00
|
|
|
|
// Экран полного описания черновика (по клику на карточку)
|
|
|
|
|
|
if (selectedDraft) {
|
|
|
|
|
|
const fullText = selectedDraft.problem_description || selectedDraft.facts_short || selectedDraft.problem_title || '—';
|
|
|
|
|
|
const draftId = selectedDraft.claim_id || selectedDraft.id;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ padding: '12px 16px' }}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
bodyStyle={{ padding: '16px 20px' }}
|
|
|
|
|
|
style={{ borderRadius: 8, border: '1px solid #d9d9d9', background: '#fff' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<ArrowLeftOutlined />}
|
|
|
|
|
|
onClick={() => setSelectedDraft(null)}
|
|
|
|
|
|
style={{ paddingLeft: 0 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Назад
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Title level={4} style={{ marginBottom: 8, color: '#111827' }}>
|
|
|
|
|
|
Обращение
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
background: '#f8fafc',
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
border: '1px solid #e2e8f0',
|
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
|
wordBreak: 'break-word',
|
|
|
|
|
|
minHeight: 80,
|
|
|
|
|
|
maxHeight: 320,
|
|
|
|
|
|
overflow: 'auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{fullText}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
|
|
|
|
{selectedDraft.is_legacy && onRestartDraft ? (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
onRestartDraft(draftId, selectedDraft.problem_description || '');
|
|
|
|
|
|
setSelectedDraft(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Начать заново
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<FolderOpenOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
onSelectDraft(draftId);
|
|
|
|
|
|
setSelectedDraft(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
К документам
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return (
|
2026-02-21 22:08:30 +03:00
|
|
|
|
<div style={{ padding: '12px 16px' }}>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<Card
|
2026-02-21 22:08:30 +03:00
|
|
|
|
bodyStyle={{ padding: '16px 0' }}
|
2025-11-19 18:46:48 +03:00
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
border: '1px solid #d9d9d9',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
|
|
|
|
<div>
|
2026-02-21 22:08:30 +03:00
|
|
|
|
<Title level={2} style={{ marginBottom: 16, color: '#1890ff' }}>
|
|
|
|
|
|
📋 Мои обращения
|
2025-11-19 18:46:48 +03:00
|
|
|
|
</Title>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : drafts.length === 0 ? (
|
|
|
|
|
|
<Empty
|
2025-12-04 12:22:23 +03:00
|
|
|
|
description="У вас пока нет незавершенных заявок"
|
2025-11-19 18:46:48 +03:00
|
|
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
2025-12-04 12:22:23 +03:00
|
|
|
|
/>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
2026-02-21 22:08:30 +03:00
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
|
{drafts.map((draft) => {
|
2025-11-26 19:54:51 +03:00
|
|
|
|
const config = getStatusConfig(draft);
|
2026-02-21 22:08:30 +03:00
|
|
|
|
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';
|
|
|
|
|
|
|
2025-11-26 19:54:51 +03:00
|
|
|
|
return (
|
2026-02-21 22:08:30 +03:00
|
|
|
|
<Col xs={12} sm={8} md={6} key={draft.claim_id || draft.id}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
hoverable
|
|
|
|
|
|
bordered
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 18,
|
|
|
|
|
|
border: `1px solid ${borderColor}`,
|
|
|
|
|
|
background: bgColor,
|
|
|
|
|
|
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
}}
|
|
|
|
|
|
bodyStyle={{
|
|
|
|
|
|
padding: 16,
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
gap: 10,
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => setSelectedDraft(draft)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
width: 52,
|
|
|
|
|
|
height: 52,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
background: iconBg,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: iconColor,
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{DirectionIcon ? (
|
|
|
|
|
|
<DirectionIcon size={28} strokeWidth={1.8} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span style={{ fontSize: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
|
|
|
|
{config.icon}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
strong
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
lineHeight: 1.3,
|
|
|
|
|
|
minHeight: 40,
|
|
|
|
|
|
display: '-webkit-box',
|
|
|
|
|
|
WebkitLineClamp: 2,
|
|
|
|
|
|
WebkitBoxOrient: 'vertical',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
color: '#111827',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
wordBreak: 'break-word',
|
|
|
|
|
|
} as React.CSSProperties}
|
|
|
|
|
|
>
|
|
|
|
|
|
{tileTitle}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
|
{config.label}
|
|
|
|
|
|
{(draft.documents_total != null && draft.documents_total > 0) && (
|
|
|
|
|
|
<span style={{ marginLeft: 4, color: '#1890ff' }}>
|
|
|
|
|
|
{draft.documents_uploaded ?? 0}/{draft.documents_total}
|
|
|
|
|
|
</span>
|
2025-12-04 12:22:23 +03:00
|
|
|
|
)}
|
2026-02-21 22:08:30 +03:00
|
|
|
|
</Text>
|
|
|
|
|
|
<Tooltip title={formatDate(draft.updated_at)}>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
|
|
|
|
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
|
|
|
|
|
{getRelativeTime(draft.updated_at)}
|
2025-11-19 18:46:48 +03:00
|
|
|
|
</Text>
|
2026-02-21 22:08:30 +03:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: '100%', marginTop: 4 }} onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
{getActionButton(draft)}
|
|
|
|
|
|
{draft.status_code !== 'in_work' && (
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="Удалить заявку?"
|
|
|
|
|
|
description="Это действие нельзя отменить"
|
|
|
|
|
|
onConfirm={() => handleDelete(draft.claim_id || draft.id)}
|
|
|
|
|
|
okText="Да, удалить"
|
|
|
|
|
|
cancelText="Отмена"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
loading={deletingId === (draft.claim_id || draft.id)}
|
|
|
|
|
|
disabled={deletingId === (draft.claim_id || draft.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Удалить
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
2025-11-26 19:54:51 +03:00
|
|
|
|
);
|
2026-02-21 22:08:30 +03:00
|
|
|
|
})}
|
|
|
|
|
|
</Row>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
2025-12-04 12:22:23 +03:00
|
|
|
|
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={loadDrafts}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
>
|
|
|
|
|
|
Обновить список
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|