import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { Steps, Card, message, Row, Col, Space, Spin } from 'antd'; import Step1Phone from '../components/form/Step1Phone'; import StepDescription from '../components/form/StepDescription'; // Step1Policy убран - старый ERV флоу import StepDraftSelection from '../components/form/StepDraftSelection'; import StepWizardPlan from '../components/form/StepWizardPlan'; import StepClaimConfirmation from '../components/form/StepClaimConfirmation'; // Step2EventType, StepDocumentUpload убраны - старый ERV флоу // Step3Payment убран - не используется import DebugPanel from '../components/DebugPanel'; // getDocumentsForEventType убран - старый ERV флоу import './ClaimForm.css'; // Используем относительные пути - Vite proxy перенаправит на backend const { Step } = Steps; /** * Генерация UUID v4 * Формат: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx */ function generateUUIDv4(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } interface FormData { // Шаг 1: Phone phone?: string; contact_id?: string; unified_id?: string; // ✅ Unified ID пользователя из PostgreSQL is_new_contact?: boolean; smsCode?: string; clientIp?: string; smsDebugCode?: string; // Шаг 2: Policy voucher: string; claim_id?: string; session_id?: string; project_id?: string; // ✅ ID проекта в vTiger (полис) is_new_project?: boolean; // ✅ Флаг: создан новый проект problemDescription?: string; wizardPlan?: any; wizardPlanStatus?: 'pending' | 'ready' | 'answered'; wizardAnswers?: Record; wizardPrefill?: Record; wizardPrefillArray?: Array<{ name: string; value: any }>; wizardCoverageReport?: any; wizardUploads?: Record; wizardSkippedDocuments?: string[]; // Подтверждение заявления (после получения данных из claim:plan) showClaimConfirmation?: boolean; claimPlanData?: any; // Данные заявления от n8n из канала claim:plan // Шаг 3: Event Type eventType?: string; ticket_id?: string; // ✅ ID заявки в vTiger (HelpDesk) ticket_number?: string; // ✅ Номер заявки (HD001234) // Шаги 4+: Documents documents?: Record; // Последний шаг: Payment fullName?: string; email?: string; paymentMethod?: string; bankId?: string; // ID банка из NSPK API bankName?: string; // Название банка для отображения cardNumber?: string; accountNumber?: string; } export default function ClaimForm() { // ✅ claim_id будет создан n8n в Step1Phone после SMS верификации // Не генерируем его локально! // session_id будет получен от n8n при создании контакта // Используем useRef чтобы sessionId не вызывал перерендер и был стабильным const sessionIdRef = useRef(`sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); const claimPlanEventSourceRef = useRef(null); const claimPlanTimeoutRef = useRef(null); const [currentStep, setCurrentStep] = useState(0); const [sessionRestored, setSessionRestored] = useState(false); // Флаг: пытались восстановить сессию const [formData, setFormData] = useState({ voucher: '', claim_id: undefined, // ✅ Будет заполнен n8n в Step1Phone session_id: sessionIdRef.current, paymentMethod: 'sbp', }); const [isPhoneVerified, setIsPhoneVerified] = useState(false); const [debugEvents, setDebugEvents] = useState([]); const [isSubmitted, setIsSubmitted] = useState(false); const [showDraftSelection, setShowDraftSelection] = useState(false); const [selectedDraftId, setSelectedDraftId] = useState(null); const [hasDrafts, setHasDrafts] = useState(false); const [telegramAuthChecked, setTelegramAuthChecked] = useState(false); /** Статус Telegram auth — показываем на странице, т.к. консоль Mini App отдельная */ const [tgDebug, setTgDebug] = useState(''); /** Дефолт = веб. Скин TG подставляется только при заходе через Telegram Mini App. */ const [isTelegramMiniApp, setIsTelegramMiniApp] = useState(false); /** Заход через MAX Mini App. */ const [isMaxMiniApp, setIsMaxMiniApp] = useState(false); useEffect(() => { // 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился! console.log('🔥 ClaimForm v3.9 - 2025-12-29 - Auto redirect to drafts after success'); }, []); // Определение: зашли с веба или из Telegram Mini App. Дефолт — веб; при TG вешаем класс для отдельного скина. // Загружаем telegram-web-app.js только если есть признаки Telegram (чтобы не мусорить в консоли). useEffect(() => { const isTelegramContext = () => { // Проверяем URL, referrer и user agent на признаки Telegram const url = window.location.href; const ref = document.referrer; const ua = navigator.userAgent; return ( url.includes('tgWebAppData') || url.includes('tgWebAppVersion') || ref.includes('telegram') || ua.includes('Telegram') ); }; if (isTelegramContext()) { // Загружаем скрипт Telegram SDK динамически const script = document.createElement('script'); script.src = 'https://telegram.org/js/telegram-web-app.js'; script.async = true; script.onload = () => { setTimeout(() => { const tg = (window as any).Telegram; const webApp = tg?.WebApp; const hasInitData = webApp?.initData && webApp.initData.length > 0; if (webApp && hasInitData) { setIsTelegramMiniApp(true); try { webApp.ready?.(); webApp.expand?.(); } catch (_) {} } }, 100); }; document.head.appendChild(script); } }, []); // ✅ Telegram Mini App: попытка авторизоваться через initData при первом заходе useEffect(() => { const tryTelegramAuth = async () => { try { // Только window: parent недоступен из-за cross-origin (iframe Telegram) const getTg = () => (window as any).Telegram; // Ждём появления initData: скрипт Telegram может подгрузиться с задержкой const maxWaitMs = 2500; const intervalMs = 150; let webApp: TelegramWebApp | null = null; let attempts = 0; while (attempts * intervalMs < maxWaitMs) { const tg = getTg(); webApp = tg?.WebApp ?? null; if (webApp?.initData) { console.log('[TG] initData появился через', attempts * intervalMs, 'ms, длина=', webApp.initData.length); break; } attempts++; await new Promise((r) => setTimeout(r, intervalMs)); } if (!webApp?.initData) { const tg = getTg(); console.log('[TG] После ожидания', maxWaitMs, 'ms: Telegram=', !!tg, 'WebApp=', !!tg?.WebApp, 'initData=', !!tg?.WebApp?.initData, '→ пропускаем tg/auth'); // Если Telegram не найден — пробуем MAX Mini App (window.WebApp от MAX Bridge) let maxWebApp = (window as any).WebApp; const maxWait = 4000; for (let t = 0; t < maxWait; t += 200) { await new Promise((r) => setTimeout(r, 200)); maxWebApp = (window as any).WebApp; if (maxWebApp?.initData && maxWebApp.initData.length > 0) break; } if (maxWebApp?.initData && typeof maxWebApp.initData === 'string' && maxWebApp.initData.length > 0) { const hasHash = maxWebApp.initData.includes('hash='); console.log('[MAX] Обнаружен MAX WebApp, initData длина=', maxWebApp.initData.length, ', есть hash=', hasHash); setIsMaxMiniApp(true); try { maxWebApp.ready?.(); } catch (_) {} const existingToken = localStorage.getItem('session_token'); if (existingToken) { console.log('[MAX] session_token уже есть → max/auth не вызываем'); setTelegramAuthChecked(true); return; } setTgDebug('MAX: POST /api/v1/max/auth...'); try { const maxRes = await fetch('/api/v1/max/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ init_data: maxWebApp.initData }), }); const maxData = await maxRes.json(); if (maxData.need_contact) { setTgDebug('MAX: Нужен контакт — закрываем приложение'); try { maxWebApp.close?.(); } catch (_) {} setTelegramAuthChecked(true); return; } if (maxRes.ok && maxData.success) { if (maxData.session_token) { localStorage.setItem('session_token', maxData.session_token); sessionIdRef.current = maxData.session_token; } setFormData((prev) => ({ ...prev, unified_id: maxData.unified_id, phone: maxData.phone, contact_id: maxData.contact_id, session_id: maxData.session_token, })); setIsPhoneVerified(true); if (maxData.has_drafts) { setShowDraftSelection(true); setHasDrafts(true); setCurrentStep(0); } else { setCurrentStep(1); } } else { console.error('[MAX] max/auth ответ', maxRes.status, maxData); } } catch (e) { console.error('[MAX] Ошибка max/auth:', e); } setTelegramAuthChecked(true); return; } setTelegramAuthChecked(true); return; } // Логирование для отладки if (webApp.initDataUnsafe?.user) { const u = webApp.initDataUnsafe.user; console.log('[TG] initDataUnsafe.user:', { id: u.id, username: u.username, first_name: u.first_name }); } // Если сессия уже есть в localStorage — ничего не делаем, дальше сработает обычное restoreSession const existingToken = localStorage.getItem('session_token'); if (existingToken) { setTgDebug('TG: session_token уже есть → tg/auth не вызываем'); console.log('[TG] session_token уже в localStorage → tg/auth не вызываем'); setTelegramAuthChecked(true); return; } setTgDebug('TG: POST /api/v1/tg/auth...'); console.log('[TG] Вызываем POST /api/v1/tg/auth, initData длина=', webApp.initData.length); const response = await fetch('/api/v1/tg/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ init_data: webApp.initData, }), }); const data = await response.json(); console.log('[TG] /api/v1/tg/auth ответ: status=', response.status, 'ok=', response.ok, 'data=', data); if (!response.ok || !data.success) { console.warn('[TG] Telegram auth не успешен → показываем экран телефона/SMS. detail=', data.detail || data); setTelegramAuthChecked(true); return; } const sessionToken = data.session_token; // Сохраняем session_token так же, как после SMS-логина if (sessionToken) { localStorage.setItem('session_token', sessionToken); sessionIdRef.current = sessionToken; } // Сохраняем базовые данные пользователя (phone может быть пустым) setFormData((prev) => ({ ...prev, unified_id: data.unified_id, phone: data.phone, contact_id: data.contact_id, session_id: sessionToken, })); // Помечаем телефон как уже "подтверждённый" для Telegram-флоу setIsPhoneVerified(true); // Если n8n сразу сообщил о наличии черновиков — показываем экран выбора if (data.has_drafts) { console.log('🤖 Telegram auth: has_drafts=true, переходим на экран черновиков'); setShowDraftSelection(true); setHasDrafts(true); setCurrentStep(0); } else { // Иначе переходим сразу к описанию проблемы console.log('🤖 Telegram auth: черновиков нет, переходим к описанию проблемы'); setCurrentStep(1); } } catch (error) { const msg = error instanceof Error ? error.message : String(error); setTgDebug(`TG: ошибка: ${msg}`); console.error('[TG] Ошибка при tg/auth (сеть или парсинг):', error); } finally { setTelegramAuthChecked(true); } }; tryTelegramAuth(); }, []); // ✅ Восстановление сессии при загрузке страницы (после попытки Telegram auth) useEffect(() => { if (!telegramAuthChecked) { // Ждём, пока не закончим попытку Telegram-авторизации, // чтобы не гонять два параллельных restoreSession. return; } 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('Добро пожаловать!'); addDebugEvent('session', 'success', '✅ Сессия восстановлена, найдены черновики'); } else { // Нет черновиков - переходим к описанию setCurrentStep(1); message.success('Добро пожаловать!'); 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(); }, [telegramAuthChecked]); // Запускаем только один раз, после попытки Telegram auth // Получаем 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(); }, []); // Автоматический переход к шагу подтверждения, когда данные готовы 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]); // Старый ERV флоу убран - documentConfigs больше не нужен const addDebugEvent = (type: string, status: string, message: string, data?: any) => { const event = { timestamp: new Date().toLocaleTimeString('ru-RU'), type, status, message, data: { ...data, claim_id: formData.claim_id // ✅ Используем claim_id из formData (от n8n) } }; setDebugEvents(prev => [event, ...prev]); }; // ✅ claim_id будет залогирован в Step1Phone после получения от n8n const updateFormData = useCallback((data: Partial) => { setFormData((prev) => ({ ...prev, ...data })); }, []); 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); // ✅ Если возвращаемся к шагу 0 и есть черновики - показываем список if (prev - 1 === 0 && formData.unified_id && hasDrafts) { console.log('📍 Возврат к списку черновиков'); setShowDraftSelection(true); setSelectedDraftId(null); } return prev - 1; }); }, [formData.unified_id, hasDrafts]); // ✅ Возврат к списку черновиков напрямую (без промежуточных шагов) const backToDraftsList = useCallback(() => { console.log('📋 Возврат к списку черновиков'); setShowDraftSelection(true); setSelectedDraftId(null); setCurrentStep(0); }, []); // Преобразование данных черновика в формат 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; console.log('🔄 transformDraftToClaimPlanFormat: входные данные:', { claimId: finalClaimId, claimUnifiedId: claim.unified_id, formDataUnifiedId: currentFormData.unified_id, claimKeys: Object.keys(claim), }); 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), hasSendToFormApprove: !!(payload.send_to_form_approve && payload.send_to_form_approve.draft), payloadKeys: Object.keys(payload), bodyKeys: Object.keys(body), }); // ✅ ПРИОРИТЕТ 1: Если есть данные в payload.send_to_form_approve.draft - используем их напрямую! const sendToFormApproveDraft = payload.send_to_form_approve?.draft; if (sendToFormApproveDraft) { console.log('✅ Найдены данные в payload.send_to_form_approve.draft, используем их напрямую!'); console.log('✅ send_to_form_approve.draft:', sendToFormApproveDraft); // Используем данные из send_to_form_approve.draft напрямую const draftData = sendToFormApproveDraft; // Формируем propertyName из draft данных const propertyName = { applicant: draftData.applicant || {}, case: draftData.case || {}, contract_or_service: draftData.contract_or_service || {}, offenders: draftData.offenders || [], claim: draftData.claim || {}, meta: { ...(draftData.meta || {}), claim_id: finalClaimId, unified_id: draftData.meta?.unified_id || claim.unified_id || currentFormData.unified_id || null, }, attachments: draftData.attachments || [], attachments_count: draftData.attachments_count || 0, attachments_names: draftData.attachments_names || [], }; // Возвращаем данные в формате объекта (для компонента StepClaimConfirmation) const result = { propertyName: propertyName, session_token: actualSessionId, prefix: '', telegram_id: null, claim_id: finalClaimId, unified_id: propertyName.meta.unified_id, user_id: propertyName.meta.user_id || null, }; console.log('🔄 transformDraftToClaimPlanFormat: результат из send_to_form_approve:', { claim_id: result.claim_id, unified_id: result.unified_id, hasPropertyName: !!result.propertyName, hasMeta: !!result.propertyName?.meta, }); return result; } // ✅ ПРИОРИТЕТ 2: Если данных нет в send_to_form_approve, извлекаем из body/payload // Извлекаем данные из 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 || []; // Извлекаем ответы на вопросы из wizard_answers const wizardAnswers = body.answers || payload.answers || body.wizard_answers || payload.wizard_answers || {}; let answersParsed = wizardAnswers; if (typeof wizardAnswers === 'string') { try { answersParsed = JSON.parse(wizardAnswers); } catch (e) { console.warn('⚠️ Не удалось распарсить answers:', e); answersParsed = {}; } } 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 формат'); console.log('🔄 wizard_answers keys:', Object.keys(answersParsed)); // Маппинг полей из wizard_answers в contract_or_service if (answersParsed.item && !contractData.subject) { contractData.subject = answersParsed.item; } if (answersParsed.price && !contractData.amount_paid) { // Нормализуем цену (убираем "рублей", пробелы и т.д.) const priceStr = String(answersParsed.price).replace(/\s+/g, '').replace(/руб(лей|ль|\.)?/gi, '').replace(/₽|р\.|р$/gi, ''); contractData.amount_paid = priceStr; contractData.amount_paid_fmt = priceStr; } if (answersParsed.place_date && !contractData.agreement_date) { contractData.agreement_date = answersParsed.place_date; contractData.agreement_date_fmt = answersParsed.place_date; } if (answersParsed.cancel_date && !contractData.period_start) { contractData.period_start = answersParsed.cancel_date; contractData.period_start_fmt = answersParsed.cancel_date; } // Маппинг полей из wizard_answers в claim if (answersParsed.steps_taken && !claimData.description) { claimData.description = answersParsed.steps_taken; } if (answersParsed.expectation && !claimData.reason) { // expectation может быть "refund", "replacement", "compensation", "other" claimData.reason = answersParsed.expectation === 'refund' ? 'consumer' : 'consumer'; } // Маппинг в case if (!caseData.category) { caseData.category = 'consumer'; // По умолчанию consumer } if (!caseData.direction) { caseData.direction = 'web_form'; } // Если есть problem_description, используем его для claim.description const problemDesc = payload.problem_description || body.problem_description; if (problemDesc && !claimData.description) { claimData.description = problemDesc; } if (problemDesc && !contractData.subject) { contractData.subject = problemDesc; } } // Данные заявителя берутся из других источников (phone, email из claim или formData) // ФИО, дата рождения, ИНН будут заполняться в форме подтверждения const applicantPhone = claim.phone || payload.phone || body.phone || currentFormData.phone || null; const applicantEmail = claim.email || payload.email || body.email || currentFormData.email || null; // Если есть данные заявителя в applicantData, используем их if (!applicantData.phone && applicantPhone) { applicantData.phone = applicantPhone; } if (!applicantData.email && applicantEmail) { applicantData.email = applicantEmail; } // Формируем attachments_names из documents_meta const attachmentsNames = documentsMeta.map((doc: any) => { return doc.original_file_name || doc.file_name || doc.field_name || 'Документ'; }); // Формируем attachments с полной информацией const attachments = documentsMeta.map((doc: any) => ({ label: doc.field_label || doc.original_file_name || doc.file_name || doc.field_name || 'Документ', // ✅ Используем field_label field_label: doc.field_label || doc.field_name || doc.original_file_name || doc.file_name || 'Документ', // ✅ Добавляем field_label отдельно 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(), })); // Формируем 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, }; // Возвращаем данные в формате объекта (для компонента StepClaimConfirmation) const result = { 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, }; console.log('🔄 transformDraftToClaimPlanFormat: результат:', { claim_id: result.claim_id, unified_id: result.unified_id, hasPropertyName: !!result.propertyName, hasMeta: !!result.propertyName?.meta, }); return result; }, []); // Загрузка черновика const loadDraft = useCallback(async (claimId: string) => { try { 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); if (!response.ok) { const errorText = await response.text(); console.error('❌ Ошибка загрузки черновика:', response.status, errorText); throw new Error(`Не удалось загрузить черновик: ${response.status} ${errorText}`); } const data = await response.json(); console.log('🔍 Данные черновика загружены:', data); const claim = data.claim; const payload = claim.payload || {}; // ✅ Сохраняем флаги подтверждения данных контакта const contact_data_confirmed = data.contact_data_confirmed || false; const contact_data_can_edit = data.contact_data_can_edit !== false; // По умолчанию true const contact_data_confirmed_at = data.contact_data_confirmed_at || null; const contact_data_from_crm = data.contact_data_from_crm || null; console.log('🔒 Статус данных контакта:', { contact_data_confirmed, contact_data_can_edit, contact_data_confirmed_at, has_crm_data: !!contact_data_from_crm }); // ✅ Для 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); console.log('🔍 claim.unified_id:', claim.unified_id); 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; // Ищем problem_description в разных местах (может быть в разных форматах) const problemDescription = body.problem_description || payload.problem_description || body.description || payload.description || payload.body?.problem_description || // Для вложенных структур payload.body?.description || null; const documentsMeta = body.documents_meta || payload.documents_meta || []; // ✅ Парсим 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); } } // ✅ Проверяем, заполнены ли все шаги // Для problem_description: если его нет в payload, но есть wizard_plan и answers, // значит описание уже было введено ранее (wizard_plan генерируется на основе описания) const hasDescription = !!problemDescription || (!!wizardPlan && !!answers); // Если есть план и ответы - описание было 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'; // ✅ Запрещаем редактирование заявок "В работе" if (claim.status_code === 'in_work') { message.warning('Эта заявка уже в работе и не может быть изменена'); console.log('⚠️ Попытка открыть заявку "В работе" для редактирования - запрещено'); return; } // ✅ НОВОЕ: Проверяем наличие form_draft (собранные данные из RAG) const formDraft = payload.form_draft; const hasFormDraft = !!(formDraft && formDraft.user && formDraft.offenders); const isDraftDocsComplete = claim.status_code === 'draft_docs_complete'; const allStepsFilled = hasDescription && hasWizardPlan && hasAnswers && hasDocuments; const isReadyForConfirmation = (allStepsFilled && isDraft) || (hasFormDraft && isDraftDocsComplete); console.log('🔍 Проверка полноты черновика:', { hasDescription, hasWizardPlan, hasAnswers, hasDocuments, isDraft, hasFormDraft, isDraftDocsComplete, allStepsFilled, isReadyForConfirmation, problemDescriptionFound: !!problemDescription, inferredFromPlan: !problemDescription && !!wizardPlan && !!answers, }); console.log('🔍 problem_description:', problemDescription ? 'есть' : (wizardPlan && answers ? 'выведено из наличия плана и ответов' : 'нет')); console.log('🔍 wizard_plan:', wizardPlan ? 'есть' : 'нет'); console.log('🔍 answers:', answers ? 'есть' : 'нет'); console.log('🔍 documents_meta:', documentsMeta.length, 'документов'); 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); // Восстанавливаем данные формы из черновика console.log('🔄 Загрузка черновика: session_id из черновика:', claim.session_token); console.log('🔄 Загрузка черновика: текущий sessionIdRef.current:', sessionIdRef.current); console.log('🔄 Загрузка черновика: текущий formData.session_id:', formData.session_id); // ✅ При загрузке черновика используем session_id из черновика (для продолжения работы с той же жалобой) // Если session_id из черновика есть - используем его, иначе текущий const actualSessionId = claim.session_token || sessionIdRef.current || formData.session_id; console.log('🔄 Загрузка черновика: ИСПОЛЬЗУЕМ session_id:', actualSessionId); // ✅ Обновляем sessionIdRef на сессию из черновика (если есть) if (claim.session_token && claim.session_token !== sessionIdRef.current) { sessionIdRef.current = claim.session_token; console.log('🔄 Обновляем sessionIdRef на сессию из черновика:', claim.session_token); } // ✅ НОВЫЙ ФЛОУ: Извлекаем documents_required из payload const documentsRequired = body.documents_required || payload.documents_required || []; const documentsUploaded = body.documents_uploaded || payload.documents_uploaded || []; const documentsSkipped = body.documents_skipped || payload.documents_skipped || []; const currentDocIndex = body.current_doc_index ?? payload.current_doc_index ?? 0; console.log('📋 Загрузка черновика - documents_required:', documentsRequired.length, 'шт.'); console.log('📋 Загрузка черновика - body.documents_required:', body.documents_required); console.log('📋 Загрузка черновика - payload.documents_required:', payload.documents_required); console.log('📋 Загрузка черновика - status_code:', claim.status_code); console.log('📋 Загрузка черновика - все ключи payload:', Object.keys(payload)); updateFormData({ 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) => { acc[item.name] = item.value; return acc; }, {}) : formData.wizardPrefill, wizardPrefillArray: body.answers_prefill || payload.answers_prefill || formData.wizardPrefillArray, wizardCoverageReport: body.coverage_report || payload.coverage_report || formData.wizardCoverageReport, wizardUploads: { documents: (body.documents_meta || payload.documents_meta) ? {} : formData.wizardUploads?.documents, custom: formData.wizardUploads?.custom || [], }, 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 // ✅ НОВЫЙ ФЛОУ: Документы documents_required: documentsRequired, documents_uploaded: documentsUploaded, documents_skipped: documentsSkipped, current_doc_index: currentDocIndex, }); setSelectedDraftId(finalClaimId); setShowDraftSelection(false); // ✅ Если все шаги заполнены и статус = draft → переходим к форме подтверждения if (isReadyForConfirmation) { console.log('✅ Все шаги заполнены, преобразуем данные для формы подтверждения'); console.log('✅ hasFormDraft:', hasFormDraft, 'isDraftDocsComplete:', isDraftDocsComplete); setIsPhoneVerified(true); let claimPlanData; // ✅ НОВОЕ: Если есть form_draft — используем его! if (hasFormDraft && formDraft) { console.log('✅ Используем form_draft из БД:', formDraft); console.log('✅ project.description:', formDraft.project?.description); console.log('✅ offenders:', formDraft.offenders); console.log('✅ documentsMeta:', documentsMeta); console.log('✅ documentsMeta[0]?.field_label:', documentsMeta[0]?.field_label); const user = formDraft.user || {}; const project = formDraft.project || {}; // Преобразуем form_draft в формат propertyName (с правильными именами полей!) claimPlanData = { propertyName: { applicant: { // Маппинг полей user → applicant first_name: user.firstname || '', middle_name: user.secondname || '', last_name: user.lastname || '', phone: user.mobile || '', email: user.email || '', birth_date: user.birthday || '', birth_place: user.birthplace || '', address: user.mailingstreet || '', inn: user.inn || '', }, case: { category: project.category || '', direction: project.direction || '', }, contract_or_service: { subject: project.subject || '', amount_paid: project.agrprice || '', agreement_date: project.agrdate || '', period_start: project.startdate || '', period_end: project.finishdate || '', country: project.country || '', hotel: project.hotel || '', }, offenders: (formDraft.offenders || []).map((o: any) => ({ name: o.accountname || '', // ✅ Форма ожидает 'name', а не 'accountname' accountname: o.accountname || '', // Дублируем для совместимости address: o.address || '', email: o.email || '', website: o.website || '', phone: o.phone || '', inn: o.inn || '', ogrn: o.ogrn || '', role: o.role || '', })), claim: { description: project.description || problemDescription || '', // ✅ Описание проблемы reason: project.category || '', // ✅ Причина обращения }, meta: { claim_id: finalClaimId, unified_id: formData.unified_id || '', session_token: actualSessionId, }, // ✅ Используем field_label (человекочитаемые названия) вместо имён файлов attachments_names: documentsMeta.map((d: any) => d.field_label || d.original_file_name || d.file_name || 'Документ'), }, session_token: actualSessionId, claim_id: finalClaimId, prefix: 'clpr_', }; console.log('✅ claimPlanData сформирован:', claimPlanData); console.log('✅ claimPlanData.propertyName.claim.description:', claimPlanData.propertyName.claim.description); console.log('✅ claimPlanData.propertyName.offenders:', claimPlanData.propertyName.offenders); } else { // Старый способ: преобразуем данные из БД claimPlanData = transformDraftToClaimPlanFormat({ claim, payload, body, isTelegramFormat, finalClaimId, actualSessionId, currentFormData: formData, }); } console.log('✅ claimPlanData для формы подтверждения:', claimPlanData); // ✅ Если данные подтверждены и есть данные из CRM - используем их if (contact_data_confirmed && contact_data_from_crm) { // Обновляем applicant данные из CRM if (claimPlanData?.propertyName?.applicant) { claimPlanData.propertyName.applicant = { ...claimPlanData.propertyName.applicant, first_name: contact_data_from_crm.firstname || claimPlanData.propertyName.applicant.first_name, last_name: contact_data_from_crm.lastname || claimPlanData.propertyName.applicant.last_name, middle_name: contact_data_from_crm.cf_1157 || claimPlanData.propertyName.applicant.middle_name, inn: contact_data_from_crm.cf_1257 || claimPlanData.propertyName.applicant.inn, birth_date: contact_data_from_crm.birthday || claimPlanData.propertyName.applicant.birth_date, birth_place: contact_data_from_crm.cf_1263 || claimPlanData.propertyName.applicant.birth_place, address: contact_data_from_crm.mailingstreet || claimPlanData.propertyName.applicant.address, email: contact_data_from_crm.email || claimPlanData.propertyName.applicant.email, phone: contact_data_from_crm.mobile || claimPlanData.propertyName.applicant.phone, }; } } // Сохраняем данные заявления в formData updateFormData({ claimPlanData: claimPlanData, showClaimConfirmation: true, // ✅ Флаги подтверждения данных contact_data_confirmed: contact_data_confirmed, contact_data_can_edit: contact_data_can_edit, contact_data_confirmed_at: contact_data_confirmed_at, }); // Переход к шагу подтверждения произойдёт автоматически через useEffect setCurrentStep(2); // StepWizardPlan (временно, useEffect переключит на подтверждение) return; } // ✅ Определяем шаг для перехода на основе данных черновика // Приоритет: если есть wizard_plan → переходим к визарду (даже если нет problem_description) // После выбора черновика showDraftSelection = false, поэтому: // - Шаг 0 = Step1Phone (но мы его пропускаем, т.к. телефон уже верифицирован) // - Шаг 1 = StepDescription // - Шаг 2 = StepWizardPlan let targetStep = 1; // По умолчанию - описание (шаг 1) // ✅ НОВЫЙ ФЛОУ: Если есть documents_required, показываем загрузку документов if (documentsRequired.length > 0) { targetStep = 2; console.log('✅ Переходим к StepWizardPlan (шаг 2) - НОВЫЙ ФЛОУ: есть documents_required, показываем загрузку документов'); console.log('✅ documents_required:', documentsRequired.length, 'документов'); } else 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'); } else { // Если нет ничего - переходим к описанию (шаг 1) targetStep = 1; console.log('✅ Переходим к StepDescription (шаг 1) - нет описания и плана'); } console.log('🔍 Устанавливаем currentStep:', targetStep); // ✅ Устанавливаем isPhoneVerified = true, чтобы пропустить шаг телефона setIsPhoneVerified(true); setCurrentStep(targetStep); } catch (error) { console.error('Ошибка загрузки черновика:', error); message.error('Не удалось загрузить черновик'); } }, [formData, updateFormData]); // Обработчик выбора черновика const handleSelectDraft = useCallback((claimId: string) => { loadDraft(claimId); }, [loadDraft]); // Проверка наличия черновиков const checkDrafts = useCallback(async (unified_id?: string, phone?: string, sessionId?: string) => { try { console.log('🔍 ========== checkDrafts вызван =========='); console.log('🔍 Параметры:', { unified_id, phone, sessionId }); const params = new URLSearchParams(); // Приоритет: unified_id > phone > session_id if (unified_id) { params.append('unified_id', unified_id); console.log('🔍 Используем unified_id:', unified_id); } else if (phone) { params.append('phone', phone); console.log('🔍 Используем phone:', phone); } else if (sessionId) { params.append('session_id', sessionId); console.log('🔍 Используем session_id:', sessionId); } else { console.warn('⚠️ Нет параметров для поиска черновиков'); return false; } const url = `/api/v1/claims/drafts/list?${params.toString()}`; console.log('🔍 Запрос черновиков:', url); const response = await fetch(url); console.log('🔍 Статус ответа:', response.status, response.statusText); if (!response.ok) { const errorText = await response.text(); console.error('❌ Ошибка запроса черновиков:', response.status, response.statusText, errorText); return false; } const data = await response.json(); console.log('🔍 Полный ответ API черновиков:', JSON.stringify(data, null, 2)); console.log('🔍 Debug info от backend:', data.debug_info || data.debug); const count = data.count || 0; console.log('🔍 Количество черновиков:', count); console.log('🔍 Список черновиков:', data.drafts); setHasDrafts(count > 0); setShowDraftSelection(count > 0); console.log('🔍 Установлены флаги: hasDrafts=', count > 0, 'showDraftSelection=', count > 0); console.log('🔍 ========== checkDrafts завершён =========='); return count > 0; } catch (error) { console.error('❌ Ошибка проверки черновиков:', error); console.error('❌ Stack trace:', (error as Error).stack); return false; } }, []); // Обработчик создания новой заявки const handleNewClaim = useCallback(() => { console.log('🆕 Начинаем новое обращение'); console.log('🆕 Текущий currentStep:', currentStep); console.log('🆕 isPhoneVerified:', isPhoneVerified); // ✅ Генерируем НОВУЮ сессию для новой жалобы const newSessionId = 'sess_' + generateUUIDv4(); console.log('🆕 Генерируем новую сессию для жалобы:', newSessionId); console.log('🆕 Старая сессия:', sessionIdRef.current); // ✅ Обновляем sessionIdRef на новую сессию sessionIdRef.current = newSessionId; // ✅ session_token в localStorage остаётся ПРЕЖНИМ (авторизация сохраняется) const savedSessionToken = localStorage.getItem('session_token'); console.log('🆕 session_token в localStorage (авторизация):', savedSessionToken || '(не сохранён)'); console.log('🆕 Авторизация сохранена: unified_id=', formData.unified_id, 'phone=', formData.phone); setShowDraftSelection(false); setSelectedDraftId(null); setHasDrafts(false); // ✅ Сбрасываем флаг наличия черновиков // ✅ Очищаем данные формы и устанавливаем НОВЫЙ session_id // unified_id, phone, contact_id остаются прежними - авторизация сохранена! updateFormData({ session_id: newSessionId, // ✅ Новая сессия для новой жалобы claim_id: undefined, problemDescription: undefined, wizardPlan: undefined, wizardAnswers: undefined, wizardPrefill: undefined, wizardPrefillArray: undefined, wizardCoverageReport: undefined, wizardUploads: undefined, wizardSkippedDocuments: undefined, eventType: undefined, // ✅ unified_id, phone, contact_id НЕ очищаем - авторизация сохраняется! }); console.log('🆕 Переходим к шагу описания проблемы (пропускаем Phone и DraftSelection)'); // ✅ Переходим к шагу описания проблемы // После сброса флагов черновиков, steps будут: // Шаг 0 - Phone (уже верифицирован, но в массиве есть) // Шаг 1 - Description (сюда переходим) // Шаг 2 - WizardPlan setCurrentStep(1); // ✅ Переходим к описанию (индекс 1) }, [updateFormData, currentStep, isPhoneVerified, formData.unified_id, formData.phone]); // ✅ Автоматический редирект на экран черновиков после успешной отправки useEffect(() => { if (isSubmitted) { console.log('✅ Обращение успешно отправлено, ждём 2.5 секунды перед редиректом на черновики...'); const redirectTimer = setTimeout(async () => { console.log('🔄 Выполняем редирект на экран черновиков'); // Проверяем наличие черновиков const hasDraftsResult = await checkDrafts( formData.unified_id, formData.phone, sessionIdRef.current ); console.log('🔍 Результат проверки черновиков:', hasDraftsResult); // Переходим на экран черновиков setShowDraftSelection(true); setHasDrafts(hasDraftsResult); setIsSubmitted(false); // Сбрасываем флаг отправки setSelectedDraftId(null); // Сбрасываем выбранный черновик // Переходим на шаг 0 (черновики) setTimeout(() => { setCurrentStep(0); console.log('✅ Переход на экран черновиков выполнен'); }, 100); }, 2500); // Задержка 2.5 секунды return () => { clearTimeout(redirectTimer); }; } }, [isSubmitted, formData.unified_id, formData.phone, checkDrafts]); const handleSubmit = useCallback(async () => { try { addDebugEvent('form', 'info', '📤 Отправка заявки в n8n через backend'); const payload = { stage: 'final', form_id: 'ticket_form', session_id: formData.session_id ?? sessionIdRef.current, 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_id: formData.bankId, // ID банка из NSPK API 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', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); 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, }); return; } addDebugEvent('form', 'success', '✅ Финальный webhook в n8n отработал', { response: parsed ?? text, }); // Помечаем, что заявка отправлена, и показываем заглушку. setIsSubmitted(true); message.success('Поздравляем! Ваше обращение направлено в Клиентправ.'); } catch (error) { message.error('Ошибка соединения с сервером'); addDebugEvent('form', 'error', '❌ Ошибка соединения', { error: String(error) }); console.error(error); } }, [formData, addDebugEvent]); // Динамически генерируем шаги на основе выбранного eventType const steps = useMemo(() => { const stepsArray: any[] = []; // Шаг 0: Выбор черновика (показывается только если есть черновики) // ✅ unified_id уже означает, что телефон верифицирован // Показываем шаг, если showDraftSelection=true ИЛИ если есть unified_id и hasDrafts if ((showDraftSelection || (formData.unified_id && hasDrafts)) && !selectedDraftId) { stepsArray.push({ title: 'Черновики', description: 'Выбор заявки', content: ( ), }); } // Шаг 1: Phone (телефон + SMS верификация) stepsArray.push({ title: 'Вход', description: 'Подтверждение телефона', content: ( { updateFormData(data); // ✅ Если 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; } // ❌ Убрано: проверка черновиков здесь избыточна, т.к. она уже есть в onNext }} 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); // ✅ Если передан unified_id, значит телефон уже верифицирован (даже если isPhoneVerified ещё false) // Проверяем черновики, если есть unified_id или телефон верифицирован const shouldCheckDrafts = finalUnifiedId || (formData.phone && isPhoneVerified); if (shouldCheckDrafts && !selectedDraftId) { console.log('🔍 Проверка черновиков с unified_id:', finalUnifiedId, 'phone:', formData.phone, 'sessionId:', sessionIdRef.current); const hasDraftsResult = await checkDrafts(finalUnifiedId, formData.phone, sessionIdRef.current); console.log('🔍 Результат checkDrafts:', hasDraftsResult); console.log('🔍 Текущие флаги после checkDrafts: hasDrafts=', hasDrafts, 'showDraftSelection=', showDraftSelection); if (hasDraftsResult) { console.log('✅ Есть черновики, переходим к шагу 0'); // ✅ ВАЖНО: Сначала устанавливаем флаги, потом переходим на шаг 0 setShowDraftSelection(true); setHasDrafts(true); // ✅ Используем setTimeout для гарантии, что React обновил состояние setTimeout(() => { console.log('🔄 Переходим на шаг 0 после установки флагов'); setCurrentStep(0); // Переходим к шагу выбора черновика }, 100); console.log('🛑 Остановка выполнения onNext - есть черновики'); return; // ✅ ВАЖНО: Не идём дальше, если есть черновики } else { console.log('❌ Нет черновиков, идем дальше к описанию проблемы'); // Нет черновиков - идём дальше nextStep(); return; } } else { console.log('⚠️ Условие не выполнено для проверки черновиков:', { shouldCheckDrafts, selectedDraftId, finalUnifiedId, phone: formData.phone, isPhoneVerified }); // Условие не выполнено - идём дальше nextStep(); return; } // ❌ ЭТОТ КОД НЕ ДОЛЖЕН ВЫПОЛНЯТЬСЯ, если есть return выше console.error('❌❌❌ КРИТИЧЕСКАЯ ОШИБКА: nextStep() вызван после return!'); nextStep(); }} onPrev={prevStep} isPhoneVerified={isPhoneVerified} setIsPhoneVerified={(verified: boolean) => { setIsPhoneVerified(verified); // ❌ Убрано: проверка черновиков делается только в onNext // onNext вызывается после успешной верификации и содержит unified_id }} addDebugEvent={addDebugEvent} /> ), }); // Шаг 2: свободное описание stepsArray.push({ title: 'Обращение', description: 'Опишите ситуацию', content: ( ), }); // Шаг 3: AI Рекомендации stepsArray.push({ title: 'Документы', description: 'Загрузка файлов', content: ( ), }); // Шаг подтверждения заявления (показывается после получения данных из claim:plan) // ✅ НОВЫЙ ФЛОУ: StepClaimConfirmation с SMS подтверждением if (formData.showClaimConfirmation && formData.claimPlanData) { stepsArray.push({ title: 'Подтверждение', description: 'Проверка данных', content: ( setIsSubmitted(true)} /> ), }); } // Step3Payment убран - не используется return stepsArray; }, [formData, isPhoneVerified, nextStep, prevStep, backToDraftsList, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]); const handleReset = () => { console.log('🔄 Начать заново - возврат к списку черновиков'); // ✅ Генерируем новую сессию для новой заявки (но сохраняем авторизацию) const newSessionId = 'sess_' + generateUUIDv4(); sessionIdRef.current = newSessionId; setIsSubmitted(false); setShowDraftSelection(false); setSelectedDraftId(null); // ✅ Очищаем данные формы, НО сохраняем авторизацию (unified_id, phone, contact_id, isPhoneVerified) updateFormData({ session_id: newSessionId, claim_id: undefined, voucher: '', paymentMethod: 'sbp', problemDescription: undefined, wizardPlan: undefined, wizardAnswers: undefined, wizardPrefill: undefined, wizardPrefillArray: undefined, wizardCoverageReport: undefined, wizardUploads: undefined, wizardSkippedDocuments: undefined, eventType: undefined, // ✅ unified_id, phone, contact_id, isPhoneVerified НЕ очищаем }); // ✅ Проверяем черновики и возвращаемся к списку if (formData.unified_id && hasDrafts) { console.log('🔄 Есть черновики - показываем список'); setShowDraftSelection(true); setCurrentStep(0); } else { console.log('🔄 Нет черновиков - переходим к новой заявке'); setCurrentStep(1); // StepDescription } message.info('Форма сброшена'); addDebugEvent('system', 'info', '🔄 Форма сброшена'); }; // Обработчик кнопки "Выход" const handleExitToList = useCallback(async () => { console.log('🚪 Выход из системы'); addDebugEvent('system', 'info', '🚪 Выход из системы'); // ✅ В Telegram Mini App — просто закрываем приложение if (isTelegramMiniApp) { try { const tg = (window as any).Telegram; const webApp = tg?.WebApp; if (webApp && typeof webApp.close === 'function') { webApp.close(); } } catch (error) { console.warn('⚠️ Ошибка при закрытии Telegram Mini App:', error); } return; } // ✅ В обычном веб — полный сброс сессии и возврат к Step1Phone // Получаем 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'); // Полный сброс: очищаем все данные авторизации и черновиков setIsSubmitted(false); setShowDraftSelection(false); setHasDrafts(false); setSelectedDraftId(null); // Генерируем новую сессию для нового пользователя const newSessionId = `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; sessionIdRef.current = newSessionId; // Полностью очищаем formData, включая unified_id и phone setFormData({ voucher: '', claim_id: undefined, session_id: newSessionId, paymentMethod: 'sbp', unified_id: undefined, phone: undefined, contact_id: undefined, is_new_contact: undefined, isPhoneVerified: false, }); // Сбрасываем флаг верификации телефона setIsPhoneVerified(false); // Переходим на экран входа (Step1Phone) setCurrentStep(0); message.info('Сессия завершена. До свидания!'); addDebugEvent('system', 'info', '🔄 Форма сброшена'); }, [formData.session_id, addDebugEvent, isTelegramMiniApp]); // ✅ Показываем loader пока идёт проверка Telegram auth и восстановление сессии if (!telegramAuthChecked || !sessionRestored) { return (
); } return (
{/* Левая часть - Форма (в проде на всю ширину, в деве 14 из 24) */} {/* Кнопка "Выход" - показываем если телефон верифицирован */} {isPhoneVerified && ( )} {/* Кнопка "Начать заново" - показываем только после шага телефона */} {currentStep > 0 && ( )} ) } > {isSubmitted ? (

Поздравляем! Ваше обращение направлено в Клиентправ.

В ближайшее время на указанную Вами электронную почту поступит письмо, подтверждающее регистрацию вашего обращения.

) : ( <> {steps.map((item, index) => ( ))}
{steps[currentStep] ? steps[currentStep].content : (

Загрузка шага...

)}
)}
{/* Правая часть - Debug консоль (только в dev режиме) */} {process.env.NODE_ENV === 'development' && ( )}
); }