diff --git a/frontend/src/components/form/StepDraftSelection.css b/frontend/src/components/form/StepDraftSelection.css
new file mode 100644
index 0000000..f4e12a8
--- /dev/null
+++ b/frontend/src/components/form/StepDraftSelection.css
@@ -0,0 +1,12 @@
+/* Карточки списка обращений — как на hello: тень и подъём при наведении */
+.draft-list-card {
+ border-radius: 16px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ box-shadow: 0 16px 28px rgba(15, 23, 42, 0.06);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.draft-list-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 22px 36px rgba(15, 23, 42, 0.12);
+}
diff --git a/frontend/src/components/form/StepDraftSelection.tsx b/frontend/src/components/form/StepDraftSelection.tsx
index d680120..48d2b7c 100644
--- a/frontend/src/components/form/StepDraftSelection.tsx
+++ b/frontend/src/components/form/StepDraftSelection.tsx
@@ -14,7 +14,7 @@
*/
import { useEffect, useState } from 'react';
-import { Button, Card, Row, Col, Typography, Space, Empty, Popconfirm, message, Spin, Tooltip } from 'antd';
+import { Button, Card, Typography, Space, Empty, message, Spin, Tooltip } from 'antd';
import {
FileTextOutlined,
DeleteOutlined,
@@ -26,9 +26,9 @@ import {
FileSearchOutlined,
MobileOutlined,
ExclamationCircleOutlined,
- ArrowLeftOutlined,
FolderOpenOutlined
} from '@ant-design/icons';
+import './StepDraftSelection.css';
import {
Package,
Wrench,
@@ -90,6 +90,41 @@ const formatDate = (dateStr: string) => {
}
};
+// Короткая дата для карточек списка: "12 апреля 2024"
+const formatDateShort = (dateStr: string) => {
+ try {
+ const date = new Date(dateStr);
+ const day = date.getDate();
+ const month = date.toLocaleDateString('ru-RU', { month: 'long' });
+ const year = date.getFullYear();
+ return `${day} ${month} ${year}`;
+ } catch {
+ return dateStr;
+ }
+};
+
+// Маппинг status_code → категория дашборда (как в StepComplaintsDashboard)
+const PENDING_CODES = ['draft', 'draft_new', 'draft_docs_progress', 'draft_docs_complete', 'draft_claim_ready', 'awaiting_sms'];
+const IN_WORK_CODE = 'in_work';
+const RESOLVED_CODES = ['completed', 'submitted'];
+const REJECTED_CODE = 'rejected';
+
+function getDraftCategory(statusCode: string): 'pending' | 'in_work' | 'resolved' | 'rejected' {
+ const code = (statusCode || '').toLowerCase();
+ if (code === IN_WORK_CODE) return 'in_work';
+ if (code === REJECTED_CODE) return 'rejected';
+ if (RESOLVED_CODES.includes(code)) return 'resolved';
+ return 'pending';
+}
+
+const CATEGORY_LABELS: Record<'all' | 'pending' | 'in_work' | 'resolved' | 'rejected', string> = {
+ all: 'Все обращения',
+ pending: 'В ожидании',
+ in_work: 'Приняты к работе',
+ resolved: 'Решены',
+ rejected: 'Отклонены',
+};
+
// Относительное время
const getRelativeTime = (dateStr: string) => {
try {
@@ -142,14 +177,23 @@ interface Draft {
is_legacy?: boolean; // Старый формат без documents_required
}
+/** Фильтр списка по категории (с дашборда) */
+export type DraftsListFilter = 'all' | 'pending' | 'in_work' | 'resolved' | 'rejected';
+
interface Props {
phone?: string;
session_id?: string;
unified_id?: string;
- isTelegramMiniApp?: boolean; // ✅ Флаг Telegram Mini App
+ isTelegramMiniApp?: boolean;
+ /** ID черновика, открытого для просмотра описания (управляется из ClaimForm, чтобы не терять при пересчёте steps) */
+ draftDetailClaimId?: string | null;
+ /** Показывать только обращения этой категории (с дашборда) */
+ categoryFilter?: DraftsListFilter;
+ onOpenDraftDetail?: (claimId: string) => void;
+ onCloseDraftDetail?: () => void;
onSelectDraft: (claimId: string) => void;
onNewClaim: () => void;
- onRestartDraft?: (claimId: string, description: string) => void; // Для legacy черновиков
+ onRestartDraft?: (claimId: string, description: string) => void;
}
// === Конфиг статусов ===
@@ -223,15 +267,31 @@ export default function StepDraftSelection({
session_id,
unified_id,
isTelegramMiniApp,
+ draftDetailClaimId = null,
+ categoryFilter = 'all',
+ onOpenDraftDetail,
+ onCloseDraftDetail,
onSelectDraft,
onNewClaim,
onRestartDraft,
}: Props) {
const [drafts, setDrafts] = useState
([]);
+
+ /** Список отфильтрован по категории с дашборда */
+ const filteredDrafts =
+ categoryFilter === 'all'
+ ? drafts
+ : drafts.filter((d) => getDraftCategory(d.status_code) === categoryFilter);
const [loading, setLoading] = useState(true);
const [deletingId, setDeletingId] = useState(null);
- /** Черновик, открытый для просмотра полного описания (по клику на карточку) */
- const [selectedDraft, setSelectedDraft] = useState(null);
+ /** Полный payload черновика с API GET /drafts/{claim_id} для экрана описания */
+ const [detailDraftPayload, setDetailDraftPayload] = useState<{ claimId: string; payload: Record } | null>(null);
+ const [detailLoading, setDetailLoading] = useState(false);
+
+ /** Черновик для экрана описания: из пропа draftDetailClaimId + список drafts */
+ const selectedDraft = draftDetailClaimId
+ ? (drafts.find((d) => (d.claim_id || d.id) === draftDetailClaimId) ?? null)
+ : null;
const loadDrafts = async () => {
try {
@@ -332,6 +392,38 @@ export default function StepDraftSelection({
return { uploaded, skipped, total, percent };
};
+ // Открыть экран полного описания (загрузка payload — в useEffect по draftDetailClaimId)
+ const openDraftDetail = (draft: Draft) => {
+ const draftId = draft.claim_id || draft.id;
+ onOpenDraftDetail?.(draftId);
+ setDetailDraftPayload(null);
+ setDetailLoading(true);
+ };
+
+ const closeDraftDetail = () => {
+ onCloseDraftDetail?.();
+ setDetailDraftPayload(null);
+ };
+
+ // Загрузка payload при открытии по draftDetailClaimId (клик по карточке или восстановление после пересчёта steps)
+ useEffect(() => {
+ if (!draftDetailClaimId) return;
+ if (detailDraftPayload?.claimId === draftDetailClaimId) return;
+ setDetailLoading(true);
+ setDetailDraftPayload(null);
+ const claimId = draftDetailClaimId;
+ fetch(`/api/v1/claims/drafts/${claimId}`)
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error('Не удалось загрузить черновик'))))
+ .then((data) => {
+ const payload = data?.claim?.payload;
+ if (payload && typeof payload === 'object') {
+ setDetailDraftPayload({ claimId, payload });
+ }
+ })
+ .catch(() => {})
+ .finally(() => setDetailLoading(false));
+ }, [draftDetailClaimId]);
+
// Обработка клика на черновик
const handleDraftAction = (draft: Draft) => {
const draftId = draft.claim_id || draft.id;
@@ -381,25 +473,28 @@ export default function StepDraftSelection({
);
};
- // Экран полного описания черновика (по клику на карточку)
- if (selectedDraft) {
- const fullText = selectedDraft.problem_description || selectedDraft.facts_short || selectedDraft.problem_title || '—';
- const draftId = selectedDraft.claim_id || selectedDraft.id;
+ // Экран полного описания черновика (draftDetailClaimId открыт; selectedDraft может быть null пока список не подгрузился)
+ if (draftDetailClaimId) {
+ const draftId = draftDetailClaimId;
+ const payload = detailDraftPayload?.claimId === draftId ? detailDraftPayload.payload : null;
+ const fromPayload =
+ (payload && (payload.problem_description ?? payload.description ?? payload.chatInput)) ?? '';
+ const fromDraft = selectedDraft
+ ? (selectedDraft.problem_description ||
+ selectedDraft.facts_short ||
+ selectedDraft.problem_title ||
+ '')
+ : '';
+ const fullText = String(fromPayload || fromDraft || '').trim();
+ const displayText = fullText || 'Описание не сохранено';
+
return (
-
+
- }
- onClick={() => setSelectedDraft(null)}
- style={{ paddingLeft: 0 }}
- >
- Назад
-
Обращение
@@ -416,17 +511,17 @@ export default function StepDraftSelection({
overflow: 'auto',
}}
>
- {fullText}
+ {detailLoading && !fromDraft ? : displayText}
- {selectedDraft.is_legacy && onRestartDraft ? (
+ {selectedDraft?.is_legacy && onRestartDraft ? (
}
onClick={() => {
onRestartDraft(draftId, selectedDraft.problem_description || '');
- setSelectedDraft(null);
+ closeDraftDetail();
}}
>
Начать заново
@@ -438,7 +533,7 @@ export default function StepDraftSelection({
icon={
}
onClick={() => {
onSelectDraft(draftId);
- setSelectedDraft(null);
+ closeDraftDetail();
}}
>
К документам
@@ -451,166 +546,95 @@ export default function StepDraftSelection({
);
}
+ // Цвет точки статуса по категории (как на макете — зелёный для «Приняты к работе»)
+ const statusDotColor: Record
= {
+ pending: '#1890ff',
+ in_work: '#52c41a',
+ resolved: '#52c41a',
+ rejected: '#ff4d4f',
+ };
+
return (
-
-
-
-
-
- 📋 Мои обращения
-
+
+ {/* Шапка: заголовок + подзаголовок категории */}
+
+
+ Мои обращения
+
+
+ {CATEGORY_LABELS[categoryFilter]}
+
+
+
+ {loading ? (
+
+
+
+ ) : filteredDrafts.length === 0 ? (
+
+ ) : (
+
+ {filteredDrafts.map((draft) => {
+ const config = getStatusConfig(draft);
+ 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 category = getDraftCategory(draft.status_code);
+ const dotColor = statusDotColor[category] || '#8c8c8c';
+
+ return (
+ openDraftDetail(draft)}
+ >
+
+
+ {tileTitle}
+
+
+
+ {config.label}
+
+
+ {config.description}
+
+
+ {formatDateShort(draft.updated_at)}
+
+
+
+ );
+ })}
+
+
+ }
+ onClick={loadDrafts}
+ loading={loading}
+ >
+ Обновить список
+
-
- {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="Отмена"
- >
- }
- loading={deletingId === (draft.claim_id || draft.id)}
- disabled={deletingId === (draft.claim_id || draft.id)}
- >
- Удалить
-
-
- )}
-
-
-
- );
- })}
-
-
-
- }
- onClick={loadDrafts}
- loading={loading}
- >
- Обновить список
-
-
- >
- )}
-
+ )}
);
}
diff --git a/frontend/src/components/form/StepWizardPlan.tsx b/frontend/src/components/form/StepWizardPlan.tsx
index 848ac63..2a79cf4 100644
--- a/frontend/src/components/form/StepWizardPlan.tsx
+++ b/frontend/src/components/form/StepWizardPlan.tsx
@@ -1456,7 +1456,6 @@ export default function StepWizardPlan({
status="warning"
title="Нет session_id"
subTitle="Не удалось определить идентификатор сессии. Вернитесь на предыдущий шаг и попробуйте снова."
- extra={
}
/>
);
}
@@ -2706,9 +2705,6 @@ export default function StepWizardPlan({
)}
-