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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
24
frontend/src/vite-env.d.ts
vendored
24
frontend/src/vite-env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user