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
This commit is contained in:
@@ -705,10 +705,17 @@ export default function StepWizardPlan({
|
||||
block.description || ''
|
||||
);
|
||||
|
||||
// Имя "поля" группы
|
||||
// Имя "поля" группы (используем docLabel если есть, иначе guessFieldName)
|
||||
const fieldLabel = block.docLabel || block.fieldName || guessFieldName(group);
|
||||
formPayload.append(
|
||||
`uploads_field_names[${i}]`,
|
||||
guessFieldName(group)
|
||||
fieldLabel
|
||||
);
|
||||
|
||||
// ✅ Добавляем реальное название поля (label) для использования в n8n
|
||||
formPayload.append(
|
||||
`uploads_field_labels[${i}]`,
|
||||
block.docLabel || block.description || fieldLabel
|
||||
);
|
||||
|
||||
// Файлы: uploads[i][j]
|
||||
@@ -754,17 +761,106 @@ export default function StepWizardPlan({
|
||||
response: parsed ?? text,
|
||||
});
|
||||
message.success('Мы изучаем ваш вопрос и документы.');
|
||||
|
||||
// Подписываемся на канал claim:plan для получения данных заявления
|
||||
if (formData.session_id) {
|
||||
subscribeToClaimPlan(formData.session_id);
|
||||
} else {
|
||||
console.warn('⚠️ session_id отсутствует, не можем подписаться на claim:plan');
|
||||
onNext();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Ошибка соединения при отправке визарда.');
|
||||
addDebugEvent?.('wizard', 'error', '❌ Ошибка соединения при отправке визарда', {
|
||||
error: String(error),
|
||||
});
|
||||
onNext();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
// Функция подписки на канал claim:plan
|
||||
const subscribeToClaimPlan = useCallback((sessionToken: string) => {
|
||||
console.log('📡 Подписка на канал claim:plan для session:', sessionToken);
|
||||
|
||||
// Закрываем предыдущее соединение, если есть
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
|
||||
// Создаём новое SSE соединение
|
||||
const eventSource = new EventSource(`/api/v1/claim-plan/${sessionToken}`);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ Подключено к каналу claim:plan');
|
||||
addDebugEvent?.('claim-plan', 'info', '📡 Ожидание данных заявления...');
|
||||
message.loading('Ожидание данных заявления...', 0);
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📥 Получены данные из claim:plan:', data);
|
||||
|
||||
if (data.event_type === 'claim_plan_ready' && data.status === 'ready') {
|
||||
// Данные заявления получены!
|
||||
message.destroy(); // Убираем loading сообщение
|
||||
message.success('Данные заявления готовы!');
|
||||
|
||||
// Сохраняем данные заявления в formData
|
||||
updateFormData({
|
||||
claimPlanData: data.data, // Данные от n8n
|
||||
showClaimConfirmation: true, // Флаг для показа формы подтверждения
|
||||
});
|
||||
|
||||
// Закрываем SSE соединение
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
|
||||
// Переходим к следующему шагу (форма подтверждения)
|
||||
onNext();
|
||||
} else if (data.event_type === 'claim_plan_error' || data.status === 'error') {
|
||||
message.destroy();
|
||||
message.error(data.message || 'Ошибка получения данных заявления');
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
onNext(); // Переходим дальше даже при ошибке
|
||||
} else if (data.event_type === 'claim_plan_timeout' || data.status === 'timeout') {
|
||||
message.destroy();
|
||||
message.warning('Превышено время ожидания. Попробуйте обновить страницу.');
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
onNext();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка парсинга данных из claim:plan:', error);
|
||||
message.destroy();
|
||||
message.error('Ошибка обработки данных заявления');
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('❌ Ошибка SSE соединения claim:plan:', error);
|
||||
message.destroy();
|
||||
message.error('Ошибка подключения к серверу');
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
onNext(); // Переходим дальше даже при ошибке
|
||||
};
|
||||
|
||||
// Таймаут на 5 минут
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.warn('⏰ Таймаут ожидания данных заявления');
|
||||
message.destroy();
|
||||
message.warning('Превышено время ожидания данных заявления');
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
onNext();
|
||||
}, 300000); // 5 минут
|
||||
}, [addDebugEvent, updateFormData, onNext]);
|
||||
|
||||
const renderQuestionField = (question: WizardQuestion) => {
|
||||
// Обработка по input_type для более точного определения типа поля
|
||||
|
||||
Reference in New Issue
Block a user