feat: Session persistence with Redis + Draft management fixes
- Implement session management API (/api/v1/session/create, verify, logout) - Add session restoration from localStorage on page reload - Fix session_id priority when loading drafts (use current, not old from DB) - Add unified_id and claim_id to wizard payload sent to n8n - Add Docker volume for frontend HMR (Hot Module Replacement) - Add comprehensive session logging for debugging Components updated: - backend/app/api/session.py (NEW) - Session management endpoints - backend/app/main.py - Include session router - frontend/src/components/form/Step1Phone.tsx v2.0 - Create session after SMS - frontend/src/pages/ClaimForm.tsx v3.8 - Session restoration & priority fix - frontend/src/components/form/StepWizardPlan.tsx v1.4 - Add unified_id/claim_id - docker-compose.yml - Add frontend volume for live reload Session flow: 1. User verifies phone -> session created in Redis (24h TTL) 2. session_token saved to localStorage 3. Page reload -> session restored automatically 4. Draft selected -> current session_id used (not old from DB) 5. Wizard submit -> unified_id, claim_id, session_id sent to n8n 6. Logout -> session removed from Redis & localStorage Fixes: - Session token not persisting after page reload - unified_id missing in n8n webhook payload - Old session_id from draft overwriting current session - Frontend changes requiring container rebuild
This commit is contained in:
@@ -52,9 +52,9 @@ export default function DebugPanel({ events, formData }: Props) {
|
||||
}}
|
||||
styles={{
|
||||
header: {
|
||||
background: '#252526',
|
||||
color: '#fff',
|
||||
borderBottom: '1px solid #333'
|
||||
background: '#252526',
|
||||
color: '#fff',
|
||||
borderBottom: '1px solid #333'
|
||||
},
|
||||
body: {
|
||||
padding: 12
|
||||
|
||||
@@ -17,6 +17,8 @@ export default function Step1Phone({
|
||||
setIsPhoneVerified,
|
||||
addDebugEvent
|
||||
}: Props) {
|
||||
// 🆕 VERSION CHECK: 2025-11-20 12:40 - session_id fix
|
||||
console.log('📱 Step1Phone v2.0 - 2025-11-20 14:40 - Session creation with debug logs');
|
||||
const [form] = Form.useForm();
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -109,37 +111,120 @@ export default function Step1Phone({
|
||||
}
|
||||
|
||||
console.log('🔥 N8N CRM Response (after array check):', crmResult);
|
||||
console.log('🔥 N8N CRM Response FULL:', JSON.stringify(crmResult, null, 2));
|
||||
|
||||
if (crmResponse.ok && crmResult.success) {
|
||||
// n8n возвращает: {success: true, result: {claim_id, contact_id, ...}}
|
||||
const result = crmResult.result || crmResult;
|
||||
|
||||
console.log('🔥 Extracted result:', result);
|
||||
console.log('🔥 Saving to formData:', {
|
||||
phone,
|
||||
contact_id: result.contact_id,
|
||||
claim_id: result.claim_id,
|
||||
unified_id: result.unified_id, // ← Добавляем в лог
|
||||
is_new_contact: result.is_new_contact
|
||||
});
|
||||
console.log('🔥 result.unified_id:', result.unified_id);
|
||||
console.log('🔥 typeof result.unified_id:', typeof result.unified_id);
|
||||
console.log('🔥 result keys:', Object.keys(result));
|
||||
|
||||
addDebugEvent?.('crm', 'success', `✅ Контакт создан/найден в CRM`, result);
|
||||
// ✅ ВАЖНО: Проверяем наличие unified_id
|
||||
if (!result.unified_id) {
|
||||
console.error('❌ unified_id отсутствует в ответе n8n!');
|
||||
console.error('❌ Полный ответ result:', result);
|
||||
console.error('❌ Полный ответ crmResult:', crmResult);
|
||||
message.warning('⚠️ unified_id не получен от n8n, черновики могут не отображаться');
|
||||
} else {
|
||||
console.log('✅ unified_id получен:', result.unified_id);
|
||||
}
|
||||
|
||||
// Сохраняем данные из CRM в форму
|
||||
updateFormData({
|
||||
// ✅ Извлекаем session_id от n8n (если есть)
|
||||
const session_id_from_n8n = result.session;
|
||||
|
||||
console.log('🔍 Проверка session_id от n8n:');
|
||||
console.log('🔍 result.session:', result.session);
|
||||
console.log('🔍 session_id_from_n8n:', session_id_from_n8n);
|
||||
console.log('🔍 formData.session_id (текущий):', formData.session_id);
|
||||
|
||||
if (session_id_from_n8n) {
|
||||
console.log('✅ session_id получен от n8n:', session_id_from_n8n);
|
||||
} else {
|
||||
console.warn('⚠️ session_id не найден в ответе n8n, используем текущий:', formData.session_id);
|
||||
}
|
||||
|
||||
const finalSessionId = session_id_from_n8n || formData.session_id;
|
||||
console.log('🔍 finalSessionId (будет сохранён):', finalSessionId);
|
||||
|
||||
const dataToSave = {
|
||||
phone,
|
||||
smsCode: code,
|
||||
contact_id: result.contact_id,
|
||||
unified_id: result.unified_id, // ✅ Unified ID из PostgreSQL (получаем от n8n)
|
||||
claim_id: result.claim_id,
|
||||
session_id: finalSessionId, // ✅ Используем session_id от n8n, если есть
|
||||
// claim_id убран - используем только session_id на этих этапах
|
||||
is_new_contact: result.is_new_contact
|
||||
});
|
||||
};
|
||||
|
||||
console.log('🔥 ========== SAVING TO FORMDATA ==========');
|
||||
console.log('🔥 Saving to formData:', JSON.stringify(dataToSave, null, 2));
|
||||
console.log('🔥 dataToSave.unified_id:', dataToSave.unified_id);
|
||||
console.log('🔥 dataToSave.session_id:', dataToSave.session_id);
|
||||
console.log('🔥 =========================================');
|
||||
|
||||
addDebugEvent?.('crm', 'success', `✅ Контакт создан/найден в CRM`, result);
|
||||
|
||||
// Сохраняем данные из CRM в форму
|
||||
updateFormData(dataToSave);
|
||||
|
||||
message.success(result.is_new_contact ? 'Контакт создан!' : 'Контакт найден!');
|
||||
|
||||
// ✅ Устанавливаем isPhoneVerified = true после успешной верификации
|
||||
setIsPhoneVerified(true);
|
||||
|
||||
// 🔑 Создаём сессию в Redis для живучести (24 часа)
|
||||
try {
|
||||
console.log('🔑 Создаём сессию в Redis:', {
|
||||
session_token: finalSessionId,
|
||||
unified_id: result.unified_id,
|
||||
phone: phone,
|
||||
contact_id: result.contact_id
|
||||
});
|
||||
|
||||
const sessionResponse = await fetch('/api/v1/session/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_token: finalSessionId,
|
||||
unified_id: result.unified_id,
|
||||
phone: phone,
|
||||
contact_id: result.contact_id,
|
||||
ttl_hours: 24
|
||||
})
|
||||
});
|
||||
|
||||
console.log('🔑 Session create response status:', sessionResponse.status);
|
||||
|
||||
if (sessionResponse.ok) {
|
||||
const sessionData = await sessionResponse.json();
|
||||
console.log('🔑 Session create response data:', sessionData);
|
||||
|
||||
// Сохраняем session_token в localStorage для последующих визитов
|
||||
localStorage.setItem('session_token', finalSessionId);
|
||||
console.log('✅ Сессия создана в Redis, session_token сохранён в localStorage:', finalSessionId);
|
||||
console.log('✅ Проверка: localStorage.getItem("session_token"):', localStorage.getItem('session_token'));
|
||||
addDebugEvent?.('session', 'success', '✅ Сессия создана (TTL 24h)');
|
||||
} else {
|
||||
const errorText = await sessionResponse.text();
|
||||
console.warn('⚠️ Не удалось создать сессию в Redis:', sessionResponse.status, errorText);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.error('❌ Ошибка создания сессии:', sessionError);
|
||||
// Не блокируем дальнейшую работу
|
||||
}
|
||||
|
||||
// ✅ Передаем unified_id напрямую в onNext для проверки черновиков
|
||||
// Это нужно, потому что formData может еще не обновиться
|
||||
onNext(result.unified_id);
|
||||
const unifiedIdToPass = result.unified_id;
|
||||
console.log('🔥 ============================================');
|
||||
console.log('🔥 Передаём unified_id в onNext:', unifiedIdToPass);
|
||||
console.log('🔥 typeof unifiedIdToPass:', typeof unifiedIdToPass);
|
||||
console.log('🔥 Вызываем onNext с unified_id:', unifiedIdToPass);
|
||||
console.log('🔥 ============================================');
|
||||
onNext(unifiedIdToPass);
|
||||
} else {
|
||||
addDebugEvent?.('crm', 'error', '❌ Ошибка создания контакта в CRM', crmResult);
|
||||
message.error('Ошибка создания контакта в CRM');
|
||||
|
||||
@@ -209,7 +209,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
body: JSON.stringify({
|
||||
claim_id: formData.claim_id, // Передаём claim_id для создания записи
|
||||
policy_number: values.voucher,
|
||||
session_id: sessionStorage.getItem('session_id') || 'unknown'
|
||||
session_id: formData.session_id || 'unknown'
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -345,7 +345,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
||||
uploadFormData.append('file_type', 'policy_scan');
|
||||
uploadFormData.append('filename', pdfFile.name); // PDF имя
|
||||
uploadFormData.append('voucher', values.voucher);
|
||||
uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown');
|
||||
uploadFormData.append('session_id', formData.session_id || 'unknown');
|
||||
uploadFormData.append('upload_timestamp', new Date().toISOString());
|
||||
uploadFormData.append('file', pdfFile); // PDF файл!
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
|
||||
uploadFormData.append('file_type', currentDocConfig.file_type);
|
||||
uploadFormData.append('filename', currentFile.name);
|
||||
uploadFormData.append('voucher', formData.voucher || '');
|
||||
uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown');
|
||||
uploadFormData.append('session_id', formData.session_id || 'unknown');
|
||||
uploadFormData.append('upload_timestamp', new Date().toISOString());
|
||||
uploadFormData.append('file', currentFile);
|
||||
|
||||
|
||||
@@ -53,20 +53,14 @@ export default function StepDescription({
|
||||
message.error('Не найден session_id. Попробуйте обновить страницу.');
|
||||
return;
|
||||
}
|
||||
if (!formData.claim_id) {
|
||||
message.error('Не удалось определить номер обращения. Вернитесь на шаг с телефоном.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
if (useMockWizard && wizardPlanSample?.wizard_plan) {
|
||||
const mockPrefill = buildPrefillMap(wizardPlanSample.answers_prefill);
|
||||
const mockClaimId = wizardPlanSample.claim_id || formData.claim_id;
|
||||
|
||||
updateFormData({
|
||||
problemDescription: safeDescription,
|
||||
claim_id: mockClaimId,
|
||||
wizardPlan: wizardPlanSample.wizard_plan,
|
||||
wizardPlanStatus: 'ready',
|
||||
wizardPrefill: mockPrefill,
|
||||
@@ -85,7 +79,6 @@ export default function StepDescription({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: formData.session_id,
|
||||
claim_id: formData.claim_id,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
problem_description: safeDescription,
|
||||
|
||||
@@ -32,8 +32,9 @@ interface Draft {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
phone: string;
|
||||
phone?: string;
|
||||
session_id?: string;
|
||||
unified_id?: string; // ✅ Добавляем unified_id
|
||||
onSelectDraft: (claimId: string) => void;
|
||||
onNewClaim: () => void;
|
||||
}
|
||||
@@ -41,6 +42,7 @@ interface Props {
|
||||
export default function StepDraftSelection({
|
||||
phone,
|
||||
session_id,
|
||||
unified_id, // ✅ Добавляем unified_id
|
||||
onSelectDraft,
|
||||
onNewClaim,
|
||||
}: Props) {
|
||||
@@ -52,18 +54,29 @@ export default function StepDraftSelection({
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
if (session_id) {
|
||||
params.append('session_id', session_id);
|
||||
// ✅ Приоритет: unified_id > phone > session_id
|
||||
if (unified_id) {
|
||||
params.append('unified_id', unified_id);
|
||||
console.log('🔍 StepDraftSelection: загружаем черновики по unified_id:', unified_id);
|
||||
} else if (phone) {
|
||||
params.append('phone', phone);
|
||||
console.log('🔍 StepDraftSelection: загружаем черновики по phone:', phone);
|
||||
} else if (session_id) {
|
||||
params.append('session_id', session_id);
|
||||
console.log('🔍 StepDraftSelection: загружаем черновики по session_id:', session_id);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/claims/drafts/list?${params.toString()}`);
|
||||
const url = `/api/v1/claims/drafts/list?${params.toString()}`;
|
||||
console.log('🔍 StepDraftSelection: запрос:', url);
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось загрузить черновики');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('🔍 StepDraftSelection: ответ API:', data);
|
||||
console.log('🔍 StepDraftSelection: количество черновиков:', data.count);
|
||||
setDrafts(data.drafts || []);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки черновиков:', error);
|
||||
@@ -75,7 +88,7 @@ export default function StepDraftSelection({
|
||||
|
||||
useEffect(() => {
|
||||
loadDrafts();
|
||||
}, [phone, session_id]);
|
||||
}, [phone, session_id, unified_id]); // ✅ Добавляем unified_id в зависимости
|
||||
|
||||
const handleDelete = async (claimId: string) => {
|
||||
try {
|
||||
@@ -119,11 +132,11 @@ export default function StepDraftSelection({
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 8 }}>
|
||||
Продолжить заполнение или создать новую заявку?
|
||||
<Title level={2} style={{ marginBottom: 8, color: '#1890ff' }}>
|
||||
📋 Ваши черновики заявок
|
||||
</Title>
|
||||
<Paragraph type="secondary">
|
||||
У вас есть незавершенные черновики. Вы можете продолжить заполнение или создать новую заявку.
|
||||
<Paragraph type="secondary" style={{ fontSize: 14, marginBottom: 16 }}>
|
||||
Выберите черновик, чтобы продолжить заполнение, или создайте новую заявку.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +170,13 @@ export default function StepDraftSelection({
|
||||
<Button
|
||||
key="continue"
|
||||
type="primary"
|
||||
onClick={() => onSelectDraft(draft.claim_id!)}
|
||||
onClick={() => {
|
||||
console.log('🔍 Выбран черновик:', draft.claim_id, 'id:', draft.id);
|
||||
// Используем id (UUID) если claim_id отсутствует
|
||||
const draftId = draft.claim_id || draft.id;
|
||||
console.log('🔍 Загружаем черновик с ID:', draftId);
|
||||
onSelectDraft(draftId);
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
Продолжить
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function StepWizardPlan({
|
||||
onPrev,
|
||||
addDebugEvent,
|
||||
}: Props) {
|
||||
console.log('🔥 StepWizardPlan v1.4 - 2025-11-20 15:00 - Add unified_id and claim_id to wizard payload');
|
||||
const [form] = Form.useForm();
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -146,6 +147,36 @@ export default function StepWizardPlan({
|
||||
debugLoggerRef.current = addDebugEvent;
|
||||
}, [addDebugEvent]);
|
||||
|
||||
// ✅ Автосохранение прогресса заполнения (debounce 3 секунды)
|
||||
useEffect(() => {
|
||||
if (!formData.claim_id || !formValues) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
const answers = form.getFieldsValue(true);
|
||||
|
||||
// Сохраняем только если есть хоть какие-то ответы
|
||||
const hasAnswers = Object.keys(answers).some(key => answers[key] !== undefined && answers[key] !== '');
|
||||
|
||||
if (hasAnswers) {
|
||||
console.log('💾 Автосохранение прогресса:', { claim_id: formData.claim_id, answersCount: Object.keys(answers).length });
|
||||
|
||||
// Обновляем formData с текущими ответами
|
||||
updateFormData({
|
||||
wizardAnswers: answers,
|
||||
wizardUploads: {
|
||||
documents: questionFileBlocks,
|
||||
custom: customFileBlocks,
|
||||
},
|
||||
wizardSkippedDocuments: Array.from(skippedDocuments),
|
||||
});
|
||||
|
||||
addDebugEvent?.('wizard', 'info', '💾 Автосохранение прогресса');
|
||||
}
|
||||
}, 3000); // 3 секунды debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [formValues, formData.claim_id]); // Зависимость от formValues, но БЕЗ questionFileBlocks/customFileBlocks/skippedDocuments (они обновляются отдельно)
|
||||
|
||||
const questions: WizardQuestion[] = useMemo(() => plan?.questions || [], [plan]);
|
||||
const documents: WizardDocument[] = plan?.documents || [];
|
||||
|
||||
@@ -339,19 +370,19 @@ export default function StepWizardPlan({
|
||||
}, [formValues, plan, questions, documentGroups, questionFileBlocks, handleDocumentBlocksChange, skippedDocuments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWaiting || !formData.claim_id || plan) {
|
||||
if (!isWaiting || !formData.session_id || plan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const claimId = formData.claim_id;
|
||||
const source = new EventSource(`/events/${claimId}`);
|
||||
const sessionId = formData.session_id;
|
||||
const source = new EventSource(`/events/${sessionId}`);
|
||||
eventSourceRef.current = source;
|
||||
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { session_id: sessionId });
|
||||
|
||||
// Таймаут: если план не пришёл за 2 минуты (RAG может работать долго), показываем ошибку
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setConnectionError('План вопросов не получен. Проверьте, что n8n обработал описание проблемы.');
|
||||
debugLoggerRef.current?.('wizard', 'error', '⏱️ Таймаут ожидания плана вопросов (2 минуты)', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'error', '⏱️ Таймаут ожидания плана вопросов (2 минуты)', { session_id: sessionId });
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
@@ -360,7 +391,7 @@ export default function StepWizardPlan({
|
||||
|
||||
source.onopen = () => {
|
||||
setConnectionError(null);
|
||||
debugLoggerRef.current?.('wizard', 'info', '✅ SSE соединение открыто', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'info', '✅ SSE соединение открыто', { session_id: sessionId });
|
||||
};
|
||||
|
||||
source.onerror = (error) => {
|
||||
@@ -368,7 +399,7 @@ export default function StepWizardPlan({
|
||||
setConnectionError('Не удалось получить ответ от AI. Попробуйте ещё раз.');
|
||||
source.close();
|
||||
eventSourceRef.current = null;
|
||||
debugLoggerRef.current?.('wizard', 'error', '❌ SSE ошибка (wizard)', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'error', '❌ SSE ошибка (wizard)', { session_id: sessionId });
|
||||
};
|
||||
|
||||
const extractWizardPayload = (incoming: any): any => {
|
||||
@@ -403,7 +434,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Логируем все события для отладки
|
||||
debugLoggerRef.current?.('wizard', 'info', '📨 Получено SSE событие', {
|
||||
claim_id: claimId,
|
||||
session_id: sessionId,
|
||||
event_type: eventType,
|
||||
has_wizard_plan: Boolean(extractWizardPayload(payload)),
|
||||
payload_keys: Object.keys(payload),
|
||||
@@ -419,7 +450,7 @@ export default function StepWizardPlan({
|
||||
const coverageReport = wizardPayload?.coverage_report;
|
||||
|
||||
debugLoggerRef.current?.('wizard', 'success', '✨ Получен план вопросов', {
|
||||
claim_id: claimId,
|
||||
session_id: sessionId,
|
||||
questions: wizardPlan?.questions?.length || 0,
|
||||
});
|
||||
|
||||
@@ -459,11 +490,11 @@ export default function StepWizardPlan({
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isWaiting, formData.claim_id, plan, updateFormData]);
|
||||
}, [isWaiting, formData.session_id, plan, updateFormData]);
|
||||
|
||||
const handleRefreshPlan = () => {
|
||||
if (!formData.claim_id) {
|
||||
message.error('Не найден claim_id для подписки на события.');
|
||||
if (!formData.session_id) {
|
||||
message.error('Не найден session_id для подписки на события.');
|
||||
return;
|
||||
}
|
||||
setIsWaiting(true);
|
||||
@@ -561,7 +592,7 @@ export default function StepWizardPlan({
|
||||
try {
|
||||
setSubmitting(true);
|
||||
addDebugEvent?.('wizard', 'info', '📤 Отправляем данные визарда в n8n', {
|
||||
claim_id: formData.claim_id,
|
||||
session_id: formData.session_id,
|
||||
});
|
||||
|
||||
const formPayload = new FormData();
|
||||
@@ -570,6 +601,8 @@ export default function StepWizardPlan({
|
||||
if (formData.session_id) formPayload.append('session_id', formData.session_id);
|
||||
if (formData.clientIp) formPayload.append('client_ip', formData.clientIp);
|
||||
if (formData.smsCode) formPayload.append('sms_code', formData.smsCode);
|
||||
// Добавляем unified_id и claim_id (если есть)
|
||||
if (formData.unified_id) formPayload.append('unified_id', formData.unified_id);
|
||||
if (formData.claim_id) formPayload.append('claim_id', formData.claim_id);
|
||||
if (formData.contact_id) formPayload.append('contact_id', String(formData.contact_id));
|
||||
if (formData.project_id) formPayload.append('project_id', String(formData.project_id));
|
||||
@@ -686,6 +719,15 @@ export default function StepWizardPlan({
|
||||
});
|
||||
});
|
||||
|
||||
// Логируем ключевые поля перед отправкой
|
||||
console.log('📤 Отправка в n8n:', {
|
||||
session_id: formData.session_id,
|
||||
unified_id: formData.unified_id,
|
||||
claim_id: formData.claim_id,
|
||||
contact_id: formData.contact_id,
|
||||
phone: formData.phone,
|
||||
});
|
||||
|
||||
const response = await fetch('/api/v1/claims/wizard', {
|
||||
method: 'POST',
|
||||
body: formPayload,
|
||||
@@ -978,7 +1020,14 @@ export default function StepWizardPlan({
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderQuestions = () => (
|
||||
const renderQuestions = () => {
|
||||
console.log('🔍 StepWizardPlan renderQuestions:', {
|
||||
questionsCount: questions.length,
|
||||
documentsCount: documents.length,
|
||||
questions: questions.map(q => ({ name: q.name, label: q.label, input_type: q.input_type, required: q.required }))
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
size="small"
|
||||
@@ -1001,21 +1050,21 @@ export default function StepWizardPlan({
|
||||
initialValues={{ ...prefillMap, ...formData.wizardAnswers }}
|
||||
>
|
||||
{questions.map((question) => {
|
||||
// Для условных полей используем dependencies для отслеживания изменений
|
||||
const dependencies = question.ask_if ? [question.ask_if.field] : undefined;
|
||||
// Для условных полей используем shouldUpdate для отслеживания изменений
|
||||
const hasCondition = !!question.ask_if;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={question.name}
|
||||
dependencies={dependencies}
|
||||
shouldUpdate={dependencies ? (prev, curr) => {
|
||||
shouldUpdate={hasCondition ? (prev, curr) => {
|
||||
// Обновляем только если изменилось значение поля, от которого зависит вопрос
|
||||
return prev[question.ask_if!.field] !== curr[question.ask_if!.field];
|
||||
} : undefined}
|
||||
} : true} // ✅ Для безусловных полей shouldUpdate=true, чтобы render function работала
|
||||
>
|
||||
{() => {
|
||||
const values = form.getFieldsValue(true);
|
||||
if (!evaluateCondition(question.ask_if, values)) {
|
||||
console.log(`⏭️ Question ${question.name} skipped: condition not met`, question.ask_if, values);
|
||||
return null;
|
||||
}
|
||||
const questionDocs = documentGroups[question.name] || [];
|
||||
@@ -1045,9 +1094,12 @@ export default function StepWizardPlan({
|
||||
// (даже если вопрос не связан с documentGroups)
|
||||
// Загрузка файлов уже реализована через блоки документов (documents)
|
||||
if (isDocumentUploadQuestion && documents.length > 0) {
|
||||
console.log(`🚫 Question ${question.name} hidden: isDocumentUploadQuestion=true, documents.length=${documents.length}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`✅ Question ${question.name} will render:`, { input_type: question.input_type, label: question.label, required: question.required });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
@@ -1094,14 +1146,15 @@ export default function StepWizardPlan({
|
||||
</Form>
|
||||
{renderCustomUploads()}
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
if (!formData.claim_id) {
|
||||
if (!formData.session_id) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title="Нет claim_id"
|
||||
subTitle="Не удалось определить идентификатор заявки. Вернитесь на предыдущий шаг и попробуйте снова."
|
||||
title="Нет session_id"
|
||||
subTitle="Не удалось определить идентификатор сессии. Вернитесь на предыдущий шаг и попробуйте снова."
|
||||
extra={<Button onClick={onPrev}>Вернуться</Button>}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { Steps, Card, message, Row, Col } from 'antd';
|
||||
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';
|
||||
@@ -68,21 +68,16 @@ export default function ClaimForm() {
|
||||
// ✅ claim_id будет создан n8n в Step1Phone после SMS верификации
|
||||
// Не генерируем его локально!
|
||||
|
||||
// Генерируем session_id и сохраняем в sessionStorage
|
||||
const [sessionId] = useState(() => {
|
||||
let sid = sessionStorage.getItem('session_id');
|
||||
if (!sid) {
|
||||
sid = `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
sessionStorage.setItem('session_id', sid);
|
||||
}
|
||||
return sid;
|
||||
});
|
||||
// session_id будет получен от n8n при создании контакта
|
||||
// Используем useRef чтобы sessionId не вызывал перерендер и был стабильным
|
||||
const sessionIdRef = useRef(`sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [sessionRestored, setSessionRestored] = useState(false); // Флаг: пытались восстановить сессию
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
voucher: '',
|
||||
claim_id: undefined, // ✅ Будет заполнен n8n в Step1Phone
|
||||
session_id: sessionId,
|
||||
session_id: sessionIdRef.current,
|
||||
paymentMethod: 'sbp',
|
||||
});
|
||||
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
|
||||
@@ -94,9 +89,104 @@ export default function ClaimForm() {
|
||||
|
||||
useEffect(() => {
|
||||
// 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился!
|
||||
console.log('🔥 ClaimForm v2.0 - claim_id НЕ генерируется на фронте!');
|
||||
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 () => {
|
||||
@@ -157,57 +247,142 @@ export default function ClaimForm() {
|
||||
// Загрузка черновика
|
||||
const loadDraft = useCallback(async (claimId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/claims/drafts/${claimId}`);
|
||||
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) {
|
||||
throw new Error('Не удалось загрузить черновик');
|
||||
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 || {};
|
||||
|
||||
// ✅ Для 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('🔍 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;
|
||||
const problemDescription = body.problem_description || payload.problem_description || body.description || payload.description;
|
||||
|
||||
// ✅ Парсим 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);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 problem_description:', problemDescription ? 'есть' : 'нет');
|
||||
console.log('🔍 wizard_plan:', wizardPlan ? 'есть' : 'нет');
|
||||
console.log('🔍 answers:', answers ? 'есть' : 'нет');
|
||||
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);
|
||||
const actualSessionId = sessionIdRef.current || formData.session_id;
|
||||
console.log('🔄 Загрузка черновика: ИСПОЛЬЗУЕМ session_id:', actualSessionId);
|
||||
|
||||
updateFormData({
|
||||
claim_id: claim.claim_id,
|
||||
session_id: claim.session_token || sessionId,
|
||||
phone: payload.phone || formData.phone,
|
||||
email: payload.email || formData.email,
|
||||
problemDescription: payload.problem_description || formData.problemDescription,
|
||||
wizardPlan: payload.wizard_plan || formData.wizardPlan,
|
||||
wizardAnswers: payload.answers || formData.wizardAnswers,
|
||||
wizardPrefill: payload.answers_prefill ?
|
||||
payload.answers_prefill.reduce((acc: any, item: any) => {
|
||||
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: payload.answers_prefill || formData.wizardPrefillArray,
|
||||
wizardCoverageReport: payload.coverage_report || formData.wizardCoverageReport,
|
||||
wizardPrefillArray: body.answers_prefill || payload.answers_prefill || formData.wizardPrefillArray,
|
||||
wizardCoverageReport: body.coverage_report || payload.coverage_report || formData.wizardCoverageReport,
|
||||
wizardUploads: {
|
||||
documents: payload.documents_meta ? {} : formData.wizardUploads?.documents,
|
||||
documents: (body.documents_meta || payload.documents_meta) ? {} : formData.wizardUploads?.documents,
|
||||
custom: formData.wizardUploads?.custom || [],
|
||||
},
|
||||
wizardSkippedDocuments: payload.wizard_skipped_documents || formData.wizardSkippedDocuments,
|
||||
eventType: payload.event_type || formData.eventType,
|
||||
contact_id: payload.contact_id || formData.contact_id,
|
||||
project_id: payload.project_id || formData.project_id,
|
||||
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
|
||||
});
|
||||
|
||||
setSelectedDraftId(claimId);
|
||||
setSelectedDraftId(finalClaimId);
|
||||
setShowDraftSelection(false);
|
||||
|
||||
// Переходим к шагу с описанием, если оно есть, иначе к шагу с рекомендациями
|
||||
if (payload.problem_description) {
|
||||
// Если есть описание, переходим к шагу с рекомендациями
|
||||
setCurrentStep(2); // StepWizardPlan
|
||||
// ✅ Определяем шаг для перехода на основе данных черновика
|
||||
// Приоритет: если есть wizard_plan → переходим к визарду (даже если нет problem_description)
|
||||
// После выбора черновика showDraftSelection = false, поэтому:
|
||||
// - Шаг 0 = Step1Phone (но мы его пропускаем, т.к. телефон уже верифицирован)
|
||||
// - Шаг 1 = StepDescription
|
||||
// - Шаг 2 = StepWizardPlan
|
||||
|
||||
let targetStep = 1; // По умолчанию - описание (шаг 1)
|
||||
|
||||
if (wizardPlan) {
|
||||
// ✅ Если есть wizard_plan - переходим к визарду (шаг 2)
|
||||
// Пользователь уже описывал проблему, и есть план вопросов
|
||||
targetStep = 2;
|
||||
console.log('✅ Переходим к StepWizardPlan (шаг 2) - есть wizard_plan');
|
||||
console.log('✅ answers в черновике:', answers ? 'есть (показываем заполненную форму)' : 'нет (показываем пустую форму)');
|
||||
} else if (problemDescription) {
|
||||
// Если есть описание, но нет плана - переходим к визарду (шаг 2), чтобы получить план
|
||||
targetStep = 2;
|
||||
console.log('✅ Переходим к StepWizardPlan (шаг 2) - есть описание, план будет получен через SSE');
|
||||
} else {
|
||||
// Если нет описания, переходим к шагу с описанием
|
||||
setCurrentStep(1); // StepDescription
|
||||
// Если нет ничего - переходим к описанию (шаг 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, sessionId, updateFormData]);
|
||||
}, [formData, updateFormData]);
|
||||
|
||||
// Обработчик выбора черновика
|
||||
const handleSelectDraft = useCallback((claimId: string) => {
|
||||
@@ -240,6 +415,7 @@ export default function ClaimForm() {
|
||||
|
||||
const data = await response.json();
|
||||
console.log('🔍 Ответ API черновиков:', data);
|
||||
console.log('🔍 Debug info от backend:', data.debug);
|
||||
const count = data.count || 0;
|
||||
console.log('🔍 Количество черновиков:', count);
|
||||
|
||||
@@ -254,8 +430,14 @@ export default function ClaimForm() {
|
||||
|
||||
// Обработчик создания новой заявки
|
||||
const handleNewClaim = useCallback(() => {
|
||||
console.log('🆕 Начинаем новое обращение');
|
||||
console.log('🆕 Текущий currentStep:', currentStep);
|
||||
console.log('🆕 isPhoneVerified:', isPhoneVerified);
|
||||
|
||||
setShowDraftSelection(false);
|
||||
setSelectedDraftId(null);
|
||||
setHasDrafts(false); // ✅ Сбрасываем флаг наличия черновиков
|
||||
|
||||
// Очищаем данные формы, кроме телефона и session_id
|
||||
updateFormData({
|
||||
claim_id: undefined,
|
||||
@@ -269,9 +451,16 @@ export default function ClaimForm() {
|
||||
wizardSkippedDocuments: undefined,
|
||||
eventType: undefined,
|
||||
});
|
||||
// Переходим к шагу с описанием
|
||||
setCurrentStep(1);
|
||||
}, [updateFormData]);
|
||||
|
||||
console.log('🆕 Переходим к шагу описания проблемы (пропускаем Phone и DraftSelection)');
|
||||
|
||||
// ✅ Переходим к шагу описания проблемы
|
||||
// После сброса флагов черновиков, steps будут:
|
||||
// Шаг 0 - Phone (уже верифицирован, но в массиве есть)
|
||||
// Шаг 1 - Description (сюда переходим)
|
||||
// Шаг 2 - WizardPlan
|
||||
setCurrentStep(1); // ✅ Переходим к описанию (индекс 1)
|
||||
}, [updateFormData, currentStep, isPhoneVerified]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
@@ -280,7 +469,7 @@ export default function ClaimForm() {
|
||||
const payload = {
|
||||
stage: 'final',
|
||||
form_id: 'ticket_form',
|
||||
session_id: formData.session_id ?? sessionId,
|
||||
session_id: formData.session_id ?? sessionIdRef.current,
|
||||
client_ip: formData.clientIp,
|
||||
sms_code: formData.smsCode,
|
||||
|
||||
@@ -346,21 +535,24 @@ export default function ClaimForm() {
|
||||
addDebugEvent('form', 'error', '❌ Ошибка соединения', { error: String(error) });
|
||||
console.error(error);
|
||||
}
|
||||
}, [formData, sessionId, addDebugEvent]);
|
||||
}, [formData, addDebugEvent]);
|
||||
|
||||
// Динамически генерируем шаги на основе выбранного eventType
|
||||
const steps = useMemo(() => {
|
||||
const stepsArray: any[] = [];
|
||||
|
||||
// Шаг 0: Выбор черновика (показывается только если есть черновики и телефон верифицирован)
|
||||
if (showDraftSelection && isPhoneVerified && !selectedDraftId && hasDrafts) {
|
||||
// Шаг 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={sessionId}
|
||||
session_id={sessionIdRef.current}
|
||||
unified_id={formData.unified_id} // ✅ Передаём unified_id
|
||||
onSelectDraft={handleSelectDraft}
|
||||
onNewClaim={handleNewClaim}
|
||||
/>
|
||||
@@ -374,13 +566,15 @@ export default function ClaimForm() {
|
||||
description: 'Подтверждение по SMS',
|
||||
content: (
|
||||
<Step1Phone
|
||||
formData={{ ...formData, session_id: sessionId }} // ✅ claim_id будет создан n8n
|
||||
formData={{ ...formData, session_id: formData.session_id || sessionIdRef.current }} // ✅ Используем session_id из formData (от n8n) или временный
|
||||
updateFormData={(data: any) => {
|
||||
updateFormData(data);
|
||||
// После верификации телефона проверяем черновики
|
||||
if (data.phone && isPhoneVerified && !selectedDraftId && !showDraftSelection) {
|
||||
setShowDraftSelection(true);
|
||||
// ✅ Если 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);
|
||||
@@ -393,33 +587,59 @@ export default function ClaimForm() {
|
||||
const finalUnifiedId = unified_id || formData.unified_id;
|
||||
console.log('🔥 finalUnifiedId:', finalUnifiedId);
|
||||
|
||||
if (formData.phone && isPhoneVerified && !selectedDraftId) {
|
||||
// ✅ Если передан unified_id, значит телефон уже верифицирован (даже если isPhoneVerified ещё false)
|
||||
// Проверяем черновики, если есть unified_id или телефон верифицирован
|
||||
const shouldCheckDrafts = finalUnifiedId || (formData.phone && isPhoneVerified);
|
||||
|
||||
if (shouldCheckDrafts && !selectedDraftId) {
|
||||
console.log('🔍 Проверка черновиков с unified_id:', finalUnifiedId, 'phone:', formData.phone);
|
||||
const hasDraftsResult = await checkDrafts(finalUnifiedId, formData.phone, sessionId);
|
||||
const hasDraftsResult = await checkDrafts(finalUnifiedId, formData.phone, sessionIdRef.current);
|
||||
console.log('🔍 Результат checkDrafts:', hasDraftsResult);
|
||||
if (hasDraftsResult) {
|
||||
console.log('✅ Есть черновики, переходим к шагу 0');
|
||||
setCurrentStep(0); // Переходим к шагу выбора черновика
|
||||
// ✅ ВАЖНО: Сначала устанавливаем флаги, потом переходим на шаг 0
|
||||
setShowDraftSelection(true);
|
||||
setHasDrafts(true);
|
||||
// ✅ Ждём следующего тика, чтобы useMemo пересчитался с новыми флагами
|
||||
// Используем requestAnimationFrame для гарантии, что React обновил состояние
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
console.log('🔄 Переходим на шаг 0 после установки флагов');
|
||||
setCurrentStep(0); // Переходим к шагу выбора черновика
|
||||
});
|
||||
});
|
||||
console.log('🛑 Остановка выполнения onNext - есть черновики');
|
||||
console.log('🛑 RETURN - функция должна остановиться здесь');
|
||||
return; // ✅ ВАЖНО: Не идём дальше, если есть черновики
|
||||
} else {
|
||||
console.log('❌ Нет черновиков, идем дальше');
|
||||
nextStep(); // Нет черновиков, идем дальше
|
||||
// Нет черновиков - идём дальше
|
||||
nextStep();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Условие не выполнено, идем дальше');
|
||||
console.log('⚠️ Условие не выполнено для проверки черновиков:', {
|
||||
shouldCheckDrafts,
|
||||
selectedDraftId,
|
||||
finalUnifiedId,
|
||||
phone: formData.phone,
|
||||
isPhoneVerified
|
||||
});
|
||||
// Условие не выполнено - идём дальше
|
||||
nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
// ❌ ЭТОТ КОД НЕ ДОЛЖЕН ВЫПОЛНЯТЬСЯ, если есть return выше
|
||||
console.error('❌❌❌ КРИТИЧЕСКАЯ ОШИБКА: nextStep() вызван после return!');
|
||||
nextStep();
|
||||
}}
|
||||
onPrev={prevStep}
|
||||
isPhoneVerified={isPhoneVerified}
|
||||
setIsPhoneVerified={async (verified: boolean) => {
|
||||
setIsPhoneVerified={(verified: boolean) => {
|
||||
setIsPhoneVerified(verified);
|
||||
// После верификации проверяем черновики
|
||||
if (verified && formData.phone && !selectedDraftId) {
|
||||
const hasDraftsResult = await checkDrafts(formData.unified_id, formData.phone, sessionId);
|
||||
if (hasDraftsResult) {
|
||||
setCurrentStep(0); // Переходим к шагу выбора черновика
|
||||
}
|
||||
}
|
||||
// ❌ Убрано: проверка черновиков делается только в onNext
|
||||
// onNext вызывается после успешной верификации и содержит unified_id
|
||||
}}
|
||||
addDebugEvent={addDebugEvent}
|
||||
/>
|
||||
@@ -461,7 +681,7 @@ export default function ClaimForm() {
|
||||
description: 'Полис ERV',
|
||||
content: (
|
||||
<Step1Policy
|
||||
formData={{ ...formData, session_id: sessionId }} // ✅ claim_id уже в formData от n8n
|
||||
formData={{ ...formData, session_id: sessionIdRef.current }} // ✅ claim_id уже в formData от n8n
|
||||
updateFormData={updateFormData}
|
||||
onNext={nextStep}
|
||||
addDebugEvent={addDebugEvent}
|
||||
@@ -525,14 +745,14 @@ export default function ClaimForm() {
|
||||
});
|
||||
|
||||
return stepsArray;
|
||||
}, [formData, documentConfigs, isPhoneVerified, sessionId, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
|
||||
}, [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: sessionId,
|
||||
session_id: sessionIdRef.current,
|
||||
paymentMethod: 'sbp',
|
||||
});
|
||||
setCurrentStep(0);
|
||||
@@ -541,6 +761,41 @@ export default function ClaimForm() {
|
||||
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}>
|
||||
@@ -550,20 +805,42 @@ export default function ClaimForm() {
|
||||
title="Подать заявку на выплату"
|
||||
className="claim-form-card"
|
||||
extra={
|
||||
!isSubmitted && currentStep > 0 && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
background: '#fff',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
🔄 Начать заново
|
||||
</button>
|
||||
!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>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -585,7 +862,13 @@ export default function ClaimForm() {
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
<div className="steps-content">{steps[currentStep].content}</div>
|
||||
<div className="steps-content">
|
||||
{steps[currentStep] ? steps[currentStep].content : (
|
||||
<div style={{ padding: '40px 0', textAlign: 'center' }}>
|
||||
<p>Загрузка шага...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user