refactor: Load claim confirmation data from DB instead of SSE for drafts

Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly

Solution:
1. Removed subscribeToClaimPlanForDraft() function
   - No longer subscribes to SSE channel for drafts
   - Removed EventSource cleanup code

2. Added transformDraftToClaimPlanFormat() function
   - Transforms draft data from DB format to propertyName format
   - Extracts data from payload/body (telegram/web_form formats)
   - Maps documents_meta to attachments array
   - Formats applicant, case, contract_or_service, offenders, claim, meta
   - Returns data in array format expected by confirmation form

3. Updated loadDraft() logic:
   - When draft is ready for confirmation (all steps filled + draft status)
   - Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
   - Immediately shows confirmation form with data from DB

Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)

Benefits:
-  Faster: no waiting for n8n to publish data
-  More reliable: data always available from DB
-  Simpler: no SSE connection management for drafts
-  Works offline: doesn't depend on Redis pub/sub

Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
This commit is contained in:
AI Assistant
2025-11-24 15:08:00 +03:00
parent 379995ba51
commit 577611c65d

View File

@@ -230,19 +230,6 @@ export default function ClaimForm() {
}
}, [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) : [];
@@ -284,97 +271,116 @@ export default function ClaimForm() {
});
}, []);
// Подписка на канал claim:plan для получения данных заявления (для черновиков)
const subscribeToClaimPlanForDraft = useCallback((sessionToken: string, claimId: string) => {
console.log('📡 Подписка на канал claim:plan для черновика:', { sessionToken, claimId });
// Преобразование данных черновика в формат propertyName для формы подтверждения
const transformDraftToClaimPlanFormat = useCallback((data: {
claim: any;
payload: any;
body: any;
isTelegramFormat: boolean;
finalClaimId: string;
actualSessionId: string;
currentFormData: FormData;
}) => {
const { claim, payload, body, finalClaimId, actualSessionId, currentFormData } = data;
// Закрываем предыдущее соединение, если есть
if (claimPlanEventSourceRef.current) {
claimPlanEventSourceRef.current.close();
claimPlanEventSourceRef.current = null;
}
// Извлекаем данные из body (telegram) или напрямую из payload (web_form)
const applicantData = body.applicant || payload.applicant || {};
const caseData = body.case || payload.case || {};
const contractData = body.contract_or_service || payload.contract_or_service || {};
const offendersData = body.offenders || payload.offenders || [];
const claimData = body.claim || payload.claim || {};
const metaData = body.meta || payload.meta || {};
const documentsMeta = body.documents_meta || payload.documents_meta || [];
// Очищаем предыдущий таймаут
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) => {
// Извлекаем ответы на вопросы из wizard_answers
const wizardAnswers = body.answers || payload.answers || {};
let answersParsed = wizardAnswers;
if (typeof wizardAnswers === 'string') {
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);
answersParsed = JSON.parse(wizardAnswers);
} catch (e) {
console.warn('⚠️ Не удалось распарсить answers:', e);
answersParsed = {};
}
}
// Формируем attachments_names из documents_meta
const attachmentsNames = documentsMeta.map((doc: any) => {
return doc.original_file_name || doc.file_name || doc.field_name || 'Документ';
});
// Формируем attachments с полной информацией
const attachments = documentsMeta.map((doc: any) => ({
label: doc.original_file_name || doc.file_name || doc.field_name || 'Документ',
url: doc.file_id ? `https://s3.twcstorage.ru${doc.file_id}` : '',
file_id: doc.file_id || '',
stored_file_name: doc.file_name || '',
original_file_name: doc.original_file_name || doc.file_name || '',
field_name: doc.field_name || '',
uploaded_at: doc.uploaded_at || new Date().toISOString(),
}));
// Формируем propertyName в нужном формате
const propertyName = {
applicant: {
first_name: applicantData.first_name || null,
middle_name: applicantData.middle_name || null,
last_name: applicantData.last_name || null,
full_name: applicantData.full_name || null,
birth_date: applicantData.birth_date || null,
birth_date_fmt: applicantData.birth_date_fmt || null,
birth_place: applicantData.birth_place || null,
inn: applicantData.inn || null,
address: applicantData.address || null,
phone: claim.phone || payload.phone || body.phone || currentFormData.phone || null,
email: claim.email || payload.email || body.email || currentFormData.email || null,
},
case: {
category: caseData.category || payload.case_type || 'consumer',
direction: caseData.direction || 'web_form',
country: caseData.country || null,
},
contract_or_service: {
agreement_date: contractData.agreement_date || null,
agreement_date_fmt: contractData.agreement_date_fmt || null,
amount_paid: contractData.amount_paid || null,
amount_paid_fmt: contractData.amount_paid_fmt || null,
subject: contractData.subject || payload.problem_description || body.problem_description || null,
period_start: contractData.period_start || null,
period_start_fmt: contractData.period_start_fmt || null,
period_end: contractData.period_end || null,
period_end_fmt: contractData.period_end_fmt || null,
period_text: contractData.period_text || null,
},
offenders: offendersData.length > 0 ? offendersData : [],
claim: {
reason: claimData.reason || caseData.category || 'consumer',
description: claimData.description || payload.problem_description || body.problem_description || null,
},
meta: {
claim_id: finalClaimId,
unified_id: claim.unified_id || currentFormData.unified_id || null,
status: claim.status_code || 'draft',
created_at: claim.created_at || new Date().toISOString(),
updated_at: claim.updated_at || new Date().toISOString(),
user_id: metaData.user_id || null,
},
attachments: attachments,
attachments_count: attachments.length,
attachments_names: attachmentsNames,
};
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]);
// Возвращаем данные в формате массива (как ожидает форма подтверждения)
return [{
propertyName: propertyName,
session_token: actualSessionId,
prefix: '',
telegram_id: null,
claim_id: finalClaimId,
unified_id: claim.unified_id || currentFormData.unified_id || null,
user_id: metaData.user_id || null,
}];
}, []);
// Загрузка черновика
const loadDraft = useCallback(async (claimId: string) => {
@@ -505,16 +511,29 @@ export default function ClaimForm() {
// ✅ Если все шаги заполнены и статус = draft → переходим к форме подтверждения
if (isReadyForConfirmation) {
console.log('✅ Все шаги заполнены, переходим к форме подтверждения');
console.log('✅ Все шаги заполнены, преобразуем данные для формы подтверждения');
setIsPhoneVerified(true);
// Подписываемся на канал claim:plan для получения данных заявления
subscribeToClaimPlanForDraft(actualSessionId, finalClaimId);
// Преобразуем данные из БД в формат propertyName для формы подтверждения
const claimPlanData = transformDraftToClaimPlanFormat({
claim,
payload,
body,
isTelegramFormat,
finalClaimId,
actualSessionId,
currentFormData: formData,
});
// Пока устанавливаем шаг визарда, переход к подтверждению произойдёт автоматически
// когда данные будут получены через SSE
setCurrentStep(2); // StepWizardPlan
// Сохраняем данные заявления в formData
updateFormData({
claimPlanData: claimPlanData,
showClaimConfirmation: true,
});
// Переход к шагу подтверждения произойдёт автоматически через useEffect
setCurrentStep(2); // StepWizardPlan (временно, useEffect переключит на подтверждение)
return;
}