Files
aiform_dev/frontend/src/pages/ClaimForm.tsx
AI Assistant 080e7ec105 feat: Получение cf_2624 из MySQL и блокировка полей при подтверждении данных
- Добавлен сервис CrmMySQLService для прямого подключения к MySQL CRM
- Обновлён метод get_draft() для получения cf_2624 напрямую из БД
- Реализована блокировка полей (readonly) при contact_data_confirmed = true
- Добавлен выбор банка для СБП выплат с динамической загрузкой из API
- Обновлена документация по работе с cf_2624 и MySQL
- Добавлен network_mode: host в docker-compose для доступа к MySQL
- Обновлены компоненты формы для поддержки блокировки полей
2025-12-04 12:22:23 +03:00

1451 lines
68 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { Steps, Card, message, Row, Col, Space } from 'antd';
import Step1Phone from '../components/form/Step1Phone';
import StepDescription from '../components/form/StepDescription';
import Step1Policy from '../components/form/Step1Policy';
import StepDraftSelection from '../components/form/StepDraftSelection';
import StepWizardPlan from '../components/form/StepWizardPlan';
import StepClaimConfirmation from '../components/form/StepClaimConfirmation';
import Step2EventType from '../components/form/Step2EventType';
import StepDocumentUpload from '../components/form/StepDocumentUpload';
import Step3Payment from '../components/form/Step3Payment';
import DebugPanel from '../components/DebugPanel';
import { getDocumentsForEventType } from '../constants/documentConfigs';
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<string, any>;
wizardPrefill?: Record<string, any>;
wizardPrefillArray?: Array<{ name: string; value: any }>;
wizardCoverageReport?: any;
wizardUploads?: Record<string, any>;
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<string, {
uploaded: boolean;
data: any;
file_type: string;
skipped?: boolean;
}>;
// Последний шаг: 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<EventSource | null>(null);
const claimPlanTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [currentStep, setCurrentStep] = useState(0);
const [sessionRestored, setSessionRestored] = useState(false); // Флаг: пытались восстановить сессию
const [formData, setFormData] = useState<FormData>({
voucher: '',
claim_id: undefined, // ✅ Будет заполнен n8n в Step1Phone
session_id: sessionIdRef.current,
paymentMethod: 'sbp',
});
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
const [debugEvents, setDebugEvents] = useState<any[]>([]);
const [isSubmitted, setIsSubmitted] = useState(false);
const [showDraftSelection, setShowDraftSelection] = useState(false);
const [selectedDraftId, setSelectedDraftId] = useState<string | null>(null);
const [hasDrafts, setHasDrafts] = useState(false);
useEffect(() => {
// 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился!
console.log('🔥 ClaimForm v3.8 - 2025-11-20 15:10 - Fix session_id priority in loadDraft');
}, []);
// ✅ Восстановление сессии при загрузке страницы
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();
}, []); // Запускаем только при загрузке
// Получаем 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]);
// Динамически определяем список шагов на основе выбранного eventType
const documentConfigs = formData.eventType ? getDocumentsForEventType(formData.eventType) : [];
const totalDocumentSteps = documentConfigs.length;
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<FormData>) => {
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);
return prev - 1;
});
}, []);
// Преобразование данных черновика в формат 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';
// ✅ НОВОЕ: Проверяем наличие 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]);
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: (
<StepDraftSelection
phone={formData.phone || ''}
session_id={sessionIdRef.current}
unified_id={formData.unified_id} // ✅ Передаём unified_id
onSelectDraft={handleSelectDraft}
onNewClaim={handleNewClaim}
/>
),
});
}
// Шаг 1: Phone (телефон + SMS верификация)
stepsArray.push({
title: 'Вход',
description: 'Подтверждение телефона',
content: (
<Step1Phone
formData={{ ...formData, session_id: formData.session_id || sessionIdRef.current }} // ✅ Используем session_id из formData (от n8n) или временный
updateFormData={(data: any) => {
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: (
<StepDescription
formData={formData}
updateFormData={updateFormData}
onPrev={prevStep}
onNext={nextStep}
/>
),
});
// Шаг 3: AI Рекомендации
stepsArray.push({
title: 'Документы',
description: 'Загрузка файлов',
content: (
<StepWizardPlan
formData={formData}
updateFormData={updateFormData}
onPrev={() => {
// Возвращаемся к списку заявок
setShowDraftSelection(true);
setSelectedDraftId(null);
setCurrentStep(0);
}}
onNext={nextStep}
addDebugEvent={addDebugEvent}
/>
),
});
// Шаг подтверждения заявления (показывается после получения данных из claim:plan)
if (formData.showClaimConfirmation && formData.claimPlanData) {
stepsArray.push({
title: 'Подтверждение',
description: 'Проверка данных',
content: (
<StepClaimConfirmation
claimPlanData={formData.claimPlanData}
contact_data_confirmed={formData.contact_data_confirmed}
onPrev={prevStep}
onNext={nextStep}
onSubmitted={() => setIsSubmitted(true)}
/>
),
});
}
// Шаги для СТАРОГО флоу (страхование ERV) — НЕ показываем для нового флоу защиты прав
const isNewClaimFlow = formData.documents_required && formData.documents_required.length > 0;
if (!isNewClaimFlow) {
// Шаг 3: Policy (только для старого флоу)
stepsArray.push({
title: 'Проверка полиса',
description: 'Полис ERV',
content: (
<Step1Policy
formData={{ ...formData, session_id: sessionIdRef.current }}
updateFormData={updateFormData}
onNext={nextStep}
addDebugEvent={addDebugEvent}
/>
),
});
// Шаг 4: Event Type Selection (только для старого флоу)
stepsArray.push({
title: 'Тип события',
description: 'Выбор случая',
content: (
<Step2EventType
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
onPrev={prevStep}
addDebugEvent={addDebugEvent}
/>
),
});
}
// Шаги Document Upload (только для старого флоу — если выбран eventType)
if (!isNewClaimFlow && 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: 'Подтверждение',
content: (
<Step3Payment
formData={formData} // ✅ claim_id уже в formData
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
});
return stepsArray;
}, [formData, documentConfigs, isPhoneVerified, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
const handleReset = () => {
setIsSubmitted(false);
setFormData({
voucher: '',
claim_id: undefined, // ✅ Очищаем для новой заявки
session_id: sessionIdRef.current,
paymentMethod: 'sbp',
});
setCurrentStep(0);
setIsPhoneVerified(false);
message.info('Форма сброшена');
addDebugEvent('system', 'info', '🔄 Форма сброшена');
};
// Обработчик кнопки "Выход" - завершить сессию и вернуться к 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]);
return (
<div className="claim-form-container" style={{ padding: '20px', background: '#ffffff' }}>
<Row gutter={16}>
{/* Левая часть - Форма */}
<Col xs={24} lg={14}>
<Card
title="Подать обращение о защите прав потребителя"
className="claim-form-card"
extra={
!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>
)
}
>
{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>
<div className="steps-content">
{steps[currentStep] ? steps[currentStep].content : (
<div style={{ padding: '40px 0', textAlign: 'center' }}>
<p>Загрузка шага...</p>
</div>
)}
</div>
</>
)}
</Card>
</Col>
{/* Правая часть - Debug консоль */}
<Col xs={24} lg={10}>
<DebugPanel events={debugEvents} formData={formData} />
</Col>
</Row>
</div>
);
}