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
121 lines
4.4 KiB
JavaScript
121 lines
4.4 KiB
JavaScript
// ========================================
|
||
// Code Node: Мерж данных проекта в сессию
|
||
// ========================================
|
||
|
||
// 1. Берём первый item
|
||
const inputItem = $input.all()[0];
|
||
|
||
if (!inputItem || !inputItem.json) {
|
||
throw new Error('Пустой input в Code Node (нет json)');
|
||
}
|
||
|
||
// root — то, что реально пришло в эту ноду
|
||
const root = inputItem.json;
|
||
|
||
// 2. Универсально получаем body
|
||
// - если нода стоит сразу после Webhook → данные лежат в root.body
|
||
// - если кто-то выше уже отдал только body → root и есть body
|
||
const body = root.body || root;
|
||
|
||
// 3. Парсим body.other (если есть) как сессию
|
||
let sessionData = {};
|
||
const rawOther = body.other;
|
||
|
||
if (rawOther) {
|
||
if (typeof rawOther === 'string') {
|
||
try {
|
||
sessionData = JSON.parse(rawOther);
|
||
} catch (e) {
|
||
throw new Error('Не смог распарсить body.other как JSON: ' + e.message + '. rawOther: ' + rawOther);
|
||
}
|
||
} else if (typeof rawOther === 'object') {
|
||
sessionData = rawOther;
|
||
}
|
||
}
|
||
|
||
// 4. Определяем claimId (основной путь)
|
||
let claimId = body.claim_id || sessionData.claim_id || null;
|
||
|
||
// 5. Fallback: пробуем достать claim_id напрямую из Webhook, если его до сих пор нет
|
||
if (!claimId) {
|
||
try {
|
||
const webhookNodeJson = $('Webhook').first()?.json;
|
||
if (webhookNodeJson?.body?.claim_id) {
|
||
claimId = webhookNodeJson.body.claim_id;
|
||
}
|
||
} catch (e) {
|
||
// молча игнорируем, просто не удалось взять из Webhook
|
||
}
|
||
}
|
||
|
||
// 6. Если всё ещё нет claimId — это реально критичная ситуация
|
||
if (!claimId) {
|
||
throw new Error(
|
||
'Нет claim_id ни в body, ни в sessionData, ни в Webhook. ' +
|
||
'body: ' + JSON.stringify(body) +
|
||
', sessionData: ' + JSON.stringify(sessionData)
|
||
);
|
||
}
|
||
|
||
// 7. Забираем результат ноды CreateClientProject (или CreateWebPorject, если опечатка в названии ноды)
|
||
let projectNode = null;
|
||
let projectNodeName = null;
|
||
|
||
// Пробуем найти ноду безопасно
|
||
try {
|
||
projectNode = $node["CreateClientProject"];
|
||
if (projectNode && projectNode.json) {
|
||
projectNodeName = "CreateClientProject";
|
||
}
|
||
} catch (e) {
|
||
// Нода CreateClientProject не найдена, пробуем альтернативное название
|
||
}
|
||
|
||
if (!projectNode || !projectNode.json) {
|
||
try {
|
||
projectNode = $node["CreateWebPorject"];
|
||
if (projectNode && projectNode.json) {
|
||
projectNodeName = "CreateWebPorject";
|
||
}
|
||
} catch (e) {
|
||
// Нода CreateWebPorject тоже не найдена
|
||
}
|
||
}
|
||
|
||
if (!projectNode || !projectNode.json) {
|
||
throw new Error('Нет данных от ноды CreateClientProject/CreateWebPorject. Убедитесь, что нода существует и выполнена.');
|
||
}
|
||
|
||
const projectResult = projectNode.json.result;
|
||
// Ожидаем что-то типа: { "project_id": "398095", "project_name": "Иванов_КлиентПрав", "is_new": false }
|
||
|
||
if (!projectResult || !projectResult.project_id) {
|
||
throw new Error('Нет projectResult.project_id. result: ' + JSON.stringify(projectNode.json));
|
||
}
|
||
|
||
// 8. Собираем обновлённую сессию
|
||
const updatedSession = {
|
||
...sessionData, // всё, что было в other
|
||
claim_id: claimId, // актуальный claim_id
|
||
project_id: projectResult.project_id, // id проекта из CRM
|
||
project_name: projectResult.project_name || null, // название проекта из CRM (новое поле)
|
||
is_new_project: projectResult.is_new, // флаг новый/старый
|
||
current_step: 2, // двигаем визард на шаг 2
|
||
updated_at: new Date().toISOString(),
|
||
// опционально дотащим полезные поля из body:
|
||
problem: body.problem ?? sessionData.problem,
|
||
last_analysis_output: body.output ?? sessionData.last_analysis_output,
|
||
};
|
||
|
||
// 9. Возвращаем один item для Redis SET
|
||
return [
|
||
{
|
||
json: {
|
||
redis_key: `claim:${claimId}`,
|
||
redis_value: JSON.stringify(updatedSession),
|
||
ttl: 604800, // 7 дней
|
||
},
|
||
},
|
||
];
|
||
|