2025-11-20 18:31:42 +03:00
|
|
|
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
|
|
|
|
|
import { Steps, Card, message, Row, Col, Space } from 'antd';
|
2025-11-01 13:31:05 +03:00
|
|
|
|
import Step1Phone from '../components/form/Step1Phone';
|
2025-11-14 19:06:36 +03:00
|
|
|
|
import StepDescription from '../components/form/StepDescription';
|
2025-10-24 20:40:44 +03:00
|
|
|
|
import Step1Policy from '../components/form/Step1Policy';
|
2025-11-19 18:46:48 +03:00
|
|
|
|
import StepDraftSelection from '../components/form/StepDraftSelection';
|
2025-11-15 18:48:15 +03:00
|
|
|
|
import StepWizardPlan from '../components/form/StepWizardPlan';
|
2025-11-24 13:36:14 +03:00
|
|
|
|
import StepClaimConfirmation from '../components/form/StepClaimConfirmation';
|
2025-10-29 12:36:30 +03:00
|
|
|
|
import Step2EventType from '../components/form/Step2EventType';
|
|
|
|
|
|
import StepDocumentUpload from '../components/form/StepDocumentUpload';
|
2025-10-24 16:19:58 +03:00
|
|
|
|
import Step3Payment from '../components/form/Step3Payment';
|
2025-10-24 22:13:52 +03:00
|
|
|
|
import DebugPanel from '../components/DebugPanel';
|
2025-10-29 12:36:30 +03:00
|
|
|
|
import { getDocumentsForEventType } from '../constants/documentConfigs';
|
2025-10-24 16:19:58 +03:00
|
|
|
|
import './ClaimForm.css';
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
// Используем относительные пути - Vite proxy перенаправит на backend
|
2025-11-14 19:06:36 +03:00
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
const { Step } = Steps;
|
|
|
|
|
|
|
|
|
|
|
|
interface FormData {
|
2025-11-01 13:31:05 +03:00
|
|
|
|
// Шаг 1: Phone
|
|
|
|
|
|
phone?: string;
|
|
|
|
|
|
contact_id?: string;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
unified_id?: string; // ✅ Unified ID пользователя из PostgreSQL
|
2025-11-01 13:31:05 +03:00
|
|
|
|
is_new_contact?: boolean;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
smsCode?: string;
|
|
|
|
|
|
clientIp?: string;
|
|
|
|
|
|
smsDebugCode?: string;
|
2025-11-01 13:31:05 +03:00
|
|
|
|
|
|
|
|
|
|
// Шаг 2: Policy
|
2025-10-29 12:36:30 +03:00
|
|
|
|
voucher: string;
|
|
|
|
|
|
claim_id?: string;
|
|
|
|
|
|
session_id?: string;
|
2025-11-02 10:25:24 +03:00
|
|
|
|
project_id?: string; // ✅ ID проекта в vTiger (полис)
|
|
|
|
|
|
is_new_project?: boolean; // ✅ Флаг: создан новый проект
|
2025-11-14 19:06:36 +03:00
|
|
|
|
problemDescription?: string;
|
2025-11-15 18:48:15 +03:00
|
|
|
|
wizardPlan?: any;
|
|
|
|
|
|
wizardPlanStatus?: 'pending' | 'ready' | 'answered';
|
|
|
|
|
|
wizardAnswers?: Record<string, any>;
|
|
|
|
|
|
wizardPrefill?: Record<string, any>;
|
|
|
|
|
|
wizardPrefillArray?: Array<{ name: string; value: any }>;
|
|
|
|
|
|
wizardCoverageReport?: any;
|
|
|
|
|
|
wizardUploads?: Record<string, any>;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
wizardSkippedDocuments?: string[];
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-11-24 13:36:14 +03:00
|
|
|
|
// Подтверждение заявления (после получения данных из claim:plan)
|
|
|
|
|
|
showClaimConfirmation?: boolean;
|
|
|
|
|
|
claimPlanData?: any; // Данные заявления от n8n из канала claim:plan
|
|
|
|
|
|
|
2025-11-01 13:31:05 +03:00
|
|
|
|
// Шаг 3: Event Type
|
2025-10-29 12:36:30 +03:00
|
|
|
|
eventType?: string;
|
2025-11-02 10:25:24 +03:00
|
|
|
|
ticket_id?: string; // ✅ ID заявки в vTiger (HelpDesk)
|
|
|
|
|
|
ticket_number?: string; // ✅ Номер заявки (HD001234)
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-11-01 13:31:05 +03:00
|
|
|
|
// Шаги 4+: Documents
|
2025-10-29 12:36:30 +03:00
|
|
|
|
documents?: Record<string, {
|
|
|
|
|
|
uploaded: boolean;
|
|
|
|
|
|
data: any;
|
|
|
|
|
|
file_type: string;
|
|
|
|
|
|
skipped?: boolean;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
|
|
|
|
|
|
// Последний шаг: Payment
|
|
|
|
|
|
fullName?: string;
|
|
|
|
|
|
email?: string;
|
|
|
|
|
|
paymentMethod?: string;
|
2025-10-24 16:19:58 +03:00
|
|
|
|
bankName?: string;
|
|
|
|
|
|
cardNumber?: string;
|
|
|
|
|
|
accountNumber?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function ClaimForm() {
|
2025-11-01 16:53:10 +03:00
|
|
|
|
// ✅ claim_id будет создан n8n в Step1Phone после SMS верификации
|
|
|
|
|
|
// Не генерируем его локально!
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// session_id будет получен от n8n при создании контакта
|
|
|
|
|
|
// Используем useRef чтобы sessionId не вызывал перерендер и был стабильным
|
|
|
|
|
|
const sessionIdRef = useRef(`sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
2025-11-24 14:11:04 +03:00
|
|
|
|
const claimPlanEventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
|
|
const claimPlanTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
const [currentStep, setCurrentStep] = useState(0);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
const [sessionRestored, setSessionRestored] = useState(false); // Флаг: пытались восстановить сессию
|
2025-10-24 16:19:58 +03:00
|
|
|
|
const [formData, setFormData] = useState<FormData>({
|
2025-10-24 20:54:57 +03:00
|
|
|
|
voucher: '',
|
2025-11-01 16:53:10 +03:00
|
|
|
|
claim_id: undefined, // ✅ Будет заполнен n8n в Step1Phone
|
2025-11-20 18:31:42 +03:00
|
|
|
|
session_id: sessionIdRef.current,
|
2025-10-24 16:19:58 +03:00
|
|
|
|
paymentMethod: 'sbp',
|
|
|
|
|
|
});
|
|
|
|
|
|
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
|
2025-10-24 22:13:52 +03:00
|
|
|
|
const [debugEvents, setDebugEvents] = useState<any[]>([]);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
|
|
const [showDraftSelection, setShowDraftSelection] = useState(false);
|
|
|
|
|
|
const [selectedDraftId, setSelectedDraftId] = useState<string | null>(null);
|
|
|
|
|
|
const [hasDrafts, setHasDrafts] = useState(false);
|
2025-10-24 22:13:52 +03:00
|
|
|
|
|
2025-11-15 18:48:15 +03:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился!
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔥 ClaimForm v3.8 - 2025-11-20 15:10 - Fix session_id priority in loadDraft');
|
2025-11-15 18:48:15 +03:00
|
|
|
|
}, []);
|
2025-11-01 16:53:10 +03:00
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ✅ Восстановление сессии при загрузке страницы
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const restoreSession = async () => {
|
|
|
|
|
|
console.log('🔑 🔑 🔑 НАЧАЛО ВОССТАНОВЛЕНИЯ СЕССИИ 🔑 🔑 🔑');
|
|
|
|
|
|
console.log('🔑 Все ключи в localStorage:', Object.keys(localStorage));
|
|
|
|
|
|
console.log('🔑 Значения всех ключей:', JSON.stringify(localStorage));
|
|
|
|
|
|
|
|
|
|
|
|
const savedSessionToken = localStorage.getItem('session_token');
|
|
|
|
|
|
|
|
|
|
|
|
if (!savedSessionToken) {
|
|
|
|
|
|
console.log('❌ Session token NOT found in localStorage');
|
|
|
|
|
|
setSessionRestored(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ Found session_token in localStorage, verifying:', savedSessionToken);
|
|
|
|
|
|
addDebugEvent('session', 'info', '🔑 Проверка сохранённой сессии');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/v1/session/verify', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ session_token: savedSessionToken })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log('🔑 Session verify response:', data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.valid) {
|
|
|
|
|
|
// Сессия валидна! Восстанавливаем состояние
|
|
|
|
|
|
console.log('✅ Session valid! Restoring user data:', {
|
|
|
|
|
|
unified_id: data.unified_id,
|
|
|
|
|
|
phone: data.phone,
|
|
|
|
|
|
expires_in: data.expires_in_seconds
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем formData с данными сессии
|
|
|
|
|
|
updateFormData({
|
|
|
|
|
|
unified_id: data.unified_id,
|
|
|
|
|
|
phone: data.phone,
|
|
|
|
|
|
contact_id: data.contact_id,
|
|
|
|
|
|
session_id: savedSessionToken
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Устанавливаем session_id в ref
|
|
|
|
|
|
sessionIdRef.current = savedSessionToken;
|
|
|
|
|
|
|
|
|
|
|
|
// Помечаем телефон как верифицированный
|
|
|
|
|
|
setIsPhoneVerified(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем черновики
|
|
|
|
|
|
const hasDraftsResult = await checkDrafts(data.unified_id, data.phone, savedSessionToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasDraftsResult) {
|
|
|
|
|
|
// Есть черновики - показываем список
|
|
|
|
|
|
setShowDraftSelection(true);
|
|
|
|
|
|
setHasDrafts(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Переходим к шагу выбора черновика
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
setCurrentStep(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
message.success(`Добро пожаловать! Сессия восстановлена (${data.phone})`);
|
|
|
|
|
|
addDebugEvent('session', 'success', '✅ Сессия восстановлена, найдены черновики');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Нет черновиков - переходим к описанию
|
|
|
|
|
|
setCurrentStep(1);
|
|
|
|
|
|
message.success(`Добро пожаловать! Сессия восстановлена (${data.phone})`);
|
|
|
|
|
|
addDebugEvent('session', 'success', '✅ Сессия восстановлена');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Сессия невалидна - удаляем из localStorage
|
|
|
|
|
|
console.log('❌ Session invalid or expired, removing from localStorage');
|
|
|
|
|
|
localStorage.removeItem('session_token');
|
|
|
|
|
|
addDebugEvent('session', 'warning', '⚠️ Сессия истекла');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ Error verifying session:', error);
|
|
|
|
|
|
localStorage.removeItem('session_token');
|
|
|
|
|
|
addDebugEvent('session', 'error', '❌ Ошибка проверки сессии');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSessionRestored(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
restoreSession();
|
|
|
|
|
|
}, []); // Запускаем только при загрузке
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
// Получаем IP клиента один раз при монтировании
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchClientIp = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/v1/utils/client-ip');
|
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data?.ip) {
|
|
|
|
|
|
setFormData((prev) => ({ ...prev, clientIp: data.ip }));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Тихо игнорируем, IP всегда можно взять на бэке из request
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
fetchClientIp();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-11-24 14:11:04 +03:00
|
|
|
|
// Автоматический переход к шагу подтверждения, когда данные готовы
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (formData.showClaimConfirmation && formData.claimPlanData) {
|
|
|
|
|
|
// Вычисляем индекс шага подтверждения динамически
|
|
|
|
|
|
// Шаг подтверждения добавляется после StepWizardPlan
|
|
|
|
|
|
// После выбора черновика showDraftSelection = false, поэтому:
|
|
|
|
|
|
// - Шаг 0 = Step1Phone
|
|
|
|
|
|
// - Шаг 1 = StepDescription
|
|
|
|
|
|
// - Шаг 2 = StepWizardPlan
|
|
|
|
|
|
// - Шаг 3 = StepClaimConfirmation (если showClaimConfirmation=true)
|
|
|
|
|
|
const confirmationStepIndex = 3; // Фиксированный индекс для шага подтверждения
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ Данные заявления готовы, переходим к шагу подтверждения:', confirmationStepIndex);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setCurrentStep(confirmationStepIndex);
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [formData.showClaimConfirmation, formData.claimPlanData]);
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-29 12:36:30 +03:00
|
|
|
|
// Динамически определяем список шагов на основе выбранного eventType
|
|
|
|
|
|
const documentConfigs = formData.eventType ? getDocumentsForEventType(formData.eventType) : [];
|
|
|
|
|
|
const totalDocumentSteps = documentConfigs.length;
|
|
|
|
|
|
|
2025-10-24 22:13:52 +03:00
|
|
|
|
const addDebugEvent = (type: string, status: string, message: string, data?: any) => {
|
|
|
|
|
|
const event = {
|
|
|
|
|
|
timestamp: new Date().toLocaleTimeString('ru-RU'),
|
|
|
|
|
|
type,
|
|
|
|
|
|
status,
|
|
|
|
|
|
message,
|
2025-10-27 08:33:16 +03:00
|
|
|
|
data: {
|
|
|
|
|
|
...data,
|
2025-11-01 16:53:10 +03:00
|
|
|
|
claim_id: formData.claim_id // ✅ Используем claim_id из formData (от n8n)
|
2025-10-27 08:33:16 +03:00
|
|
|
|
}
|
2025-10-24 22:13:52 +03:00
|
|
|
|
};
|
|
|
|
|
|
setDebugEvents(prev => [event, ...prev]);
|
|
|
|
|
|
};
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-11-01 16:53:10 +03:00
|
|
|
|
// ✅ claim_id будет залогирован в Step1Phone после получения от n8n
|
2025-10-27 08:33:16 +03:00
|
|
|
|
|
2025-10-29 14:09:20 +03:00
|
|
|
|
const updateFormData = useCallback((data: Partial<FormData>) => {
|
|
|
|
|
|
setFormData((prev) => ({ ...prev, ...data }));
|
|
|
|
|
|
}, []);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-29 14:09:20 +03:00
|
|
|
|
const nextStep = useCallback(() => {
|
|
|
|
|
|
console.log('⏩ nextStep called');
|
|
|
|
|
|
setCurrentStep((prev) => {
|
|
|
|
|
|
console.log('📍 Current step:', prev, '→ Next:', prev + 1);
|
|
|
|
|
|
return prev + 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-29 14:09:20 +03:00
|
|
|
|
const prevStep = useCallback(() => {
|
|
|
|
|
|
console.log('⏪ prevStep called');
|
|
|
|
|
|
setCurrentStep((prev) => {
|
|
|
|
|
|
console.log('📍 Current step:', prev, '→ Prev:', prev - 1);
|
|
|
|
|
|
return prev - 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Преобразование данных черновика в формат propertyName для формы подтверждения
|
|
|
|
|
|
const transformDraftToClaimPlanFormat = useCallback((data: {
|
|
|
|
|
|
claim: any;
|
|
|
|
|
|
payload: any;
|
|
|
|
|
|
body: any;
|
|
|
|
|
|
isTelegramFormat: boolean;
|
|
|
|
|
|
finalClaimId: string;
|
|
|
|
|
|
actualSessionId: string;
|
|
|
|
|
|
currentFormData: FormData;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { claim, payload, body, finalClaimId, actualSessionId, currentFormData } = data;
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
2025-11-24 15:16:46 +03:00
|
|
|
|
console.log('🔄 transformDraftToClaimPlanFormat: входные данные:', {
|
|
|
|
|
|
claimId: finalClaimId,
|
|
|
|
|
|
claimUnifiedId: claim.unified_id,
|
|
|
|
|
|
formDataUnifiedId: currentFormData.unified_id,
|
|
|
|
|
|
claimKeys: Object.keys(claim),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-24 16:33:16 +03:00
|
|
|
|
console.log('🔄 Данные из БД:', {
|
|
|
|
|
|
hasApplicantData: !!(body.applicant || payload.applicant),
|
|
|
|
|
|
hasCaseData: !!(body.case || payload.case),
|
|
|
|
|
|
hasContractData: !!(body.contract_or_service || payload.contract_or_service),
|
|
|
|
|
|
hasWizardAnswers: !!(body.answers || payload.answers || body.wizard_answers || payload.wizard_answers),
|
|
|
|
|
|
payloadKeys: Object.keys(payload),
|
|
|
|
|
|
bodyKeys: Object.keys(body),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Извлекаем данные из body (telegram) или напрямую из payload (web_form)
|
|
|
|
|
|
const applicantData = body.applicant || payload.applicant || {};
|
|
|
|
|
|
const caseData = body.case || payload.case || {};
|
|
|
|
|
|
const contractData = body.contract_or_service || payload.contract_or_service || {};
|
|
|
|
|
|
const offendersData = body.offenders || payload.offenders || [];
|
|
|
|
|
|
const claimData = body.claim || payload.claim || {};
|
|
|
|
|
|
const metaData = body.meta || payload.meta || {};
|
|
|
|
|
|
const documentsMeta = body.documents_meta || payload.documents_meta || [];
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Извлекаем ответы на вопросы из wizard_answers
|
2025-11-24 16:33:16 +03:00
|
|
|
|
const wizardAnswers = body.answers || payload.answers || body.wizard_answers || payload.wizard_answers || {};
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
let answersParsed = wizardAnswers;
|
|
|
|
|
|
if (typeof wizardAnswers === 'string') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
answersParsed = JSON.parse(wizardAnswers);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('⚠️ Не удалось распарсить answers:', e);
|
|
|
|
|
|
answersParsed = {};
|
|
|
|
|
|
}
|
2025-11-24 14:11:04 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:33:16 +03:00
|
|
|
|
console.log('🔄 wizard_answers parsed:', answersParsed);
|
|
|
|
|
|
|
|
|
|
|
|
// Преобразуем wizard_answers в формат propertyName, если данных нет в propertyName формате
|
|
|
|
|
|
// Маппинг полей из wizard_answers в propertyName структуру
|
|
|
|
|
|
const hasPropertyNameData = !!(applicantData.first_name || applicantData.last_name || caseData.category || contractData.subject);
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasPropertyNameData && answersParsed && Object.keys(answersParsed).length > 0) {
|
|
|
|
|
|
console.log('🔄 Преобразуем wizard_answers в propertyName формат');
|
|
|
|
|
|
|
|
|
|
|
|
// Маппинг полей из wizard_answers
|
|
|
|
|
|
// Например: answersParsed.item → contract_or_service.subject
|
|
|
|
|
|
// answersParsed.price → contract_or_service.amount_paid
|
|
|
|
|
|
// и т.д.
|
|
|
|
|
|
|
|
|
|
|
|
// Если есть данные в wizard_answers, используем их для заполнения propertyName
|
|
|
|
|
|
if (answersParsed.item && !contractData.subject) {
|
|
|
|
|
|
contractData.subject = answersParsed.item;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (answersParsed.price && !contractData.amount_paid) {
|
|
|
|
|
|
contractData.amount_paid = answersParsed.price;
|
|
|
|
|
|
contractData.amount_paid_fmt = answersParsed.price;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (answersParsed.place_date && !contractData.agreement_date) {
|
|
|
|
|
|
// Можно попытаться распарсить дату из place_date
|
|
|
|
|
|
contractData.agreement_date = answersParsed.place_date;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (answersParsed.steps_taken && !claimData.description) {
|
|
|
|
|
|
claimData.description = answersParsed.steps_taken;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Формируем attachments_names из documents_meta
|
|
|
|
|
|
const attachmentsNames = documentsMeta.map((doc: any) => {
|
|
|
|
|
|
return doc.original_file_name || doc.file_name || doc.field_name || 'Документ';
|
|
|
|
|
|
});
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Формируем attachments с полной информацией
|
|
|
|
|
|
const attachments = documentsMeta.map((doc: any) => ({
|
|
|
|
|
|
label: doc.original_file_name || doc.file_name || doc.field_name || 'Документ',
|
|
|
|
|
|
url: doc.file_id ? `https://s3.twcstorage.ru${doc.file_id}` : '',
|
|
|
|
|
|
file_id: doc.file_id || '',
|
|
|
|
|
|
stored_file_name: doc.file_name || '',
|
|
|
|
|
|
original_file_name: doc.original_file_name || doc.file_name || '',
|
|
|
|
|
|
field_name: doc.field_name || '',
|
|
|
|
|
|
uploaded_at: doc.uploaded_at || new Date().toISOString(),
|
|
|
|
|
|
}));
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Формируем propertyName в нужном формате
|
|
|
|
|
|
const propertyName = {
|
|
|
|
|
|
applicant: {
|
|
|
|
|
|
first_name: applicantData.first_name || null,
|
|
|
|
|
|
middle_name: applicantData.middle_name || null,
|
|
|
|
|
|
last_name: applicantData.last_name || null,
|
|
|
|
|
|
full_name: applicantData.full_name || null,
|
|
|
|
|
|
birth_date: applicantData.birth_date || null,
|
|
|
|
|
|
birth_date_fmt: applicantData.birth_date_fmt || null,
|
|
|
|
|
|
birth_place: applicantData.birth_place || null,
|
|
|
|
|
|
inn: applicantData.inn || null,
|
|
|
|
|
|
address: applicantData.address || null,
|
|
|
|
|
|
phone: claim.phone || payload.phone || body.phone || currentFormData.phone || null,
|
|
|
|
|
|
email: claim.email || payload.email || body.email || currentFormData.email || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
case: {
|
|
|
|
|
|
category: caseData.category || payload.case_type || 'consumer',
|
|
|
|
|
|
direction: caseData.direction || 'web_form',
|
|
|
|
|
|
country: caseData.country || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
contract_or_service: {
|
|
|
|
|
|
agreement_date: contractData.agreement_date || null,
|
|
|
|
|
|
agreement_date_fmt: contractData.agreement_date_fmt || null,
|
|
|
|
|
|
amount_paid: contractData.amount_paid || null,
|
|
|
|
|
|
amount_paid_fmt: contractData.amount_paid_fmt || null,
|
|
|
|
|
|
subject: contractData.subject || payload.problem_description || body.problem_description || null,
|
|
|
|
|
|
period_start: contractData.period_start || null,
|
|
|
|
|
|
period_start_fmt: contractData.period_start_fmt || null,
|
|
|
|
|
|
period_end: contractData.period_end || null,
|
|
|
|
|
|
period_end_fmt: contractData.period_end_fmt || null,
|
|
|
|
|
|
period_text: contractData.period_text || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
offenders: offendersData.length > 0 ? offendersData : [],
|
|
|
|
|
|
claim: {
|
|
|
|
|
|
reason: claimData.reason || caseData.category || 'consumer',
|
|
|
|
|
|
description: claimData.description || payload.problem_description || body.problem_description || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
claim_id: finalClaimId,
|
|
|
|
|
|
unified_id: claim.unified_id || currentFormData.unified_id || null,
|
|
|
|
|
|
status: claim.status_code || 'draft',
|
|
|
|
|
|
created_at: claim.created_at || new Date().toISOString(),
|
|
|
|
|
|
updated_at: claim.updated_at || new Date().toISOString(),
|
|
|
|
|
|
user_id: metaData.user_id || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
attachments: attachments,
|
|
|
|
|
|
attachments_count: attachments.length,
|
|
|
|
|
|
attachments_names: attachmentsNames,
|
2025-11-24 14:11:04 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-24 15:16:46 +03:00
|
|
|
|
// Возвращаем данные в формате объекта (для компонента StepClaimConfirmation)
|
|
|
|
|
|
const result = {
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
propertyName: propertyName,
|
|
|
|
|
|
session_token: actualSessionId,
|
|
|
|
|
|
prefix: '',
|
|
|
|
|
|
telegram_id: null,
|
|
|
|
|
|
claim_id: finalClaimId,
|
|
|
|
|
|
unified_id: claim.unified_id || currentFormData.unified_id || null,
|
|
|
|
|
|
user_id: metaData.user_id || null,
|
2025-11-24 15:16:46 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🔄 transformDraftToClaimPlanFormat: результат:', {
|
|
|
|
|
|
claim_id: result.claim_id,
|
|
|
|
|
|
unified_id: result.unified_id,
|
|
|
|
|
|
hasPropertyName: !!result.propertyName,
|
|
|
|
|
|
hasMeta: !!result.propertyName?.meta,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
}, []);
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
// Загрузка черновика
|
|
|
|
|
|
const loadDraft = useCallback(async (claimId: string) => {
|
2025-10-24 16:19:58 +03:00
|
|
|
|
try {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 Загрузка черновика с ID:', claimId);
|
|
|
|
|
|
const url = `/api/v1/claims/drafts/${claimId}`;
|
|
|
|
|
|
console.log('🔍 URL запроса:', url);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
console.log('🔍 Статус ответа:', response.status, response.statusText);
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
if (!response.ok) {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
console.error('❌ Ошибка загрузки черновика:', response.status, errorText);
|
|
|
|
|
|
throw new Error(`Не удалось загрузить черновик: ${response.status} ${errorText}`);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 Данные черновика загружены:', data);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
const claim = data.claim;
|
|
|
|
|
|
const payload = claim.payload || {};
|
2025-11-20 18:31:42 +03:00
|
|
|
|
|
|
|
|
|
|
// ✅ Для telegram черновиков данные могут быть в payload.body
|
|
|
|
|
|
const body = payload.body || {};
|
|
|
|
|
|
const isTelegramFormat = !!payload.body;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🔍 Claim объект:', claim);
|
|
|
|
|
|
console.log('🔍 claim.claim_id:', claim.claim_id);
|
|
|
|
|
|
console.log('🔍 claim.id:', claim.id);
|
2025-11-24 15:16:46 +03:00
|
|
|
|
console.log('🔍 claim.unified_id:', claim.unified_id);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 Payload черновика:', payload);
|
|
|
|
|
|
console.log('🔍 payload.body:', body);
|
|
|
|
|
|
console.log('🔍 Формат:', isTelegramFormat ? 'telegram (body)' : 'web_form (прямой)');
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Извлекаем данные из body (telegram) или напрямую из payload (web_form)
|
|
|
|
|
|
const wizardPlanRaw = body.wizard_plan || payload.wizard_plan;
|
|
|
|
|
|
const answersRaw = body.answers || payload.answers;
|
2025-11-24 15:12:29 +03:00
|
|
|
|
// Ищем problem_description в разных местах (может быть в разных форматах)
|
|
|
|
|
|
const problemDescription =
|
|
|
|
|
|
body.problem_description ||
|
|
|
|
|
|
payload.problem_description ||
|
|
|
|
|
|
body.description ||
|
|
|
|
|
|
payload.description ||
|
|
|
|
|
|
payload.body?.problem_description || // Для вложенных структур
|
|
|
|
|
|
payload.body?.description ||
|
|
|
|
|
|
null;
|
2025-11-24 14:11:04 +03:00
|
|
|
|
const documentsMeta = body.documents_meta || payload.documents_meta || [];
|
2025-11-20 18:31:42 +03:00
|
|
|
|
|
|
|
|
|
|
// ✅ Парсим wizard_plan и answers, если они строки (JSON)
|
|
|
|
|
|
let wizardPlan = wizardPlanRaw;
|
|
|
|
|
|
if (typeof wizardPlanRaw === 'string') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
wizardPlan = JSON.parse(wizardPlanRaw);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('⚠️ Не удалось распарсить wizard_plan:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let answers = answersRaw;
|
|
|
|
|
|
if (typeof answersRaw === 'string') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
answers = JSON.parse(answersRaw);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('⚠️ Не удалось распарсить answers:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 14:11:04 +03:00
|
|
|
|
// ✅ Проверяем, заполнены ли все шаги
|
2025-11-24 15:12:29 +03:00
|
|
|
|
// Для problem_description: если его нет в payload, но есть wizard_plan и answers,
|
|
|
|
|
|
// значит описание уже было введено ранее (wizard_plan генерируется на основе описания)
|
|
|
|
|
|
const hasDescription = !!problemDescription || (!!wizardPlan && !!answers); // Если есть план и ответы - описание было
|
2025-11-24 14:11:04 +03:00
|
|
|
|
const hasWizardPlan = !!wizardPlan;
|
|
|
|
|
|
const hasAnswers = !!answers && Object.keys(answers).length > 0;
|
|
|
|
|
|
const hasDocuments = Array.isArray(documentsMeta) && documentsMeta.length > 0;
|
|
|
|
|
|
const isDraft = claim.status_code === 'draft';
|
|
|
|
|
|
|
|
|
|
|
|
const allStepsFilled = hasDescription && hasWizardPlan && hasAnswers && hasDocuments;
|
|
|
|
|
|
const isReadyForConfirmation = allStepsFilled && isDraft;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🔍 Проверка полноты черновика:', {
|
|
|
|
|
|
hasDescription,
|
|
|
|
|
|
hasWizardPlan,
|
|
|
|
|
|
hasAnswers,
|
|
|
|
|
|
hasDocuments,
|
|
|
|
|
|
isDraft,
|
|
|
|
|
|
allStepsFilled,
|
|
|
|
|
|
isReadyForConfirmation,
|
2025-11-24 15:12:29 +03:00
|
|
|
|
problemDescriptionFound: !!problemDescription,
|
|
|
|
|
|
inferredFromPlan: !problemDescription && !!wizardPlan && !!answers,
|
2025-11-24 14:11:04 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-24 15:12:29 +03:00
|
|
|
|
console.log('🔍 problem_description:', problemDescription ? 'есть' : (wizardPlan && answers ? 'выведено из наличия плана и ответов' : 'нет'));
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 wizard_plan:', wizardPlan ? 'есть' : 'нет');
|
|
|
|
|
|
console.log('🔍 answers:', answers ? 'есть' : 'нет');
|
2025-11-24 14:11:04 +03:00
|
|
|
|
console.log('🔍 documents_meta:', documentsMeta.length, 'документов');
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔍 Все ключи payload:', Object.keys(payload));
|
|
|
|
|
|
if (isTelegramFormat) {
|
|
|
|
|
|
console.log('🔍 Все ключи body:', Object.keys(body));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Извлекаем claim_id из разных возможных мест
|
|
|
|
|
|
const finalClaimId = claim.claim_id || payload.claim_id || body.claim_id || claim.id || formData.claim_id || claimId;
|
|
|
|
|
|
console.log('🔍 Извлечённый claim_id:', finalClaimId);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
// Восстанавливаем данные формы из черновика
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🔄 Загрузка черновика: session_id из черновика:', claim.session_token);
|
|
|
|
|
|
console.log('🔄 Загрузка черновика: текущий sessionIdRef.current:', sessionIdRef.current);
|
|
|
|
|
|
console.log('🔄 Загрузка черновика: текущий formData.session_id:', formData.session_id);
|
|
|
|
|
|
const actualSessionId = sessionIdRef.current || formData.session_id;
|
|
|
|
|
|
console.log('🔄 Загрузка черновика: ИСПОЛЬЗУЕМ session_id:', actualSessionId);
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
updateFormData({
|
2025-11-20 18:31:42 +03:00
|
|
|
|
claim_id: finalClaimId, // ✅ Используем извлечённый claim_id
|
|
|
|
|
|
session_id: actualSessionId, // ✅ Используем ТЕКУЩИЙ session_id, а не старый из черновика
|
|
|
|
|
|
phone: body.phone || payload.phone || formData.phone,
|
|
|
|
|
|
email: body.email || payload.email || formData.email,
|
|
|
|
|
|
problemDescription: problemDescription || formData.problemDescription,
|
|
|
|
|
|
wizardPlan: wizardPlan || formData.wizardPlan,
|
|
|
|
|
|
wizardPlanStatus: wizardPlan ? (answers ? 'answered' : 'ready') : 'pending', // ✅ Устанавливаем статус
|
|
|
|
|
|
wizardAnswers: answers || formData.wizardAnswers,
|
|
|
|
|
|
wizardPrefill: (body.answers_prefill || payload.answers_prefill) ?
|
|
|
|
|
|
(body.answers_prefill || payload.answers_prefill).reduce((acc: any, item: any) => {
|
2025-11-19 18:46:48 +03:00
|
|
|
|
acc[item.name] = item.value;
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {}) : formData.wizardPrefill,
|
2025-11-20 18:31:42 +03:00
|
|
|
|
wizardPrefillArray: body.answers_prefill || payload.answers_prefill || formData.wizardPrefillArray,
|
|
|
|
|
|
wizardCoverageReport: body.coverage_report || payload.coverage_report || formData.wizardCoverageReport,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
wizardUploads: {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
documents: (body.documents_meta || payload.documents_meta) ? {} : formData.wizardUploads?.documents,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
custom: formData.wizardUploads?.custom || [],
|
|
|
|
|
|
},
|
2025-11-20 18:31:42 +03:00
|
|
|
|
wizardSkippedDocuments: body.wizard_skipped_documents || payload.wizard_skipped_documents || formData.wizardSkippedDocuments,
|
|
|
|
|
|
eventType: body.event_type || payload.event_type || formData.eventType,
|
|
|
|
|
|
contact_id: body.contact_id || payload.contact_id || formData.contact_id,
|
|
|
|
|
|
project_id: body.project_id || payload.project_id || formData.project_id,
|
|
|
|
|
|
unified_id: formData.unified_id, // ✅ Сохраняем unified_id
|
2025-11-19 18:46:48 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
setSelectedDraftId(finalClaimId);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
setShowDraftSelection(false);
|
2025-10-29 12:36:30 +03:00
|
|
|
|
|
2025-11-24 14:11:04 +03:00
|
|
|
|
// ✅ Если все шаги заполнены и статус = draft → переходим к форме подтверждения
|
|
|
|
|
|
if (isReadyForConfirmation) {
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
console.log('✅ Все шаги заполнены, преобразуем данные для формы подтверждения');
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
|
|
|
|
|
setIsPhoneVerified(true);
|
|
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Преобразуем данные из БД в формат propertyName для формы подтверждения
|
|
|
|
|
|
const claimPlanData = transformDraftToClaimPlanFormat({
|
|
|
|
|
|
claim,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
body,
|
|
|
|
|
|
isTelegramFormat,
|
|
|
|
|
|
finalClaimId,
|
|
|
|
|
|
actualSessionId,
|
|
|
|
|
|
currentFormData: formData,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Сохраняем данные заявления в formData
|
|
|
|
|
|
updateFormData({
|
|
|
|
|
|
claimPlanData: claimPlanData,
|
|
|
|
|
|
showClaimConfirmation: true,
|
|
|
|
|
|
});
|
2025-11-24 14:11:04 +03:00
|
|
|
|
|
refactor: Load claim confirmation data from DB instead of SSE for drafts
Problem:
- When draft is fully filled, we subscribed to Redis SSE channel claim:plan
- But all data already exists in PostgreSQL database
- No need to wait for n8n to publish data - we can load it directly
Solution:
1. Removed subscribeToClaimPlanForDraft() function
- No longer subscribes to SSE channel for drafts
- Removed EventSource cleanup code
2. Added transformDraftToClaimPlanFormat() function
- Transforms draft data from DB format to propertyName format
- Extracts data from payload/body (telegram/web_form formats)
- Maps documents_meta to attachments array
- Formats applicant, case, contract_or_service, offenders, claim, meta
- Returns data in array format expected by confirmation form
3. Updated loadDraft() logic:
- When draft is ready for confirmation (all steps filled + draft status)
- Calls transformDraftToClaimPlanFormat() instead of subscribing to SSE
- Immediately shows confirmation form with data from DB
Flow:
1. User selects fully filled draft
2. System checks completeness (description, plan, answers, documents)
3. If ready → transforms DB data to propertyName format
4. Shows confirmation form immediately (no SSE wait)
Benefits:
- ✅ Faster: no waiting for n8n to publish data
- ✅ More reliable: data always available from DB
- ✅ Simpler: no SSE connection management for drafts
- ✅ Works offline: doesn't depend on Redis pub/sub
Files:
- frontend/src/pages/ClaimForm.tsx: Added transform function, removed SSE subscription
2025-11-24 15:08:00 +03:00
|
|
|
|
// Переход к шагу подтверждения произойдёт автоматически через useEffect
|
|
|
|
|
|
setCurrentStep(2); // StepWizardPlan (временно, useEffect переключит на подтверждение)
|
2025-11-24 14:11:04 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ✅ Определяем шаг для перехода на основе данных черновика
|
|
|
|
|
|
// Приоритет: если есть wizard_plan → переходим к визарду (даже если нет problem_description)
|
|
|
|
|
|
// После выбора черновика showDraftSelection = false, поэтому:
|
|
|
|
|
|
// - Шаг 0 = Step1Phone (но мы его пропускаем, т.к. телефон уже верифицирован)
|
|
|
|
|
|
// - Шаг 1 = StepDescription
|
|
|
|
|
|
// - Шаг 2 = StepWizardPlan
|
|
|
|
|
|
|
|
|
|
|
|
let targetStep = 1; // По умолчанию - описание (шаг 1)
|
|
|
|
|
|
|
|
|
|
|
|
if (wizardPlan) {
|
|
|
|
|
|
// ✅ Если есть wizard_plan - переходим к визарду (шаг 2)
|
|
|
|
|
|
// Пользователь уже описывал проблему, и есть план вопросов
|
|
|
|
|
|
targetStep = 2;
|
|
|
|
|
|
console.log('✅ Переходим к StepWizardPlan (шаг 2) - есть wizard_plan');
|
|
|
|
|
|
console.log('✅ answers в черновике:', answers ? 'есть (показываем заполненную форму)' : 'нет (показываем пустую форму)');
|
|
|
|
|
|
} else if (problemDescription) {
|
|
|
|
|
|
// Если есть описание, но нет плана - переходим к визарду (шаг 2), чтобы получить план
|
|
|
|
|
|
targetStep = 2;
|
|
|
|
|
|
console.log('✅ Переходим к StepWizardPlan (шаг 2) - есть описание, план будет получен через SSE');
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// Если нет ничего - переходим к описанию (шаг 1)
|
|
|
|
|
|
targetStep = 1;
|
|
|
|
|
|
console.log('✅ Переходим к StepDescription (шаг 1) - нет описания и плана');
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
|
|
|
|
|
|
console.log('🔍 Устанавливаем currentStep:', targetStep);
|
|
|
|
|
|
// ✅ Устанавливаем isPhoneVerified = true, чтобы пропустить шаг телефона
|
|
|
|
|
|
setIsPhoneVerified(true);
|
|
|
|
|
|
setCurrentStep(targetStep);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Ошибка загрузки черновика:', error);
|
|
|
|
|
|
message.error('Не удалось загрузить черновик');
|
|
|
|
|
|
}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
}, [formData, updateFormData]);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
// Обработчик выбора черновика
|
|
|
|
|
|
const handleSelectDraft = useCallback((claimId: string) => {
|
|
|
|
|
|
loadDraft(claimId);
|
|
|
|
|
|
}, [loadDraft]);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверка наличия черновиков
|
|
|
|
|
|
const checkDrafts = useCallback(async (unified_id?: string, phone?: string, sessionId?: string) => {
|
|
|
|
|
|
try {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 ========== checkDrafts вызван ==========');
|
|
|
|
|
|
console.log('🔍 Параметры:', { unified_id, phone, sessionId });
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
// Приоритет: unified_id > phone > session_id
|
|
|
|
|
|
if (unified_id) {
|
|
|
|
|
|
params.append('unified_id', unified_id);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Используем unified_id:', unified_id);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else if (phone) {
|
|
|
|
|
|
params.append('phone', phone);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Используем phone:', phone);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else if (sessionId) {
|
|
|
|
|
|
params.append('session_id', sessionId);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Используем session_id:', sessionId);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.warn('⚠️ Нет параметров для поиска черновиков');
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const url = `/api/v1/claims/drafts/list?${params.toString()}`;
|
|
|
|
|
|
console.log('🔍 Запрос черновиков:', url);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Статус ответа:', response.status, response.statusText);
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
if (!response.ok) {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
console.error('❌ Ошибка запроса черновиков:', response.status, response.statusText, errorText);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Полный ответ API черновиков:', JSON.stringify(data, null, 2));
|
|
|
|
|
|
console.log('🔍 Debug info от backend:', data.debug_info || data.debug);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
const count = data.count || 0;
|
|
|
|
|
|
console.log('🔍 Количество черновиков:', count);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Список черновиков:', data.drafts);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
setHasDrafts(count > 0);
|
|
|
|
|
|
setShowDraftSelection(count > 0);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Установлены флаги: hasDrafts=', count > 0, 'showDraftSelection=', count > 0);
|
|
|
|
|
|
console.log('🔍 ========== checkDrafts завершён ==========');
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return count > 0;
|
|
|
|
|
|
} catch (error) {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.error('❌ Ошибка проверки черновиков:', error);
|
|
|
|
|
|
console.error('❌ Stack trace:', (error as Error).stack);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// Обработчик создания новой заявки
|
|
|
|
|
|
const handleNewClaim = useCallback(() => {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🆕 Начинаем новое обращение');
|
|
|
|
|
|
console.log('🆕 Текущий currentStep:', currentStep);
|
|
|
|
|
|
console.log('🆕 isPhoneVerified:', isPhoneVerified);
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
setShowDraftSelection(false);
|
|
|
|
|
|
setSelectedDraftId(null);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
setHasDrafts(false); // ✅ Сбрасываем флаг наличия черновиков
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
// Очищаем данные формы, кроме телефона и session_id
|
|
|
|
|
|
updateFormData({
|
|
|
|
|
|
claim_id: undefined,
|
|
|
|
|
|
problemDescription: undefined,
|
|
|
|
|
|
wizardPlan: undefined,
|
|
|
|
|
|
wizardAnswers: undefined,
|
|
|
|
|
|
wizardPrefill: undefined,
|
|
|
|
|
|
wizardPrefillArray: undefined,
|
|
|
|
|
|
wizardCoverageReport: undefined,
|
|
|
|
|
|
wizardUploads: undefined,
|
|
|
|
|
|
wizardSkippedDocuments: undefined,
|
|
|
|
|
|
eventType: undefined,
|
|
|
|
|
|
});
|
2025-11-20 18:31:42 +03:00
|
|
|
|
|
|
|
|
|
|
console.log('🆕 Переходим к шагу описания проблемы (пропускаем Phone и DraftSelection)');
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Переходим к шагу описания проблемы
|
|
|
|
|
|
// После сброса флагов черновиков, steps будут:
|
|
|
|
|
|
// Шаг 0 - Phone (уже верифицирован, но в массиве есть)
|
|
|
|
|
|
// Шаг 1 - Description (сюда переходим)
|
|
|
|
|
|
// Шаг 2 - WizardPlan
|
|
|
|
|
|
setCurrentStep(1); // ✅ Переходим к описанию (индекс 1)
|
|
|
|
|
|
}, [updateFormData, currentStep, isPhoneVerified]);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
addDebugEvent('form', 'info', '📤 Отправка заявки в n8n через backend');
|
|
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
stage: 'final',
|
|
|
|
|
|
form_id: 'ticket_form',
|
2025-11-20 18:31:42 +03:00
|
|
|
|
session_id: formData.session_id ?? sessionIdRef.current,
|
2025-11-19 18:46:48 +03:00
|
|
|
|
client_ip: formData.clientIp,
|
|
|
|
|
|
sms_code: formData.smsCode,
|
|
|
|
|
|
|
|
|
|
|
|
// Базовые идентификаторы
|
|
|
|
|
|
claim_id: formData.claim_id,
|
|
|
|
|
|
contact_id: formData.contact_id,
|
|
|
|
|
|
project_id: formData.project_id,
|
|
|
|
|
|
ticket_id: formData.ticket_id,
|
|
|
|
|
|
is_new_contact: formData.is_new_contact,
|
|
|
|
|
|
is_new_project: formData.is_new_project,
|
|
|
|
|
|
|
|
|
|
|
|
// Основные поля формы (для удобства в n8n)
|
|
|
|
|
|
voucher: formData.voucher,
|
|
|
|
|
|
phone: formData.phone,
|
|
|
|
|
|
email: formData.email,
|
|
|
|
|
|
event_type: formData.eventType,
|
|
|
|
|
|
payment_method: formData.paymentMethod,
|
|
|
|
|
|
bank_name: formData.bankName,
|
|
|
|
|
|
card_number: formData.cardNumber,
|
|
|
|
|
|
account_number: formData.accountNumber,
|
|
|
|
|
|
|
|
|
|
|
|
// Старый блок документов + новые загрузки визарда (пока как есть)
|
|
|
|
|
|
documents: formData.documents || {},
|
|
|
|
|
|
wizard_uploads: formData.wizardUploads || {},
|
|
|
|
|
|
|
|
|
|
|
|
// Всё состояние формы целиком — на всякий случай
|
|
|
|
|
|
form: formData,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/v1/claims/create', {
|
2025-10-24 16:19:58 +03:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
2025-11-19 18:46:48 +03:00
|
|
|
|
body: JSON.stringify(payload),
|
2025-10-24 16:19:58 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
const text = await response.text();
|
|
|
|
|
|
let parsed: any = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsed = text ? JSON.parse(text) : null;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
parsed = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
message.error('Ошибка при создании заявки (n8n)');
|
|
|
|
|
|
addDebugEvent('form', 'error', '❌ Ошибка создания заявки в n8n', {
|
|
|
|
|
|
status: response.status,
|
|
|
|
|
|
body: text,
|
2025-10-24 16:19:58 +03:00
|
|
|
|
});
|
2025-11-19 18:46:48 +03:00
|
|
|
|
return;
|
2025-10-24 16:19:58 +03:00
|
|
|
|
}
|
2025-11-19 18:46:48 +03:00
|
|
|
|
|
|
|
|
|
|
addDebugEvent('form', 'success', '✅ Финальный webhook в n8n отработал', {
|
|
|
|
|
|
response: parsed ?? text,
|
|
|
|
|
|
});
|
|
|
|
|
|
// Помечаем, что заявка отправлена, и показываем заглушку.
|
|
|
|
|
|
setIsSubmitted(true);
|
|
|
|
|
|
message.success('Данные отправлены, заявка принята в обработку.');
|
2025-10-24 16:19:58 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('Ошибка соединения с сервером');
|
2025-11-19 18:46:48 +03:00
|
|
|
|
addDebugEvent('form', 'error', '❌ Ошибка соединения', { error: String(error) });
|
2025-10-24 16:19:58 +03:00
|
|
|
|
console.error(error);
|
|
|
|
|
|
}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
}, [formData, addDebugEvent]);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-29 12:36:30 +03:00
|
|
|
|
// Динамически генерируем шаги на основе выбранного eventType
|
|
|
|
|
|
const steps = useMemo(() => {
|
|
|
|
|
|
const stepsArray: any[] = [];
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// Шаг 0: Выбор черновика (показывается только если есть черновики)
|
|
|
|
|
|
// ✅ unified_id уже означает, что телефон верифицирован
|
|
|
|
|
|
// Показываем шаг, если showDraftSelection=true ИЛИ если есть unified_id и hasDrafts
|
|
|
|
|
|
if ((showDraftSelection || (formData.unified_id && hasDrafts)) && !selectedDraftId) {
|
2025-11-19 18:46:48 +03:00
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Черновики',
|
|
|
|
|
|
description: 'Выбор заявки',
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<StepDraftSelection
|
|
|
|
|
|
phone={formData.phone || ''}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
session_id={sessionIdRef.current}
|
|
|
|
|
|
unified_id={formData.unified_id} // ✅ Передаём unified_id
|
2025-11-19 18:46:48 +03:00
|
|
|
|
onSelectDraft={handleSelectDraft}
|
|
|
|
|
|
onNewClaim={handleNewClaim}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-01 13:31:05 +03:00
|
|
|
|
// Шаг 1: Phone (телефон + SMS верификация)
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Телефон',
|
|
|
|
|
|
description: 'Подтверждение по SMS',
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<Step1Phone
|
2025-11-20 18:31:42 +03:00
|
|
|
|
formData={{ ...formData, session_id: formData.session_id || sessionIdRef.current }} // ✅ Используем session_id из formData (от n8n) или временный
|
2025-11-19 18:46:48 +03:00
|
|
|
|
updateFormData={(data: any) => {
|
|
|
|
|
|
updateFormData(data);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ✅ Если n8n вернул session_id, обновляем ref
|
|
|
|
|
|
if (data.session_id && data.session_id !== sessionIdRef.current) {
|
|
|
|
|
|
console.log('🔄 Обновляем sessionIdRef на значение от n8n:', data.session_id);
|
|
|
|
|
|
sessionIdRef.current = data.session_id;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ❌ Убрано: проверка черновиков здесь избыточна, т.к. она уже есть в onNext
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}}
|
|
|
|
|
|
onNext={async (unified_id?: string) => {
|
|
|
|
|
|
console.log('🔥 onNext вызван с unified_id:', unified_id);
|
|
|
|
|
|
console.log('🔥 formData.unified_id:', formData.unified_id);
|
|
|
|
|
|
console.log('🔥 isPhoneVerified:', isPhoneVerified);
|
|
|
|
|
|
console.log('🔥 selectedDraftId:', selectedDraftId);
|
|
|
|
|
|
|
|
|
|
|
|
// После верификации проверяем черновики
|
|
|
|
|
|
// Используем unified_id из параметра (если передан) или из formData
|
|
|
|
|
|
const finalUnifiedId = unified_id || formData.unified_id;
|
|
|
|
|
|
console.log('🔥 finalUnifiedId:', finalUnifiedId);
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ✅ Если передан unified_id, значит телефон уже верифицирован (даже если isPhoneVerified ещё false)
|
|
|
|
|
|
// Проверяем черновики, если есть unified_id или телефон верифицирован
|
|
|
|
|
|
const shouldCheckDrafts = finalUnifiedId || (formData.phone && isPhoneVerified);
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldCheckDrafts && !selectedDraftId) {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Проверка черновиков с unified_id:', finalUnifiedId, 'phone:', formData.phone, 'sessionId:', sessionIdRef.current);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
const hasDraftsResult = await checkDrafts(finalUnifiedId, formData.phone, sessionIdRef.current);
|
2025-11-19 18:46:48 +03:00
|
|
|
|
console.log('🔍 Результат checkDrafts:', hasDraftsResult);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('🔍 Текущие флаги после checkDrafts: hasDrafts=', hasDrafts, 'showDraftSelection=', showDraftSelection);
|
|
|
|
|
|
|
2025-11-19 18:46:48 +03:00
|
|
|
|
if (hasDraftsResult) {
|
|
|
|
|
|
console.log('✅ Есть черновики, переходим к шагу 0');
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ✅ ВАЖНО: Сначала устанавливаем флаги, потом переходим на шаг 0
|
|
|
|
|
|
setShowDraftSelection(true);
|
|
|
|
|
|
setHasDrafts(true);
|
2025-11-24 13:36:14 +03:00
|
|
|
|
// ✅ Используем setTimeout для гарантии, что React обновил состояние
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
console.log('🔄 Переходим на шаг 0 после установки флагов');
|
|
|
|
|
|
setCurrentStep(0); // Переходим к шагу выбора черновика
|
|
|
|
|
|
}, 100);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('🛑 Остановка выполнения onNext - есть черновики');
|
|
|
|
|
|
return; // ✅ ВАЖНО: Не идём дальше, если есть черновики
|
2025-11-19 18:46:48 +03:00
|
|
|
|
} else {
|
2025-11-24 13:36:14 +03:00
|
|
|
|
console.log('❌ Нет черновиков, идем дальше к описанию проблемы');
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// Нет черновиков - идём дальше
|
|
|
|
|
|
nextStep();
|
|
|
|
|
|
return;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-11-20 18:31:42 +03:00
|
|
|
|
console.log('⚠️ Условие не выполнено для проверки черновиков:', {
|
|
|
|
|
|
shouldCheckDrafts,
|
|
|
|
|
|
selectedDraftId,
|
|
|
|
|
|
finalUnifiedId,
|
|
|
|
|
|
phone: formData.phone,
|
|
|
|
|
|
isPhoneVerified
|
|
|
|
|
|
});
|
|
|
|
|
|
// Условие не выполнено - идём дальше
|
2025-11-19 18:46:48 +03:00
|
|
|
|
nextStep();
|
2025-11-20 18:31:42 +03:00
|
|
|
|
return;
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
|
|
|
|
|
|
// ❌ ЭТОТ КОД НЕ ДОЛЖЕН ВЫПОЛНЯТЬСЯ, если есть return выше
|
|
|
|
|
|
console.error('❌❌❌ КРИТИЧЕСКАЯ ОШИБКА: nextStep() вызван после return!');
|
|
|
|
|
|
nextStep();
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}}
|
2025-11-01 13:31:05 +03:00
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
isPhoneVerified={isPhoneVerified}
|
2025-11-20 18:31:42 +03:00
|
|
|
|
setIsPhoneVerified={(verified: boolean) => {
|
2025-11-19 18:46:48 +03:00
|
|
|
|
setIsPhoneVerified(verified);
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// ❌ Убрано: проверка черновиков делается только в onNext
|
|
|
|
|
|
// onNext вызывается после успешной верификации и содержит unified_id
|
2025-11-19 18:46:48 +03:00
|
|
|
|
}}
|
2025-11-01 13:31:05 +03:00
|
|
|
|
addDebugEvent={addDebugEvent}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-14 19:06:36 +03:00
|
|
|
|
// Шаг 2: свободное описание
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Описание',
|
|
|
|
|
|
description: 'Что случилось?',
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<StepDescription
|
|
|
|
|
|
formData={formData}
|
|
|
|
|
|
updateFormData={updateFormData}
|
|
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
onNext={nextStep}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-15 18:48:15 +03:00
|
|
|
|
// Шаг 3: AI Рекомендации
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Рекомендации',
|
|
|
|
|
|
description: 'AI ассистент',
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<StepWizardPlan
|
|
|
|
|
|
formData={formData}
|
|
|
|
|
|
updateFormData={updateFormData}
|
|
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
onNext={nextStep}
|
|
|
|
|
|
addDebugEvent={addDebugEvent}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-24 13:36:14 +03:00
|
|
|
|
// Шаг подтверждения заявления (показывается после получения данных из claim:plan)
|
|
|
|
|
|
if (formData.showClaimConfirmation && formData.claimPlanData) {
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Подтверждение',
|
|
|
|
|
|
description: 'Проверка данных',
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<StepClaimConfirmation
|
|
|
|
|
|
claimPlanData={formData.claimPlanData}
|
|
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
onNext={nextStep}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-14 19:06:36 +03:00
|
|
|
|
// Шаг 3: Policy (всегда)
|
2025-10-29 12:36:30 +03:00
|
|
|
|
stepsArray.push({
|
2025-10-24 20:40:44 +03:00
|
|
|
|
title: 'Проверка полиса',
|
2025-10-29 12:36:30 +03:00
|
|
|
|
description: 'Полис ERV',
|
2025-10-24 16:19:58 +03:00
|
|
|
|
content: (
|
2025-10-27 08:33:16 +03:00
|
|
|
|
<Step1Policy
|
2025-11-20 18:31:42 +03:00
|
|
|
|
formData={{ ...formData, session_id: sessionIdRef.current }} // ✅ claim_id уже в formData от n8n
|
2025-10-27 08:33:16 +03:00
|
|
|
|
updateFormData={updateFormData}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
onNext={nextStep}
|
2025-10-24 22:13:52 +03:00
|
|
|
|
addDebugEvent={addDebugEvent}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
/>
|
|
|
|
|
|
),
|
2025-10-29 12:36:30 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-14 19:06:36 +03:00
|
|
|
|
// Шаг 4: Event Type Selection (всегда)
|
2025-10-29 12:36:30 +03:00
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Тип события',
|
|
|
|
|
|
description: 'Выбор случая',
|
2025-10-24 16:19:58 +03:00
|
|
|
|
content: (
|
2025-10-29 12:36:30 +03:00
|
|
|
|
<Step2EventType
|
|
|
|
|
|
formData={formData}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
updateFormData={updateFormData}
|
|
|
|
|
|
onNext={nextStep}
|
|
|
|
|
|
onPrev={prevStep}
|
2025-11-02 00:09:35 +03:00
|
|
|
|
addDebugEvent={addDebugEvent}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
/>
|
|
|
|
|
|
),
|
2025-10-29 12:36:30 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-30 19:22:14 +03:00
|
|
|
|
// Шаги 3+: Document Upload (динамически, если выбран eventType)
|
2025-10-29 12:36:30 +03:00
|
|
|
|
if (formData.eventType && documentConfigs.length > 0) {
|
|
|
|
|
|
documentConfigs.forEach((docConfig, index) => {
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: `Документ ${index + 1}`,
|
|
|
|
|
|
description: docConfig.name,
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<StepDocumentUpload
|
|
|
|
|
|
key={`doc-${docConfig.file_type}`}
|
|
|
|
|
|
documentConfig={docConfig}
|
|
|
|
|
|
formData={formData}
|
|
|
|
|
|
updateFormData={updateFormData}
|
|
|
|
|
|
onNext={nextStep}
|
|
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
isLastDocument={index === documentConfigs.length - 1}
|
|
|
|
|
|
currentDocNumber={index + 1}
|
|
|
|
|
|
totalDocs={documentConfigs.length}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Последний шаг: Payment (всегда)
|
|
|
|
|
|
stepsArray.push({
|
|
|
|
|
|
title: 'Оплата',
|
|
|
|
|
|
description: 'Контакты и выплата',
|
2025-10-24 16:19:58 +03:00
|
|
|
|
content: (
|
|
|
|
|
|
<Step3Payment
|
2025-11-01 16:53:10 +03:00
|
|
|
|
formData={formData} // ✅ claim_id уже в formData
|
2025-10-24 16:19:58 +03:00
|
|
|
|
updateFormData={updateFormData}
|
|
|
|
|
|
onPrev={prevStep}
|
|
|
|
|
|
onSubmit={handleSubmit}
|
2025-10-24 20:40:44 +03:00
|
|
|
|
isPhoneVerified={isPhoneVerified}
|
|
|
|
|
|
setIsPhoneVerified={setIsPhoneVerified}
|
2025-10-24 22:13:52 +03:00
|
|
|
|
addDebugEvent={addDebugEvent}
|
2025-10-24 16:19:58 +03:00
|
|
|
|
/>
|
|
|
|
|
|
),
|
2025-10-29 12:36:30 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return stepsArray;
|
2025-11-20 18:31:42 +03:00
|
|
|
|
}, [formData, documentConfigs, isPhoneVerified, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
|
2025-10-24 16:19:58 +03:00
|
|
|
|
|
2025-10-24 21:34:50 +03:00
|
|
|
|
const handleReset = () => {
|
2025-11-19 18:46:48 +03:00
|
|
|
|
setIsSubmitted(false);
|
2025-10-24 21:34:50 +03:00
|
|
|
|
setFormData({
|
|
|
|
|
|
voucher: '',
|
2025-11-01 16:53:10 +03:00
|
|
|
|
claim_id: undefined, // ✅ Очищаем для новой заявки
|
2025-11-20 18:31:42 +03:00
|
|
|
|
session_id: sessionIdRef.current,
|
2025-10-24 21:34:50 +03:00
|
|
|
|
paymentMethod: 'sbp',
|
|
|
|
|
|
});
|
|
|
|
|
|
setCurrentStep(0);
|
|
|
|
|
|
setIsPhoneVerified(false);
|
|
|
|
|
|
message.info('Форма сброшена');
|
2025-10-29 12:36:30 +03:00
|
|
|
|
addDebugEvent('system', 'info', '🔄 Форма сброшена');
|
2025-10-24 21:34:50 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-20 18:31:42 +03:00
|
|
|
|
// Обработчик кнопки "Выход" - завершить сессию и вернуться к Step1Phone
|
|
|
|
|
|
const handleExitToList = useCallback(async () => {
|
|
|
|
|
|
console.log('🚪 Выход из системы');
|
|
|
|
|
|
addDebugEvent('system', 'info', '🚪 Выход из системы');
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем session_token из localStorage
|
|
|
|
|
|
const sessionToken = localStorage.getItem('session_token') || formData.session_id;
|
|
|
|
|
|
|
|
|
|
|
|
if (sessionToken) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Вызываем API logout для удаления сессии из Redis
|
|
|
|
|
|
const response = await fetch('/api/v1/session/logout', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ session_token: sessionToken })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
console.log('✅ Сессия удалена из Redis');
|
|
|
|
|
|
addDebugEvent('session', 'success', '✅ Сессия завершена');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('⚠️ Ошибка при завершении сессии:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Удаляем session_token из localStorage
|
|
|
|
|
|
localStorage.removeItem('session_token');
|
|
|
|
|
|
|
|
|
|
|
|
// Сбрасываем форму
|
|
|
|
|
|
handleReset();
|
|
|
|
|
|
|
|
|
|
|
|
message.info('Сессия завершена. До свидания!');
|
|
|
|
|
|
}, [formData.session_id, addDebugEvent]);
|
|
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
return (
|
2025-11-19 18:46:48 +03:00
|
|
|
|
<div className="claim-form-container" style={{ padding: '20px', background: '#ffffff' }}>
|
2025-10-24 22:13:52 +03:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
{/* Левая часть - Форма */}
|
|
|
|
|
|
<Col xs={24} lg={14}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
title="Подать заявку на выплату"
|
|
|
|
|
|
className="claim-form-card"
|
|
|
|
|
|
extra={
|
2025-11-20 18:31:42 +03:00
|
|
|
|
!isSubmitted && (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
{/* Кнопка "Выход" - показываем если телефон верифицирован */}
|
|
|
|
|
|
{isPhoneVerified && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleExitToList}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '4px 12px',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
border: '1px solid #ff4d4f',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
color: '#ff4d4f'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
🚪 Выход
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* Кнопка "Начать заново" - показываем только после шага телефона */}
|
|
|
|
|
|
{currentStep > 0 && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '4px 12px',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
border: '1px solid #d9d9d9',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
fontSize: '14px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
🔄 Начать заново
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
2025-10-24 22:13:52 +03:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
{isSubmitted ? (
|
|
|
|
|
|
<div style={{ padding: '40px 0', textAlign: 'center' }}>
|
|
|
|
|
|
<h3 style={{ fontSize: 22, marginBottom: 8 }}>Мы изучаем ваш вопрос и документы</h3>
|
|
|
|
|
|
<p style={{ color: '#666666', maxWidth: 480, margin: '0 auto 24px' }}>
|
|
|
|
|
|
Заявка отправлена в работу. Юристы проверят информацию и свяжутся с вами по указанным контактам.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Steps current={currentStep} className="steps">
|
|
|
|
|
|
{steps.map((item, index) => (
|
|
|
|
|
|
<Step
|
|
|
|
|
|
key={`step-${index}`}
|
|
|
|
|
|
title={item.title}
|
|
|
|
|
|
description={item.description}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Steps>
|
2025-11-20 18:31:42 +03:00
|
|
|
|
<div className="steps-content">
|
|
|
|
|
|
{steps[currentStep] ? steps[currentStep].content : (
|
|
|
|
|
|
<div style={{ padding: '40px 0', textAlign: 'center' }}>
|
|
|
|
|
|
<p>Загрузка шага...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-11-19 18:46:48 +03:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-10-24 22:13:52 +03:00
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Правая часть - Debug консоль */}
|
|
|
|
|
|
<Col xs={24} lg={10}>
|
|
|
|
|
|
<DebugPanel events={debugEvents} formData={formData} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2025-10-24 16:19:58 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|