# 📋 Лог сессии: Рефакторинг визарда на динамические шаги **Дата:** 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:** ```typescript { 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 **Назначение:** Централизованная конфигурация документов для всех типов событий **Структура:** ```typescript export interface DocumentConfig { name: string; // Название документа field: string; // Поле в formData file_type: string; // Уникальный идентификатор для n8n required: boolean; // Обязательный? maxFiles: number; // Максимум файлов description: string; // Описание для пользователя } export const DOCUMENT_CONFIGS: Record = { delay_flight: [...], cancel_flight: [...], miss_connection: [...], delay_train: [...], cancel_train: [...], delay_ferry: [...], emergency_landing: [...] }; ``` **Пример для отмены рейса:** ```typescript 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. Импорты ```typescript 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 интерфейс ```typescript interface FormData { // Шаг 1: Policy voucher: string; claim_id?: string; session_id?: string; // Шаг 2: Event Type eventType?: string; // Шаги 3+: Documents documents?: Record; // Последний шаг: Payment fullName?: string; email?: string; phone?: string; paymentMethod?: string; // ... } ``` #### 4.3. Динамическое определение документов ```typescript const documentConfigs = formData.eventType ? getDocumentsForEventType(formData.eventType) : []; const totalDocumentSteps = documentConfigs.length; ``` #### 4.4. useCallback для функций навигации ```typescript 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) => { setFormData((prev) => ({ ...prev, ...data })); }, []); ``` **Почему useCallback критично:** - Без useCallback функции пересоздаются при каждом рендере - Компоненты получают новые ссылки → ререндер → closure захватывает старые значения - `prevStep` вызывался, но `setCurrentStep` не срабатывал #### 4.5. Динамическая генерация шагов через useMemo ```typescript const steps = useMemo(() => { const stepsArray: any[] = []; // Шаг 1: Policy (всегда) stepsArray.push({ title: 'Проверка полиса', description: 'Полис ERV', content: }); // Шаг 2: Event Type (всегда) stepsArray.push({ title: 'Тип события', description: 'Выбор случая', content: }); // Шаги 3+: Documents (динамически) if (formData.eventType && documentConfigs.length > 0) { documentConfigs.forEach((docConfig, index) => { stepsArray.push({ title: `Документ ${index + 1}`, description: docConfig.name, content: }); }); } // Последний шаг: Payment (всегда) stepsArray.push({ title: 'Оплата', description: 'Контакты и выплата', content: }); return stepsArray; }, [formData, documentConfigs, isPhoneVerified, claimId, sessionId, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent]); ``` #### 4.6. Прогресс-бар с описаниями ```typescript {steps.map((item, index) => ( ))} ``` **Файл:** `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` **Решение:** ```diff - https://n8n.clientright.ru/webhook/erv-upload + https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95 ``` **Commit:** `4e5bc76` --- ### Проблема 2: Неправильная структура FormData **Симптом:** n8n получал данные в неправильном формате **Было:** ```javascript formDataToSend.append('files', file); // множественное число // Нет filename и upload_timestamp ``` **Стало:** ```javascript 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` → выводится красная ошибка **Решение:** ```javascript 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%" ДО загрузки **Было:** ```javascript percent = (currentDocNumber / totalDocs) * 100 // Документ 2/2 = 100% (неправильно!) ``` **Стало:** ```javascript percent = ((currentDocNumber - 1) / totalDocs) * 100 // Документ 1/2: 0% (до) → 50% (после) // Документ 2/2: 50% (до) → 100% (после) ``` **Commit:** `145a9bd` --- ### Проблема 5: Кнопки "Назад" не кликабельны **Симптом:** Кнопки "Назад" серые (disabled), хотя в коде `disabled` не было **Решение:** Явно установил `disabled={false}` и добавил логирование: ```javascript ``` **Commit:** `d727b74` --- ### Проблема 6: Навигация назад не работает **Симптом:** Клик регистрируется в консоли, но `currentStep` не изменяется **Причина:** - Функции `nextStep`, `prevStep` пересоздавались при каждом рендере - Компоненты получали новые ссылки → ререндер - Closure захватывал старое значение `currentStep` **Решение:** Обернул в `useCallback` + functional update: ```javascript 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 История ```bash # 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.tsx` - `StepDocumentUpload.tsx` - `constants/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.header` - `bodyStyle` → `styles.body` - `Timeline.Item` → `items` Эти warning в `DebugPanel.tsx` - не влияют на работу, можно исправить позже. --- ## ✅ Итоговый результат ### Что работает: 1. ✅ Динамические шаги на основе выбранного `eventType` 2. ✅ Каждый документ загружается на отдельном шаге 3. ✅ Прогресс-бар показывает все шаги с описаниями 4. ✅ Upload → n8n → S3 → PostgreSQL → OCR → AI → Redis → SSE 5. ✅ Модалка показывает процесс обработки и результаты 6. ✅ Навигация вперёд/назад работает корректно 7. ✅ DEV MODE кнопки на всех шагах 8. ✅ Логирование в консоль для отладки ### Архитектура: ``` 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