Files
aiform_dev/frontend/src/components/form/StepDraftSelection.tsx

275 lines
10 KiB
TypeScript
Raw Normal View History

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 (если библиотека не установлена)
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 { Title, Text, Paragraph } = Typography;
interface Draft {
id: string;
claim_id: string;
session_token: string;
status_code: string;
created_at: string;
updated_at: string;
problem_description?: string;
wizard_plan: boolean;
wizard_answers: boolean;
has_documents: boolean;
}
interface Props {
phone?: string;
session_id?: string;
unified_id?: string; // ✅ Добавляем unified_id
onSelectDraft: (claimId: string) => void;
onNewClaim: () => void;
}
export default function StepDraftSelection({
phone,
session_id,
unified_id, // ✅ Добавляем unified_id
onSelectDraft,
onNewClaim,
}: Props) {
const [drafts, setDrafts] = useState<Draft[]>([]);
const [loading, setLoading] = useState(true);
const [deletingId, setDeletingId] = useState<string | null>(null);
const loadDrafts = async () => {
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);
} 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);
console.log('🔍 StepDraftSelection: количество черновиков:', data.count);
setDrafts(data.drafts || []);
} catch (error) {
console.error('Ошибка загрузки черновиков:', error);
message.error('Не удалось загрузить список черновиков');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadDrafts();
}, [phone, session_id, unified_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 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(', ') : 'Начато';
};
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: '24px 0' }}>
<Card
style={{
borderRadius: 8,
border: '1px solid #d9d9d9',
background: '#fff',
}}
>
<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>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Spin size="large" />
</div>
) : drafts.length === 0 ? (
<Empty
description="У вас нет незавершенных черновиков"
image={Empty.PRESENTED_IMAGE_SIMPLE}
>
<Button type="primary" icon={<PlusOutlined />} onClick={onNewClaim} size="large">
Создать новую заявку
</Button>
</Empty>
) : (
<>
<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}
>
Удалить
</Button>
</Popconfirm>,
]}
>
<List.Item.Meta
avatar={<FileTextOutlined style={{ fontSize: 24, color: '#595959' }} />}
title={
<Space>
feat: Add claim plan confirmation flow via Redis SSE Problem: - After wizard form submission, need to wait for claim data from n8n - Claim data comes via Redis channel claim:plan:{session_token} - Need to display confirmation form with claim data Solution: 1. Backend: Added SSE endpoint /api/v1/claim-plan/{session_token} - Subscribes to Redis channel claim:plan:{session_token} - Streams claim data from n8n to frontend - Handles timeouts and errors gracefully 2. Frontend: Added subscription to claim:plan channel - StepWizardPlan: After form submission, subscribes to SSE - Waits for claim_plan_ready event - Shows loading message while waiting - On success: saves claimPlanData and shows confirmation step 3. New component: StepClaimConfirmation - Displays claim confirmation form in iframe - Receives claimPlanData from parent - Generates HTML form (placeholder - should call n8n for real HTML) - Handles confirmation/cancellation via postMessage 4. ClaimForm: Added conditional step for confirmation - Shows StepClaimConfirmation when showClaimConfirmation=true - Step appears after StepWizardPlan - Only visible when claimPlanData is available Flow: 1. User fills wizard form → submits 2. Form data sent to n8n via /api/v1/claims/wizard 3. Frontend subscribes to SSE /api/v1/claim-plan/{session_token} 4. n8n processes data → publishes to Redis claim:plan:{session_token} 5. Backend receives → streams to frontend via SSE 6. Frontend receives → shows StepClaimConfirmation 7. User confirms → proceeds to next step Files: - backend/app/api/events.py: Added stream_claim_plan endpoint - frontend/src/components/form/StepWizardPlan.tsx: Added subscribeToClaimPlan - frontend/src/components/form/StepClaimConfirmation.tsx: New component - frontend/src/pages/ClaimForm.tsx: Added confirmation step to steps array
2025-11-24 13:36:14 +03:00
<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}
</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>
)}
/>
<div style={{ textAlign: 'center', marginTop: 24 }}>
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={onNewClaim}
size="large"
style={{ width: '100%' }}
>
Создать новую заявку
</Button>
</div>
<div style={{ textAlign: 'center' }}>
<Button
type="link"
icon={<ReloadOutlined />}
onClick={loadDrafts}
loading={loading}
>
Обновить список
</Button>
</div>
</>
)}
</Space>
</Card>
</div>
);
}