feat: Auto-navigate to confirmation form when draft is fully filled

Problem:
- When user selects a draft with all steps filled (description, plan, answers, documents)
- But claim status is still 'draft' (not submitted)
- User has to manually navigate through all steps again

Solution:
1. Added check in loadDraft() to detect fully filled drafts:
   - hasDescription: problem_description exists
   - hasWizardPlan: wizard_plan exists
   - hasAnswers: answers exist and not empty
   - hasDocuments: documents_meta array has items
   - isDraft: status_code === 'draft'
   - allStepsFilled: all checks pass

2. When draft is ready for confirmation:
   - Automatically subscribe to claim:plan SSE channel
   - Wait for claim data from n8n
   - Show loading message while waiting
   - On success: show confirmation form automatically

3. Added subscribeToClaimPlanForDraft() function:
   - Subscribes to /api/v1/claim-plan/{session_token}
   - Handles claim_plan_ready event
   - Updates formData with claimPlanData
   - Auto-navigates to confirmation step via useEffect

4. Added useEffect for auto-navigation:
   - Watches formData.showClaimConfirmation and formData.claimPlanData
   - When both true, navigates to step 3 (confirmation)
   - Handles cleanup of EventSource on unmount

Flow:
1. User selects draft → loadDraft() checks completeness
2. If all filled + draft → subscribeToClaimPlanForDraft()
3. SSE receives data → updates formData
4. useEffect detects → navigates to confirmation step
5. User sees confirmation form immediately

Files:
- frontend/src/pages/ClaimForm.tsx: Added auto-navigation logic
This commit is contained in:
AI Assistant
2025-11-24 14:11:04 +03:00
parent 0978e485dc
commit 379995ba51

View File

