- Документирован полный рефакторинг визарда (Вариант B) - Каждый документ = отдельный шаг в прогресс-баре - Созданы Step2EventType.tsx, StepDocumentUpload.tsx, documentConfigs.ts - Переделан ClaimForm.tsx на динамические шаги через useMemo - Исправлены проблемы: URL n8n, FormData структура, SSE логирование - Исправлены прогресс и навигация через useCallback - Всего 9 коммитов, ~1500 строк кода
22 KiB
📋 Лог сессии: Рефакторинг визарда на динамические шаги
Дата: 29 октября 2025 (12:00 - 15:00 MSK)
Задача: Переделка визарда - каждый документ отдельным шагом (Вариант B)
Статус: ✅ Успешно завершено
🎯 Основная задача
Переделать структуру визарда так, чтобы каждый документ был отдельным шагом в прогресс-баре:
Было (inline документы):
[1. Полис] → [2. Детали + все документы] → [3. Оплата]
Стало (каждый документ = шаг):
[1. Полис] → [2. Тип] → [3. Док 1] → [4. Док 2] → [5. Док 3] → [N. Оплата]
✅ Выполненные задачи
1. Создан Step2EventType.tsx
Назначение: Выбор типа страхового случая
Функционал:
- Выпадающий список с иконками (✈️, 🚂, ⛴️)
- 7 типов событий: delay_flight, cancel_flight, miss_connection, emergency_landing, delay_train, cancel_train, delay_ferry
- Alert с подтверждением выбора
- DEV MODE кнопка для быстрого выбора "Отмена рейса"
Файл: frontend/src/components/form/Step2EventType.tsx
2. Создан StepDocumentUpload.tsx
Назначение: Универсальный компонент для загрузки одного документа
Функционал:
- Прогресс-бар: "Документ X из Y" + процент завершения
- Upload компонент для выбора файлов
- Автоматическая загрузка на n8n webhook
- SSE для получения результатов AI обработки
- Модалка "Обрабатываем документ..." с результатами
- Проверка
isAlreadyUploadedдля пропуска повторной загрузки - Кнопки: "Назад", "Загрузить", "Пропустить" (для необязательных)
- DEV MODE: "Назад" и "Пропустить [dev]"
Props:
{
documentConfig: DocumentConfig; // Конфигурация документа
formData: any; // Данные формы
updateFormData: (data) => void; // Обновление данных
onNext: () => void; // Следующий шаг
onPrev: () => void; // Предыдущий шаг
isLastDocument: boolean; // Последний документ?
currentDocNumber: number; // Номер текущего документа
totalDocs: number; // Всего документов
}
Файл: frontend/src/components/form/StepDocumentUpload.tsx
3. Создан constants/documentConfigs.ts
Назначение: Централизованная конфигурация документов для всех типов событий
Структура:
export interface DocumentConfig {
name: string; // Название документа
field: string; // Поле в formData
file_type: string; // Уникальный идентификатор для n8n
required: boolean; // Обязательный?
maxFiles: number; // Максимум файлов
description: string; // Описание для пользователя
}
export const DOCUMENT_CONFIGS: Record<string, DocumentConfig[]> = {
delay_flight: [...],
cancel_flight: [...],
miss_connection: [...],
delay_train: [...],
cancel_train: [...],
delay_ferry: [...],
emergency_landing: [...]
};
Пример для отмены рейса:
cancel_flight: [
{
name: "Билет",
field: "ticket",
file_type: "flight_cancel_ticket",
required: true,
maxFiles: 1,
description: "Ticket/booking confirmation"
},
{
name: "Уведомление об отмене",
field: "cancellation_notice",
file_type: "flight_cancel_notice",
required: true,
maxFiles: 3,
description: "Email, SMS или скриншот из приложения АК"
}
]
Функции:
getDocumentsForEventType(eventType)- получить список документовgetTotalDocumentsCount(eventType)- количество документов
Файл: frontend/src/constants/documentConfigs.ts
4. Переделан ClaimForm.tsx на динамические шаги
Изменения:
4.1. Импорты
import { useState, useMemo, useCallback } from 'react';
import Step2EventType from '../components/form/Step2EventType';
import StepDocumentUpload from '../components/form/StepDocumentUpload';
import { getDocumentsForEventType } from '../constants/documentConfigs';
4.2. FormData интерфейс
interface FormData {
// Шаг 1: Policy
voucher: string;
claim_id?: string;
session_id?: string;
// Шаг 2: Event Type
eventType?: string;
// Шаги 3+: Documents
documents?: Record<string, {
uploaded: boolean;
data: any;
file_type: string;
skipped?: boolean;
}>;
// Последний шаг: Payment
fullName?: string;
email?: string;
phone?: string;
paymentMethod?: string;
// ...
}
4.3. Динамическое определение документов
const documentConfigs = formData.eventType
? getDocumentsForEventType(formData.eventType)
: [];
const totalDocumentSteps = documentConfigs.length;
4.4. useCallback для функций навигации
const nextStep = useCallback(() => {
console.log('⏩ nextStep called');
setCurrentStep((prev) => {
console.log('📍 Current step:', prev, '→ Next:', prev + 1);
return prev + 1;
});
}, []);
const prevStep = useCallback(() => {
console.log('⏪ prevStep called');
setCurrentStep((prev) => {
console.log('📍 Current step:', prev, '→ Prev:', prev - 1);
return prev - 1;
});
}, []);
const updateFormData = useCallback((data: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...data }));
}, []);
Почему useCallback критично:
- Без useCallback функции пересоздаются при каждом рендере
- Компоненты получают новые ссылки → ререндер → closure захватывает старые значения
prevStepвызывался, ноsetCurrentStepне срабатывал
4.5. Динамическая генерация шагов через useMemo
const steps = useMemo(() => {
const stepsArray: any[] = [];
// Шаг 1: Policy (всегда)
stepsArray.push({
title: 'Проверка полиса',
description: 'Полис ERV',
content: <Step1Policy ... />
});
// Шаг 2: Event Type (всегда)
stepsArray.push({
title: 'Тип события',
description: 'Выбор случая',
content: <Step2EventType ... />
});
// Шаги 3+: Documents (динамически)
if (formData.eventType && documentConfigs.length > 0) {
documentConfigs.forEach((docConfig, index) => {
stepsArray.push({
title: `Документ ${index + 1}`,
description: docConfig.name,
content: <StepDocumentUpload ... />
});
});
}
// Последний шаг: Payment (всегда)
stepsArray.push({
title: 'Оплата',
description: 'Контакты и выплата',
content: <Step3Payment ... />
});
return stepsArray;
}, [formData, documentConfigs, isPhoneVerified, claimId, sessionId,
nextStep, prevStep, updateFormData, handleSubmit,
setIsPhoneVerified, addDebugEvent]);
4.6. Прогресс-бар с описаниями
<Steps current={currentStep} className="steps">
{steps.map((item, index) => (
<Step
key={`step-${index}`}
title={item.title}
description={item.description}
/>
))}
</Steps>
Файл: frontend/src/pages/ClaimForm.tsx
5. Бэкап старых версий
Step2Details.OLD_MANUAL_INPUT.tsx- версия с ручным вводом полейStep2Details.OLD_WIZARD_INLINE.tsx- версия с inline загрузкой документов
🐛 Исправленные проблемы
Проблема 1: Неправильный URL n8n webhook
Симптом:
POST https://n8n.clientright.ru/webhook/erv-upload
net::ERR_NAME_NOT_RESOLVED
Причина: Использовался несуществующий домен n8n.clientright.ru
Решение:
- https://n8n.clientright.ru/webhook/erv-upload
+ https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95
Commit: 4e5bc76
Проблема 2: Неправильная структура FormData
Симптом: n8n получал данные в неправильном формате
Было:
formDataToSend.append('files', file); // множественное число
// Нет filename и upload_timestamp
Стало:
formDataToSend.append('claim_id', claimId);
formDataToSend.append('file_type', documentConfig.file_type);
formDataToSend.append('filename', file.name); // ✅
formDataToSend.append('voucher', formData.voucher);
formDataToSend.append('session_id', sessionId);
formDataToSend.append('upload_timestamp', new Date().toISOString()); // ✅
formDataToSend.append('file', file.originFileObj); // ✅ единственное число
Commit: 4ad6b78
Проблема 3: Ложные ошибки SSE в консоли
Симптом:
❌ SSE connection error: Event {...}
Причина: Backend закрывает SSE после отправки результата → браузер триггерит onerror → выводится красная ошибка
Решение:
eventSource.onerror = (error) => {
console.log('🔌 SSE connection closed');
setProcessingModalContent((prev) => {
if (prev && prev !== 'loading') {
console.log('✅ SSE закрыто после получения результата - всё ОК');
return prev; // Не затираем результат
}
console.error('❌ SSE ошибка: не получили данные', error);
return { success: false, message: 'Ошибка подключения' };
});
};
Commit: 67f054d
Проблема 4: Неправильный расчёт прогресса
Симптом: "Документ 2/2" показывал "100%" ДО загрузки
Было:
percent = (currentDocNumber / totalDocs) * 100
// Документ 2/2 = 100% (неправильно!)
Стало:
percent = ((currentDocNumber - 1) / totalDocs) * 100
// Документ 1/2: 0% (до) → 50% (после)
// Документ 2/2: 50% (до) → 100% (после)
Commit: 145a9bd
Проблема 5: Кнопки "Назад" не кликабельны
Симптом: Кнопки "Назад" серые (disabled), хотя в коде disabled не было
Решение: Явно установил disabled={false} и добавил логирование:
<Button
onClick={() => {
console.log('🔙 Кнопка Назад нажата');
onPrev();
}}
size="large"
disabled={false}
>
← Назад
</Button>
Commit: d727b74
Проблема 6: Навигация назад не работает
Симптом: Клик регистрируется в консоли, но currentStep не изменяется
Причина:
- Функции
nextStep,prevStepпересоздавались при каждом рендере - Компоненты получали новые ссылки → ререндер
- Closure захватывал старое значение
currentStep
Решение: Обернул в useCallback + functional update:
const prevStep = useCallback(() => {
console.log('⏪ prevStep called');
setCurrentStep((prev) => {
console.log('📍 Current step:', prev, '→ Prev:', prev - 1);
return prev - 1; // Functional update!
});
}, []);
Commit: 9f39847
📦 Git История
# Commit history (от старого к новому)
6fe1459 - backup: Сохранён старый Step2Details с ручным вводом полей
122af07 - feat: Умная форма Step2 с автоматическим распознаванием документов
9084d75 - feat: Пошаговая загрузка документов с модалкой на Step 2
2999951 - fix: Удалён дублирующийся код в Step1Policy.tsx
1207222 - fix: Удалён дублирующийся код в Step3Payment.tsx
6c19392 - docs: Обновлён лог сессии - добавлена вторая часть (умная форма Step 2)
# Сессия 29 октября (рефакторинг на динамические шаги)
1f25301 - feat: Переделан визард на динамические шаги - каждый документ отдельный Step
f06105d - fix: Исправлена работа Upload и кнопки Назад в StepDocumentUpload
4e5bc76 - fix: Исправлен URL n8n webhook на правильный домен
4ad6b78 - fix: Исправлена структура FormData для загрузки документов
67f054d - fix: Улучшено логирование SSE - убраны ложные ошибки
145a9bd - fix: Исправлен расчёт прогресса загрузки документов
d727b74 - fix: Явно установлен disabled=false для всех кнопок Назад
9f39847 - fix: Исправлена навигация назад через useCallback
Push: ✅ origin/main (все коммиты)
🎨 Примеры визуализации
Пример 1: Отмена рейса (2 документа)
Шаг 1: Проверка полиса
└─ Полис ERV
Шаг 2: Тип события
└─ ✈️❌ Отмена авиарейса
Шаг 3: Документ 1 (0% → 50%)
└─ Билет
└─ Upload → n8n → AI → SSE → Модалка с результатами
Шаг 4: Документ 2 (50% → 100%)
└─ Уведомление об отмене
└─ Upload → n8n → AI → SSE → Модалка с результатами
Шаг 5: Оплата
└─ Контакты и выплата
Пример 2: Пропуск стыковки (3 документа, 1 опциональный)
[1.Полис] → [2.Тип] → [3.Посадочный талон прибытия] →
[4.Билет отправления] → [5.Доказательство задержки (опционально)] →
[6.Оплата]
🔧 Технические детали
Data Flow для одного документа
Frontend (StepDocumentUpload)
│
├─ User selects file
│ └─ Upload component → setFileList([file])
│
├─ User clicks "Загрузить и обработать"
│ └─ handleUpload() called
│
├─ FormData creation
│ ├─ claim_id
│ ├─ file_type (уникальный для каждого документа)
│ ├─ filename
│ ├─ voucher
│ ├─ session_id
│ ├─ upload_timestamp
│ └─ file (originFileObj)
│
├─ POST to n8n webhook
│ └─ https://n8n.clientright.pro/webhook/7e2abc64-...
│
├─ SSE connection opens
│ └─ GET /events/{claim_id}?event_type={file_type}_processed
│
├─ Show modal "Обрабатываем документ..."
│ └─ Spin + "Извлекаем данные с помощью AI"
│
│ [n8n workflow]
│ ├─ Upload to S3
│ ├─ PostgreSQL UPSERT (claims + claim_files)
│ ├─ OCR Service (147.45.146.17:8001)
│ ├─ AI Vision (OpenRouter Gemini 2.0 Flash)
│ └─ Redis PUBLISH to ocr_events:{claim_id}
│
├─ Backend receives Redis message
│ └─ SSE sends event to frontend
│
├─ Frontend receives SSE message
│ └─ eventSource.onmessage
│ └─ setProcessingModalContent(result.data)
│
├─ Modal shows results
│ ├─ ✅ Документ обработан
│ ├─ JSON with extracted data
│ └─ Button: "Продолжить к следующему документу →"
│
├─ User clicks "Продолжить"
│ └─ handleContinue()
│ ├─ setProcessingModalVisible(false)
│ ├─ setUploading(false)
│ ├─ eventSource.close()
│ └─ onNext() → nextStep() → setCurrentStep(prev => prev + 1)
│
└─ Next document step renders (or Payment if last)
Уникальные file_type для n8n
| Событие | Документ | file_type | event_type |
|---|---|---|---|
| Задержка рейса | Талон/билет | flight_delay_boarding_or_ticket |
flight_delay_boarding_or_ticket_processed |
| Задержка рейса | Подтверждение | flight_delay_confirmation |
flight_delay_confirmation_processed |
| Отмена рейса | Билет | flight_cancel_ticket |
flight_cancel_ticket_processed |
| Отмена рейса | Уведомление | flight_cancel_notice |
flight_cancel_notice_processed |
| Пропуск стыковки | Талон прибытия | connection_arrival_boarding |
connection_arrival_boarding_processed |
| Пропуск стыковки | Талон/билет отправления | connection_departure_boarding_or_ticket |
connection_departure_boarding_or_ticket_processed |
| Пропуск стыковки | Доказательство задержки | connection_delay_proof |
connection_delay_proof_processed |
Формула: event_type = file_type + "_processed"
📊 Метрики
Время выполнения сессии: ~3 часа
Количество коммитов: 9
Созданных файлов: 3
Step2EventType.tsxStepDocumentUpload.tsxconstants/documentConfigs.ts
Изменённых файлов: 2
ClaimForm.tsx(полная переделка логики)StepDocumentUpload.tsx(множество фиксов)
Строк добавлено: ~1500
Строк удалено: ~50
Frontend rebuilds: 9
Тестовых загрузок: 5
🔗 Ссылки
- Frontend: http://147.45.146.17:5173
- Backend API: http://localhost:8100
- Gitea: http://147.45.146.17:3002/negodiy/erv-platform
- n8n Production: https://n8n.clientright.pro
- n8n Dev: http://147.45.146.17:5678
- n8n Webhook: https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95
📝 Важные заметки
Redis Configuration
Host: crm.clientright.ru
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Channel pattern: ocr_events:{claim_id}
DEV MODE во всех шагах
Для ускорения разработки и тестирования добавлены кнопки быстрой навигации:
- Step 1: "Далее → (Step 2) [пропустить]"
- Step 2: "Далее → [Отмена рейса]"
- Step 3+: "Пропустить [dev] →"
- Step Payment: "✅ Автоподтверждение телефона [dev]", "🚀 Отправить [пропустить]"
Ant Design Warnings (не критично)
В консоли показываются deprecation warnings:
headStyle→styles.headerbodyStyle→styles.bodyTimeline.Item→items
Эти warning в DebugPanel.tsx - не влияют на работу, можно исправить позже.
✅ Итоговый результат
Что работает:
- ✅ Динамические шаги на основе выбранного
eventType - ✅ Каждый документ загружается на отдельном шаге
- ✅ Прогресс-бар показывает все шаги с описаниями
- ✅ Upload → n8n → S3 → PostgreSQL → OCR → AI → Redis → SSE
- ✅ Модалка показывает процесс обработки и результаты
- ✅ Навигация вперёд/назад работает корректно
- ✅ DEV MODE кнопки на всех шагах
- ✅ Логирование в консоль для отладки
Архитектура:
ClaimForm (главный компонент)
├─ useMemo для динамической генерации steps
├─ useCallback для стабильных функций навигации
│
├─ Step 1: Step1Policy
│ └─ Загрузка и OCR полиса
│
├─ Step 2: Step2EventType
│ └─ Выбор типа события
│
├─ Steps 3...N-1: StepDocumentUpload (динамически)
│ └─ Для каждого документа из DOCUMENT_CONFIGS
│ ├─ Прогресс: "Документ X из Y"
│ ├─ Upload компонент
│ ├─ POST to n8n → S3 → DB → OCR → AI
│ ├─ SSE для получения результата
│ └─ Модалка с извлечёнными данными
│
└─ Step N: Step3Payment
└─ Контакты и выплата
Статус: ✅ Успешно завершено
Автор: AI Assistant (Claude Sonnet 4.5)
Дата: 29 октября 2025, 15:00 MSK