Files
aiform_dev/docs/CODE_FILES_RENAME_FIXED.js
AI Assistant 0978e485dc 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
2025-11-24 13:36:14 +03:00

165 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === НАСТРОЙКА ===
const FILES_ROWS_NODE = 'editfiletobd1'; // <--- имя ноды, где формировались filesRows
// === ВХОД ИЗ PG ===
const sql = $json || {};
const claim_id = sql?.claim?.claim_id || $json.claim_id || null;
const docs = Array.isArray(sql.documents) ? sql.documents : [];
// === filesRows из предыдущей ноды ===
const filesRows = $items(FILES_ROWS_NODE)?.[0]?.json?.filesRows || [];
// === Получаем project_id и project_name ===
// Пробуем получить из Edit Fields6, затем из текущего $json, затем из предыдущих нод
let project_id = null;
let project_name = null;
// 1. Пробуем из Edit Fields6
try {
const editFields6 = $('Edit Fields6').first().json.propertyName || {};
project_id = editFields6.project_id || null;
project_name = editFields6.project_name || null;
} catch (e) {
// Игнорируем, пробуем другие источники
}
// 2. Пробуем из текущего $json
if (!project_id) {
project_id = $json?.project_id || null;
}
if (!project_name) {
project_name = $json?.project_name || null;
}
// 3. Пробуем из предыдущей ноды (если есть нода, которая получает данные из Redis)
if (!project_name) {
try {
// Пробуем найти ноду, которая получает данные сессии из Redis
const redisNode = $node["Get Session"] || $node["GetSession"] || $node["Redis Get"];
if (redisNode && redisNode.json) {
const sessionData = typeof redisNode.json.value === 'string'
? JSON.parse(redisNode.json.value)
: redisNode.json.value;
if (!project_name) {
project_name = sessionData?.project_name || null;
}
if (!project_id) {
project_id = sessionData?.project_id || null;
}
}
} catch (e) {
// Игнорируем ошибки
}
}
// === Утилиты ===
const S = v => (v == null ? '' : String(v));
const extOf = n => (S(n).match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'pdf');
const baseName = k => S(k).split('/').pop() || 'file.pdf';
const slugify = s => {
s = S(s).toLowerCase().trim();
const map = {а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'e',ж:'zh',з:'z',и:'i',й:'y',к:'k',л:'l',
м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f',х:'h',ц:'c',ч:'ch',ш:'sh',
щ:'sch',ъ:'',ы:'y',ь:'',э:'e',ю:'yu',я:'ya'};
s = s.replace(/[а-яё]/g, ch => map[ch] ?? ch);
return s.replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'') || 'doc';
};
// field_name -> doc_id
const byField = Object.create(null);
for (const d of docs) if (d?.field_name && d?.id) byField[d.field_name] = d.id;
// Правило финального пути
// Новый формат: /f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/Project/{project_name}_{project_id}/{doc_id}__{slug}.{ext}
// Пример: /.../Project/ERV_6381_КлиентПрав_398957/{doc_id}__{slug}.{ext}
// project_name уже содержит "ERV_6381_КлиентПрав", просто добавляем к нему _project_id
const buildFinalKey = (row, doc_id) => {
const bucketPrefix = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
const basePath = 'crm2/CRM_Active_Files/Documents/Project';
// Используем название поля из формы визарда
// Приоритет: field_label (из uploads_field_labels) > field_name (из uploads_field_names) > description > group_index
// field_label должен приходить из n8n workflow, где формируется filesRows из uploads_field_labels[i]
const fieldLabel = row.field_label || row.field_name || row.description || `group-${row.group_index}`;
const slug = slugify(fieldLabel);
const ext = extOf(row.original_name || 'pdf');
// Формируем название папки: {project_name}_{project_id}
// project_name уже содержит "ERV_6381_КлиентПрав", добавляем к нему _project_id
let projectFolder = 'unknown';
if (project_name && project_id) {
projectFolder = `${project_name}_${project_id}`;
} else if (project_id) {
// Fallback: если нет project_name, используем старый формат
projectFolder = `${project_id}_Клиентправ`;
}
// Очищаем от недопустимых символов для пути
projectFolder = String(projectFolder)
.replace(/[\/\\\?\*\:\|\"<>]/g, '_')
.replace(/^\.+|\.+$/g, '')
.trim() || 'unknown';
// Формируем путь: /{bucketPrefix}/{basePath}/{project_folder}/{doc_id}__{slug}.{ext}
return `/${bucketPrefix}/${basePath}/${projectFolder}/${doc_id}__${slug}.${ext}`;
};
// Собираем renames + финальные метаданные
const renames = [];
const finalDocumentsMeta = [];
const nowIso = new Date().toISOString();
for (const row of filesRows) {
const g = Number(row.group_index) || 0;
const field_name = `uploads[${g}][0]`;
const doc_id = byField[field_name] || `grp${g}`; // фолбэк если чего-то не сошлось
const fromKey = S(row.draft_key);
const toKey = buildFinalKey(row, doc_id);
// Получаем название поля из row (field_label должен быть добавлен в ноде editfiletobd1)
const field_label = row.field_label || row.field_name || row.description || `group-${g}`;
renames.push({
from: fromKey,
to: toKey,
doc_id,
field_name,
field_label // ✅ Добавляем название поля
});
finalDocumentsMeta.push({
field_name,
field_label, // ✅ Добавляем название поля
file_id: toKey,
file_name: baseName(toKey),
original_file_name: baseName(row.original_name || toKey),
uploaded_at: nowIso
});
}
return [{
json: {
claim_id,
project_id,
renames, // план копирования/переименования на S3
payload_partial_json: { documents_meta: finalDocumentsMeta } // для финального апдейта в БД
}
}];