2025-10-29 15:12:20 +03:00
|
|
|
|
# 📋 Лог сессии: Рефакторинг визарда на динамические шаги
|
|
|
|
|
|
|
|
|
|
|
|
**Дата:** 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<string, DocumentConfig[]> = {
|
|
|
|
|
|
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<string, {
|
|
|
|
|
|
uploaded: boolean;
|
|
|
|
|
|
data: any;
|
|
|
|
|
|
file_type: string;
|
|
|
|
|
|
skipped?: boolean;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
|
|
|
|
|
|
// Последний шаг: 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<FormData>) => {
|
|
|
|
|
|
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: <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. Прогресс-бар с описаниями
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
<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`
|
|
|
|
|
|
|
|
|
|
|
|
**Решение:**
|
|
|
|
|
|
```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
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
console.log('🔙 Кнопка Назад нажата');
|
|
|
|
|
|
onPrev();
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
disabled={false}
|
|
|
|
|
|
>
|
|
|
|
|
|
← Назад
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
2025-10-30 09:35:27 +03:00
|
|
|
|
|