diff --git a/frontend/src/pages/ClaimForm.tsx b/frontend/src/pages/ClaimForm.tsx index 7402247..ecc03a8 100644 --- a/frontend/src/pages/ClaimForm.tsx +++ b/frontend/src/pages/ClaimForm.tsx @@ -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(null); + const claimPlanTimeoutRef = useRef(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, поэтому: