feat: Telegram Mini App integration and UX improvements

- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
This commit is contained in:
AI Assistant
2026-01-29 16:12:48 +03:00
parent 73524465fd
commit 2e45786e46
57 changed files with 6776 additions and 234 deletions

View File

@@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clientright — защита прав потребителей</title>
<!-- Telegram SDK загружается динамически только при заходе из Telegram -->
</head>
<body>
<div id="root"></div>

View File

@@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clientright — защита прав потребителей</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<div id="root"></div>

View File

@@ -52,8 +52,9 @@ export default function Step1Phone({
setCodeSent(true);
updateFormData({ phone });
// 🔧 DEV MODE: показываем debug код в модалке
if (result.debug_code) {
// 🔧 DEV MODE: показываем debug код в модалке (только в development)
// В production debug_code не приходит с сервера, поэтому модалка не покажется
if (result.debug_code && import.meta.env.MODE === 'development') {
setDebugCode(result.debug_code);
setShowDebugModal(true);
}
@@ -341,7 +342,8 @@ export default function Step1Phone({
)}
</Form.Item>
{/* 🔧 DEV MODE: Модалка с SMS кодом */}
{/* 🔧 DEV MODE: Модалка с SMS кодом (только в development) */}
{import.meta.env.MODE === 'development' && (
<Modal
title="🔧 DEV MODE - SMS Код"
open={showDebugModal}
@@ -393,6 +395,7 @@ export default function Step1Phone({
</div>
</div>
</Modal>
)}
</Form>
);
}

View File

@@ -3,7 +3,8 @@ import { Form, Input, Button, AutoComplete, message, Space, Divider } from 'antd
import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined, CopyOutlined } from '@ant-design/icons';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8200';
const NSPK_BANKS_API = 'http://212.193.27.93/api/payouts/dictionaries/nspk-banks';
// API для получения списка банков СБП через backend (избегаем Mixed Content ошибок)
const NSPK_BANKS_API = `${API_BASE_URL}/api/v1/banks/nspk`;
interface Bank {
bankid: string;
@@ -51,10 +52,16 @@ export default function Step3Payment({
throw new Error(`HTTP ${response.status}`);
}
// Наш API возвращает формат: [{"bankId":"...","bankName":"..."}]
let banksData: Bank[] = await response.json();
// ✅ Фильтруем банки без названия
banksData = banksData.filter(bank => bank && bank.bankname && typeof bank.bankname === 'string');
// Преобразуем формат нашего API в наш внутренний формат
banksData = banksData
.filter((bank: any) => bank && bank.bankName && typeof bank.bankName === 'string')
.map((bank: any) => ({
bankid: bank.bankId || '',
bankname: bank.bankName
}));
// Сортируем по названию для удобства
banksData.sort((a, b) => {

View File

@@ -98,7 +98,8 @@ export default function StepClaimConfirmation({
false;
// Генерируем HTML форму здесь, на нашей стороне
const html = generateConfirmationFormHTML(formData, contact_data_confirmed);
const apiBaseUrl = import.meta.env.VITE_API_URL || 'https://aiform.clientright.ru';
const html = generateConfirmationFormHTML(formData, contact_data_confirmed, apiBaseUrl);
setHtmlContent(html);
setLoading(false);
}, [claimPlanData]);

View File

@@ -100,6 +100,7 @@ interface Props {
phone?: string;
session_id?: string;
unified_id?: string;
isTelegramMiniApp?: boolean; // ✅ Флаг Telegram Mini App
onSelectDraft: (claimId: string) => void;
onNewClaim: () => void;
onRestartDraft?: (claimId: string, description: string) => void; // Для legacy черновиков
@@ -175,6 +176,7 @@ export default function StepDraftSelection({
phone,
session_id,
unified_id,
isTelegramMiniApp,
onSelectDraft,
onNewClaim,
onRestartDraft,
@@ -211,7 +213,7 @@ export default function StepDraftSelection({
console.log('🔍 StepDraftSelection: ответ API:', data);
// Определяем legacy черновики (без documents_required в payload)
const processedDrafts = (data.drafts || []).map((draft: Draft) => {
let processedDrafts = (data.drafts || []).map((draft: Draft) => {
// Legacy только если:
// 1. Статус 'draft' (старый формат) ИЛИ
// 2. Нет новых статусов (draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready)
@@ -224,6 +226,12 @@ export default function StepDraftSelection({
};
});
// ✅ В Telegram Mini App скрываем заявки "В работе"
if (isTelegramMiniApp) {
processedDrafts = processedDrafts.filter((draft: Draft) => draft.status_code !== 'in_work');
console.log('🔍 Telegram Mini App: заявки "В работе" скрыты');
}
setDrafts(processedDrafts);
} catch (error) {
console.error('Ошибка загрузки черновиков:', error);
@@ -291,6 +299,27 @@ export default function StepDraftSelection({
// Кнопка действия
const getActionButton = (draft: Draft) => {
// Для заявок "В работе"
if (draft.status_code === 'in_work') {
// ✅ В веб-версии показываем кнопку "Просмотреть в Telegram"
if (!isTelegramMiniApp) {
return (
<Button
type="primary"
icon={<FileSearchOutlined />}
onClick={() => {
// Открываем Telegram бота
window.open('https://t.me/klientprav_bot', '_blank');
}}
>
Просмотреть в Telegram
</Button>
);
}
// ✅ В Telegram Mini App не показываем (но этот код не выполнится, т.к. заявки отфильтрованы)
return null;
}
const config = getStatusConfig(draft);
return (
@@ -521,7 +550,7 @@ export default function StepDraftSelection({
</Text>
{/* Кнопки действий */}
<div style={{
<div className="draft-actions" style={{
display: 'flex',
gap: 12,
marginTop: 12,
@@ -529,22 +558,25 @@ export default function StepDraftSelection({
borderTop: '1px solid #f0f0f0',
}}>
{getActionButton(draft)}
<Popconfirm
title="Удалить заявку?"
description="Это действие нельзя отменить"
onConfirm={() => handleDelete(draft.claim_id || draft.id)}
okText="Да, удалить"
cancelText="Отмена"
>
<Button
danger
icon={<DeleteOutlined />}
loading={deletingId === (draft.claim_id || draft.id)}
disabled={deletingId === (draft.claim_id || draft.id)}
>
Удалить
</Button>
</Popconfirm>
{/* Скрываем кнопку "Удалить" для заявок "В работе" */}
{draft.status_code !== 'in_work' && (
<Popconfirm
title="Удалить заявку?"
description="Это действие нельзя отменить"
onConfirm={() => handleDelete(draft.claim_id || draft.id)}
okText="Да, удалить"
cancelText="Отмена"
>
<Button
danger
icon={<DeleteOutlined />}
loading={deletingId === (draft.claim_id || draft.id)}
disabled={deletingId === (draft.claim_id || draft.id)}
>
Удалить
</Button>
</Popconfirm>
)}
</div>
</Space>
}

View File

@@ -50,6 +50,7 @@ interface Props {
updateFormData: (data: any) => void;
onNext: () => void;
onPrev: () => void;
backToDraftsList?: () => void; // ✅ Возврат к списку черновиков напрямую
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
@@ -110,6 +111,7 @@ export default function StepWizardPlan({
updateFormData,
onNext,
onPrev,
backToDraftsList,
addDebugEvent,
}: Props) {
console.log('🔥 StepWizardPlan v1.4 - 2025-11-20 15:00 - Add unified_id and claim_id to wizard payload');
@@ -2271,7 +2273,7 @@ export default function StepWizardPlan({
{/* Кнопки */}
<Space style={{ marginTop: 16 }}>
<Button onClick={onPrev}> К списку заявок</Button>
<Button onClick={backToDraftsList || onPrev}> К списку заявок</Button>
<Button
type="primary"
onClick={handleDocContinue}

View File

@@ -1,7 +1,10 @@
// Функция генерации HTML формы подтверждения заявления
// Основана на структуре из n8n Code node "Mini-app Подтверждение данных"
export function generateConfirmationFormHTML(data: any, contact_data_confirmed: boolean = false): string {
export function generateConfirmationFormHTML(data: any, contact_data_confirmed: boolean = false, apiBaseUrl?: string): string {
// API URL для загрузки банков (избегаем Mixed Content)
const API_BASE_URL = apiBaseUrl || (typeof window !== 'undefined' && (window as any).API_BASE_URL) || 'https://aiform.clientright.ru';
const BANKS_API_URL = `${API_BASE_URL}/api/v1/banks/nspk`;
// Извлекаем SMS данные (до нормализации, так как структура может быть разной)
const smsInputData = {
prefix: data.sms_meta?.prefix || data.prefix || '',
@@ -1667,7 +1670,9 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
console.log('Loading NSPK banks...');
fetch('http://212.193.27.93/api/payouts/dictionaries/nspk-banks')
// Используем backend endpoint через HTTPS (избегаем Mixed Content)
var banksApiUrl = ${JSON.stringify(BANKS_API_URL)};
fetch(banksApiUrl)
.then(function(response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();

View File

@@ -1,3 +1,4 @@
/* ========== ВЕБ (дефолт): как в aiform_dev ========== */
.claim-form-container {
min-height: 100vh;
padding: 40px 20px;
@@ -51,3 +52,76 @@
}
}
/* ========== Telegram Mini App: отдельный компактный скин ========== */
.claim-form-container.telegram-mini-app {
min-height: 100vh;
min-height: 100dvh;
padding: 12px 10px max(16px, env(safe-area-inset-bottom));
align-items: flex-start;
justify-content: flex-start;
}
.claim-form-container.telegram-mini-app .claim-form-card {
max-width: 100%;
box-shadow: none;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.claim-form-container.telegram-mini-app .claim-form-card .ant-card-head {
padding: 10px 12px;
min-height: auto;
}
.claim-form-container.telegram-mini-app .claim-form-card .ant-card-head-title {
font-size: 16px;
font-weight: 600;
line-height: 1.3;
white-space: normal;
}
.claim-form-container.telegram-mini-app .claim-form-card .ant-card-body {
padding: 12px;
}
.claim-form-container.telegram-mini-app .steps {
margin-bottom: 16px;
}
.claim-form-container.telegram-mini-app .steps .ant-steps-item-title {
font-size: 12px;
line-height: 1.2;
}
.claim-form-container.telegram-mini-app .steps .ant-steps-item-description {
font-size: 11px;
}
.claim-form-container.telegram-mini-app .steps-content {
min-height: 280px;
padding: 8px 4px 12px;
}
.claim-form-container.telegram-mini-app .ant-btn {
font-size: 14px;
}
.claim-form-container.telegram-mini-app .ant-input,
.claim-form-container.telegram-mini-app .ant-select-selector {
font-size: 16px;
}
.claim-form-container.telegram-mini-app .ant-card-extra .ant-space-item .ant-btn,
.claim-form-container.telegram-mini-app .ant-card-extra button {
padding: 6px 10px;
font-size: 13px;
}
/* Кнопки действий в черновиках - вертикально в Telegram */
.claim-form-container.telegram-mini-app .draft-actions {
flex-direction: column !important;
}
.claim-form-container.telegram-mini-app .draft-actions .ant-btn {
width: 100%;
}

View File

@@ -1,5 +1,5 @@
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { Steps, Card, message, Row, Col, Space } from 'antd';
import { Steps, Card, message, Row, Col, Space, Spin } from 'antd';
import Step1Phone from '../components/form/Step1Phone';
import StepDescription from '../components/form/StepDescription';
// Step1Policy убран - старый ERV флоу
@@ -7,7 +7,7 @@ import StepDraftSelection from '../components/form/StepDraftSelection';
import StepWizardPlan from '../components/form/StepWizardPlan';
import StepClaimConfirmation from '../components/form/StepClaimConfirmation';
// Step2EventType, StepDocumentUpload убраны - старый ERV флоу
import Step3Payment from '../components/form/Step3Payment';
// Step3Payment убран - не используется
import DebugPanel from '../components/DebugPanel';
// getDocumentsForEventType убран - старый ERV флоу
import './ClaimForm.css';
@@ -105,14 +105,173 @@ export default function ClaimForm() {
const [showDraftSelection, setShowDraftSelection] = useState(false);
const [selectedDraftId, setSelectedDraftId] = useState<string | null>(null);
const [hasDrafts, setHasDrafts] = useState(false);
const [telegramAuthChecked, setTelegramAuthChecked] = useState(false);
/** Статус Telegram auth — показываем на странице, т.к. консоль Mini App отдельная */
const [tgDebug, setTgDebug] = useState<string>('');
/** Дефолт = веб. Скин TG подставляется только при заходе через Telegram Mini App. */
const [isTelegramMiniApp, setIsTelegramMiniApp] = useState(false);
useEffect(() => {
// 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился!
console.log('🔥 ClaimForm v3.9 - 2025-12-29 - Auto redirect to drafts after success');
}, []);
// ✅ Восстановление сессии при загрузке страницы
// Определение: зашли с веба или из Telegram Mini App. Дефолт — веб; при TG вешаем класс для отдельного скина.
// Загружаем telegram-web-app.js только если есть признаки Telegram (чтобы не мусорить в консоли).
useEffect(() => {
const isTelegramContext = () => {
// Проверяем URL, referrer и user agent на признаки Telegram
const url = window.location.href;
const ref = document.referrer;
const ua = navigator.userAgent;
return (
url.includes('tgWebAppData') ||
url.includes('tgWebAppVersion') ||
ref.includes('telegram') ||
ua.includes('Telegram')
);
};
if (isTelegramContext()) {
// Загружаем скрипт Telegram SDK динамически
const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-web-app.js';
script.async = true;
script.onload = () => {
setTimeout(() => {
const tg = (window as any).Telegram;
const webApp = tg?.WebApp;
const hasInitData = webApp?.initData && webApp.initData.length > 0;
if (webApp && hasInitData) {
setIsTelegramMiniApp(true);
try {
webApp.ready?.();
webApp.expand?.();
} catch (_) {}
}
}, 100);
};
document.head.appendChild(script);
}
}, []);
// ✅ Telegram Mini App: попытка авторизоваться через initData при первом заходе
useEffect(() => {
const tryTelegramAuth = async () => {
try {
// Только window: parent недоступен из-за cross-origin (iframe Telegram)
const getTg = () => (window as any).Telegram;
// Ждём появления initData: скрипт Telegram может подгрузиться с задержкой
const maxWaitMs = 2500;
const intervalMs = 150;
let webApp: TelegramWebApp | null = null;
let attempts = 0;
while (attempts * intervalMs < maxWaitMs) {
const tg = getTg();
webApp = tg?.WebApp ?? null;
if (webApp?.initData) {
console.log('[TG] initData появился через', attempts * intervalMs, 'ms, длина=', webApp.initData.length);
break;
}
attempts++;
await new Promise((r) => setTimeout(r, intervalMs));
}
if (!webApp?.initData) {
const tg = getTg();
console.log('[TG] После ожидания', maxWaitMs, 'ms: Telegram=', !!tg, 'WebApp=', !!tg?.WebApp, 'initData=', !!tg?.WebApp?.initData, '→ пропускаем tg/auth');
setTelegramAuthChecked(true);
return;
}
// Логирование для отладки
if (webApp.initDataUnsafe?.user) {
const u = webApp.initDataUnsafe.user;
console.log('[TG] initDataUnsafe.user:', { id: u.id, username: u.username, first_name: u.first_name });
}
// Если сессия уже есть в localStorage — ничего не делаем, дальше сработает обычное restoreSession
const existingToken = localStorage.getItem('session_token');
if (existingToken) {
setTgDebug('TG: session_token уже есть → tg/auth не вызываем');
console.log('[TG] session_token уже в localStorage → tg/auth не вызываем');
setTelegramAuthChecked(true);
return;
}
setTgDebug('TG: POST /api/v1/tg/auth...');
console.log('[TG] Вызываем POST /api/v1/tg/auth, initData длина=', webApp.initData.length);
const response = await fetch('/api/v1/tg/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
init_data: webApp.initData,
}),
});
const data = await response.json();
console.log('[TG] /api/v1/tg/auth ответ: status=', response.status, 'ok=', response.ok, 'data=', data);
if (!response.ok || !data.success) {
console.warn('[TG] Telegram auth не успешен → показываем экран телефона/SMS. detail=', data.detail || data);
setTelegramAuthChecked(true);
return;
}
const sessionToken = data.session_token;
// Сохраняем session_token так же, как после SMS-логина
if (sessionToken) {
localStorage.setItem('session_token', sessionToken);
sessionIdRef.current = sessionToken;
}
// Сохраняем базовые данные пользователя (phone может быть пустым)
setFormData((prev) => ({
...prev,
unified_id: data.unified_id,
phone: data.phone,
contact_id: data.contact_id,
session_id: sessionToken,
}));
// Помечаем телефон как уже "подтверждённый" для Telegram-флоу
setIsPhoneVerified(true);
// Если n8n сразу сообщил о наличии черновиков — показываем экран выбора
if (data.has_drafts) {
console.log('🤖 Telegram auth: has_drafts=true, переходим на экран черновиков');
setShowDraftSelection(true);
setHasDrafts(true);
setCurrentStep(0);
} else {
// Иначе переходим сразу к описанию проблемы
console.log('🤖 Telegram auth: черновиков нет, переходим к описанию проблемы');
setCurrentStep(1);
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
setTgDebug(`TG: ошибка: ${msg}`);
console.error('[TG] Ошибка при tg/auth (сеть или парсинг):', error);
} finally {
setTelegramAuthChecked(true);
}
};
tryTelegramAuth();
}, []);
// ✅ Восстановление сессии при загрузке страницы (после попытки Telegram auth)
useEffect(() => {
if (!telegramAuthChecked) {
// Ждём, пока не закончим попытку Telegram-авторизации,
// чтобы не гонять два параллельных restoreSession.
return;
}
const restoreSession = async () => {
console.log('🔑 🔑 🔑 НАЧАЛО ВОССТАНОВЛЕНИЯ СЕССИИ 🔑 🔑 🔑');
console.log('🔑 Все ключи в localStorage:', Object.keys(localStorage));
@@ -180,12 +339,12 @@ export default function ClaimForm() {
});
});
message.success(`Добро пожаловать! Сессия восстановлена (${data.phone})`);
message.success('Добро пожаловать!');
addDebugEvent('session', 'success', '✅ Сессия восстановлена, найдены черновики');
} else {
// Нет черновиков - переходим к описанию
setCurrentStep(1);
message.success(`Добро пожаловать! Сессия восстановлена (${data.phone})`);
message.success('Добро пожаловать!');
addDebugEvent('session', 'success', '✅ Сессия восстановлена');
}
} else {
@@ -204,7 +363,7 @@ export default function ClaimForm() {
};
restoreSession();
}, []); // Запускаем только при загрузке
}, [telegramAuthChecked]); // Запускаем только один раз, после попытки Telegram auth
// Получаем IP клиента один раз при монтировании
useEffect(() => {
@@ -277,8 +436,24 @@ export default function ClaimForm() {
console.log('⏪ prevStep called');
setCurrentStep((prev) => {
console.log('📍 Current step:', prev, '→ Prev:', prev - 1);
// ✅ Если возвращаемся к шагу 0 и есть черновики - показываем список
if (prev - 1 === 0 && formData.unified_id && hasDrafts) {
console.log('📍 Возврат к списку черновиков');
setShowDraftSelection(true);
setSelectedDraftId(null);
}
return prev - 1;
});
}, [formData.unified_id, hasDrafts]);
// ✅ Возврат к списку черновиков напрямую (без промежуточных шагов)
const backToDraftsList = useCallback(() => {
console.log('📋 Возврат к списку черновиков');
setShowDraftSelection(true);
setSelectedDraftId(null);
setCurrentStep(0);
}, []);
// Преобразование данных черновика в формат propertyName для формы подтверждения
@@ -624,6 +799,13 @@ export default function ClaimForm() {
const hasDocuments = Array.isArray(documentsMeta) && documentsMeta.length > 0;
const isDraft = claim.status_code === 'draft';
// ✅ Запрещаем редактирование заявок "В работе"
if (claim.status_code === 'in_work') {
message.warning('Эта заявка уже в работе и не может быть изменена');
console.log('⚠️ Попытка открыть заявку "В работе" для редактирования - запрещено');
return;
}
// ✅ НОВОЕ: Проверяем наличие form_draft (собранные данные из RAG)
const formDraft = payload.form_draft;
const hasFormDraft = !!(formDraft && formDraft.user && formDraft.offenders);
@@ -1126,6 +1308,7 @@ export default function ClaimForm() {
phone={formData.phone || ''}
session_id={sessionIdRef.current}
unified_id={formData.unified_id} // ✅ Передаём unified_id
isTelegramMiniApp={isTelegramMiniApp} // ✅ Передаём флаг Telegram
onSelectDraft={handleSelectDraft}
onNewClaim={handleNewClaim}
/>
@@ -1241,6 +1424,7 @@ export default function ClaimForm() {
updateFormData={updateFormData}
onPrev={prevStep}
onNext={nextStep}
backToDraftsList={backToDraftsList}
addDebugEvent={addDebugEvent}
/>
),
@@ -1262,48 +1446,75 @@ export default function ClaimForm() {
/>
),
});
} else {
// ✅ СТАРЫЙ ФЛОУ: Step3Payment (только если нет StepClaimConfirmation)
// Используется как fallback, если данные claim:plan не получены
stepsArray.push({
title: 'Заявление',
description: 'Подтверждение',
content: (
<Step3Payment
formData={formData}
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
});
}
// Step3Payment убран - не используется
return stepsArray;
}, [formData, isPhoneVerified, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
}, [formData, isPhoneVerified, nextStep, prevStep, backToDraftsList, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
const handleReset = () => {
console.log('🔄 Начать заново - возврат к списку черновиков');
// ✅ Генерируем новую сессию для новой заявки (но сохраняем авторизацию)
const newSessionId = 'sess_' + generateUUIDv4();
sessionIdRef.current = newSessionId;
setIsSubmitted(false);
setFormData({
setShowDraftSelection(false);
setSelectedDraftId(null);
// ✅ Очищаем данные формы, НО сохраняем авторизацию (unified_id, phone, contact_id, isPhoneVerified)
updateFormData({
session_id: newSessionId,
claim_id: undefined,
voucher: '',
claim_id: undefined, // ✅ Очищаем для новой заявки
session_id: sessionIdRef.current,
paymentMethod: 'sbp',
problemDescription: undefined,
wizardPlan: undefined,
wizardAnswers: undefined,
wizardPrefill: undefined,
wizardPrefillArray: undefined,
wizardCoverageReport: undefined,
wizardUploads: undefined,
wizardSkippedDocuments: undefined,
eventType: undefined,
// ✅ unified_id, phone, contact_id, isPhoneVerified НЕ очищаем
});
setCurrentStep(0);
setIsPhoneVerified(false);
// ✅ Проверяем черновики и возвращаемся к списку
if (formData.unified_id && hasDrafts) {
console.log('🔄 Есть черновики - показываем список');
setShowDraftSelection(true);
setCurrentStep(0);
} else {
console.log('🔄 Нет черновиков - переходим к новой заявке');
setCurrentStep(1); // StepDescription
}
message.info('Форма сброшена');
addDebugEvent('system', 'info', '🔄 Форма сброшена');
};
// Обработчик кнопки "Выход" - завершить сессию и вернуться к Step1Phone
// Обработчик кнопки "Выход"
const handleExitToList = useCallback(async () => {
console.log('🚪 Выход из системы');
addDebugEvent('system', 'info', '🚪 Выход из системы');
// ✅ В Telegram Mini App — просто закрываем приложение
if (isTelegramMiniApp) {
try {
const tg = (window as any).Telegram;
const webApp = tg?.WebApp;
if (webApp && typeof webApp.close === 'function') {
webApp.close();
}
} catch (error) {
console.warn('⚠️ Ошибка при закрытии Telegram Mini App:', error);
}
return;
}
// ✅ В обычном веб — полный сброс сессии и возврат к Step1Phone
// Получаем session_token из localStorage
const sessionToken = localStorage.getItem('session_token') || formData.session_id;
@@ -1328,42 +1539,50 @@ export default function ClaimForm() {
// Удаляем session_token из localStorage
localStorage.removeItem('session_token');
// Полный сброс: очищаем все данные авторизации и черновиков
// Полный сброс: очищаем все данные авторизации и черновиков
setIsSubmitted(false);
setShowDraftSelection(false);
setHasDrafts(false);
setSelectedDraftId(null);
// Генерируем новую сессию для нового пользователя
// Генерируем новую сессию для нового пользователя
const newSessionId = `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
sessionIdRef.current = newSessionId;
// Полностью очищаем formData, включая unified_id и phone
// Полностью очищаем formData, включая unified_id и phone
setFormData({
voucher: '',
claim_id: undefined,
session_id: newSessionId,
paymentMethod: 'sbp',
unified_id: undefined, // ✅ Очищаем unified_id
phone: undefined, // ✅ Очищаем phone
contact_id: undefined, // ✅ Очищаем contact_id
unified_id: undefined,
phone: undefined,
contact_id: undefined,
is_new_contact: undefined,
isPhoneVerified: false,
});
// Сбрасываем флаг верификации телефона
// Сбрасываем флаг верификации телефона
setIsPhoneVerified(false);
// Переходим на экран входа (Step1Phone)
// Если showDraftSelection = false и нет unified_id, то шаг 0 будет Step1Phone
// Переходим на экран входа (Step1Phone)
setCurrentStep(0);
message.info('Сессия завершена. До свидания!');
addDebugEvent('system', 'info', '🔄 Форма сброшена');
}, [formData.session_id, addDebugEvent]);
}, [formData.session_id, addDebugEvent, isTelegramMiniApp]);
// ✅ Показываем loader пока идёт проверка Telegram auth и восстановление сессии
if (!telegramAuthChecked || !sessionRestored) {
return (
<div className={`claim-form-container ${isTelegramMiniApp ? 'telegram-mini-app' : ''}`} style={{ padding: '20px', background: '#ffffff', display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<Spin size="large" tip="Загрузка..." />
</div>
);
}
return (
<div className="claim-form-container" style={{ padding: '20px', background: '#ffffff' }}>
<div className={`claim-form-container ${isTelegramMiniApp ? 'telegram-mini-app' : ''}`} style={{ padding: '20px', background: '#ffffff' }}>
<Row gutter={16}>
{/* Левая часть - Форма (в проде на всю ширину, в деве 14 из 24) */}
<Col xs={24} lg={process.env.NODE_ENV === 'development' ? 14 : 24}>

View File

@@ -5,4 +5,28 @@ declare module '*.svg' {
export default content;
}
interface TelegramWebAppUser {
id: number;
first_name?: string;
last_name?: string;
username?: string;
language_code?: string;
}
interface TelegramWebApp {
initData: string;
initDataUnsafe: {
user?: TelegramWebAppUser;
[key: string]: any;
};
}
interface TelegramNamespace {
WebApp?: TelegramWebApp;
}
interface Window {
Telegram?: TelegramNamespace;
}