@@ -76,6 +76,8 @@ export default function ClaimForm() {
// session_id будет получен от n8n при создании контакта
// Используем useRef чтобы sessionId не вызывал перерендер и был стабильным
const sessionIdRef = useRef(`sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
const claimPlanEventSourceRef = useRef<EventSource | null>(null);
const claimPlanTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [currentStep, setCurrentStep] = useState(0);
const [sessionRestored, setSessionRestored] = useState(false); // Флаг: пытались восстановить сессию
@@ -209,6 +211,39 @@ export default function ClaimForm() {
fetchClientIp();
}, []);
// Автоматический переход к шагу подтверждения, когда данные готовы
useEffect(() => {
if (formData.showClaimConfirmation && formData.claimPlanData) {
// Вычисляем индекс шага подтверждения динамически
// Шаг подтверждения добавляется после StepWizardPlan
// После выбора черновика showDraftSelection = false, поэтому:
// - Шаг 0 = Step1Phone
// - Шаг 1 = StepDescription
// - Шаг 2 = StepWizardPlan
// - Шаг 3 = StepClaimConfirmation (если showClaimConfirmation=true)
const confirmationStepIndex = 3; // Фиксированный индекс для шага подтверждения
console.log('✅ Данные заявления готовы, переходим к шагу подтверждения:', confirmationStepIndex);
setTimeout(() => {
setCurrentStep(confirmationStepIndex);
}, 100);
}
}, [formData.showClaimConfirmation, formData.claimPlanData]);
// Cleanup: закрываем SSE соединение при размонтировании
useEffect(() => {
return () => {
if (claimPlanEventSourceRef.current) {
claimPlanEventSourceRef.current.close();
claimPlanEventSourceRef.current = null;
}
if (claimPlanTimeoutRef.current) {
clearTimeout(claimPlanTimeoutRef.current);
claimPlanTimeoutRef.current = null;
}
};
}, []);
// Динамически определяем список шагов на основе выбранного eventType
const documentConfigs = formData.eventType ? getDocumentsForEventType(formData.eventType) : [];
const totalDocumentSteps = documentConfigs.length;
@@ -249,6 +284,98 @@ export default function ClaimForm() {
});
}, []);
// Подписка на канал claim:plan для получения данных заявления (для черновиков)
const subscribeToClaimPlanForDraft = useCallback((sessionToken: string, claimId: string) => {
console.log('📡 Подписка на канал claim:plan для черновика:', { sessionToken, claimId });
// Закрываем предыдущее соединение, если есть
if (claimPlanEventSourceRef.current) {
claimPlanEventSourceRef.current.close();
claimPlanEventSourceRef.current = null;
}
// Очищаем предыдущий таймаут
if (claimPlanTimeoutRef.current) {
clearTimeout(claimPlanTimeoutRef.current);
claimPlanTimeoutRef.current = null;
}
// Создаём новое SSE соединение
const eventSource = new EventSource(`/api/v1/claim-plan/${sessionToken}`);
claimPlanEventSourceRef.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();
claimPlanEventSourceRef.current = null;
// Переход к шагу подтверждения произойдёт автоматически через useEffect
} else if (data.event_type === 'claim_plan_error' || data.status === 'error') {
message.destroy();
message.error(data.message || 'Ошибка получения данных заявления');
eventSource.close();
claimPlanEventSourceRef.current = null;
// Переходим к визарду вместо подтверждения
setCurrentStep(2);
} else if (data.event_type === 'claim_plan_timeout' || data.status === 'timeout') {
message.destroy();
message.warning('Данные заявления ещё обрабатываются. Вы можете продолжить редактирование.');
eventSource.close();
claimPlanEventSourceRef.current = null;
// Переходим к визарду вместо подтверждения
setCurrentStep(2);
}
} catch (error) {
console.error('❌ Ошибка парсинга данных из claim:plan:', error);
message.destroy();
message.error('Ошибка обработки данных заявления');
eventSource.close();
claimPlanEventSourceRef.current = null;
setCurrentStep(2);
}
};
eventSource.onerror = (error) => {
console.error('❌ Ошибка SSE соединения claim:plan:', error);
message.destroy();
message.warning('Не удалось получить данные заявления. Вы можете продолжить редактирование.');
eventSource.close();
claimPlanEventSourceRef.current = null;
setCurrentStep(2); // Переходим к визарду вместо подтверждения
};
// Таймаут на 5 минут
claimPlanTimeoutRef.current = setTimeout(() => {
console.warn('⏰ Таймаут ожидания данных заявления для черновика');
message.destroy();
message.warning('Превышено время ожидания данных заявления. Вы можете продолжить редактирование.');
eventSource.close();
claimPlanEventSourceRef.current = null;
setCurrentStep(2);
}, 300000); // 5 минут
}, [updateFormData, addDebugEvent]);
// Загрузка черновика
const loadDraft = useCallback(async (claimId: string) => {
try {
@@ -285,6 +412,7 @@ export default function ClaimForm() {
const wizardPlanRaw = body.wizard_plan || payload.wizard_plan;
const answersRaw = body.answers || payload.answers;
const problemDescription = body.problem_description || payload.problem_description || body.description || payload.description;
const documentsMeta = body.documents_meta || payload.documents_meta || [];
// ✅ Парсим wizard_plan и answers, если они строки (JSON)
let wizardPlan = wizardPlanRaw;
@@ -305,9 +433,30 @@ export default function ClaimForm() {
}
}
// ✅ Проверяем, заполнены ли все шаги
const hasDescription = !!problemDescription;
const hasWizardPlan = !!wizardPlan;
const hasAnswers = !!answers && Object.keys(answers).length > 0;
const hasDocuments = Array.isArray(documentsMeta) && documentsMeta.length > 0;
const isDraft = claim.status_code === 'draft';
const allStepsFilled = hasDescription && hasWizardPlan && hasAnswers && hasDocuments;
const isReadyForConfirmation = allStepsFilled && isDraft;
console.log('🔍 Проверка полноты черновика:', {
hasDescription,
hasWizardPlan,
hasAnswers,
hasDocuments,
isDraft,
allStepsFilled,
isReadyForConfirmation,
});
console.log('🔍 problem_description:', problemDescription ? 'есть' : 'нет');
console.log('🔍 wizard_plan:', wizardPlan ? 'есть' : 'нет');
console.log('🔍 answers:', answers ? 'есть' : 'нет');
console.log('🔍 documents_meta:', documentsMeta.length, 'документов');
console.log('🔍 Все ключи payload:', Object.keys(payload));
if (isTelegramFormat) {
console.log('🔍 Все ключи body:', Object.keys(body));
@@ -354,6 +503,21 @@ export default function ClaimForm() {
setSelectedDraftId(finalClaimId);
setShowDraftSelection(false);
// ✅ Если все шаги заполнены и статус = draft → переходим к форме подтверждения
if (isReadyForConfirmation) {
console.log('✅ Все шаги заполнены, переходим к форме подтверждения');
setIsPhoneVerified(true);
// Подписываемся на канал claim:plan для получения данных заявления
subscribeToClaimPlanForDraft(actualSessionId, finalClaimId);
// Пока устанавливаем шаг визарда, переход к подтверждению произойдёт автоматически
// когда данные будут получены через SSE
setCurrentStep(2); // StepWizardPlan
return;
}
// ✅ Определяем шаг для перехода на основе данных черновика
// Приоритет: если есть wizard_plan → переходим к визарду (даже если нет problem_description)
// После выбора черновика showDraftSelection = false, поэтому: