feat: Add SMS approval before form submission
Implemented SMS verification modal before saving confirmed form data: - Added SMS modal component in StepClaimConfirmation.tsx - Intercept claim_confirmed event and show SMS modal - Send SMS code automatically when form is confirmed - Verify SMS code before proceeding to save data - Phone number extracted from claimPlanData (applicant.phone, user.mobile, or phone) - Added saveFormData placeholder for future implementation Features: - Modal with SMS code input field - Auto-send SMS code on form confirmation - Code validation (6 digits, numbers only) - Resend code functionality - Cancel option - Proper error handling TODO: Implement saveFormData function to save form data to backend/n8n Files: - frontend/src/components/form/StepClaimConfirmation.tsx
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Card, Spin, message } from 'antd';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Card, Spin, message, Modal, Input, Button, Form } from 'antd';
|
||||
import { generateConfirmationFormHTML } from './generateConfirmationFormHTML';
|
||||
|
||||
interface Props {
|
||||
@@ -17,6 +17,14 @@ export default function StepClaimConfirmation({
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||
|
||||
// SMS Approval state
|
||||
const [smsModalVisible, setSmsModalVisible] = useState(false);
|
||||
const [smsCodeSent, setSmsCodeSent] = useState(false);
|
||||
const [smsLoading, setSmsLoading] = useState(false);
|
||||
const [smsVerifyLoading, setSmsVerifyLoading] = useState(false);
|
||||
const [pendingFormData, setPendingFormData] = useState<any>(null);
|
||||
const [smsForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (!claimPlanData) {
|
||||
message.error('Данные заявления не получены');
|
||||
@@ -84,6 +92,79 @@ export default function StepClaimConfirmation({
|
||||
setLoading(false);
|
||||
}, [claimPlanData]);
|
||||
|
||||
// Функция сохранения данных формы (TODO: реализовать сохранение)
|
||||
const saveFormData = useCallback(async (formData: any) => {
|
||||
console.log('💾 Сохраняем данные формы:', formData);
|
||||
// TODO: Реализовать сохранение данных в бэкенд/n8n
|
||||
// Здесь будет вызов API для сохранения отредактированных данных формы
|
||||
}, []);
|
||||
|
||||
// Функция отправки SMS-кода
|
||||
const sendSMSCode = useCallback(async (phone: string) => {
|
||||
try {
|
||||
setSmsLoading(true);
|
||||
// SMS API ожидает телефон в формате +79001234567
|
||||
const phoneWithPlus = phone.startsWith('+') ? phone : `+${phone}`;
|
||||
|
||||
const response = await fetch('/api/v1/sms/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ phone: phoneWithPlus }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
message.success('Код отправлен на ваш телефон');
|
||||
setSmsCodeSent(true);
|
||||
if (result.debug_code) {
|
||||
message.info(`DEBUG: Код ${result.debug_code}`);
|
||||
}
|
||||
} else {
|
||||
message.error(result.detail || 'Ошибка отправки кода');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Ошибка соединения с сервером');
|
||||
} finally {
|
||||
setSmsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Функция проверки SMS-кода
|
||||
const verifySMSCode = useCallback(async (phone: string, code: string) => {
|
||||
try {
|
||||
setSmsVerifyLoading(true);
|
||||
// SMS API ожидает телефон в формате +79001234567
|
||||
const phoneWithPlus = phone.startsWith('+') ? phone : `+${phone}`;
|
||||
|
||||
const response = await fetch('/api/v1/sms/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ phone: phoneWithPlus, code }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
message.success('Код подтвержден!');
|
||||
// Закрываем модалку и продолжаем с сохранением данных
|
||||
setSmsModalVisible(false);
|
||||
setSmsCodeSent(false);
|
||||
smsForm.resetFields();
|
||||
|
||||
// Сохраняем данные и переходим дальше
|
||||
await saveFormData(pendingFormData);
|
||||
onNext();
|
||||
} else {
|
||||
message.error(result.detail || 'Неверный код');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Ошибка соединения с сервером');
|
||||
} finally {
|
||||
setSmsVerifyLoading(false);
|
||||
}
|
||||
}, [pendingFormData, saveFormData, smsForm, onNext]);
|
||||
|
||||
useEffect(() => {
|
||||
// Слушаем сообщения от iframe
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
@@ -91,13 +172,29 @@ export default function StepClaimConfirmation({
|
||||
|
||||
if (event.data.type === 'claim_confirmed') {
|
||||
console.log('✅ Заявление подтверждено с данными:', event.data.data);
|
||||
message.success('Заявление подтверждено!');
|
||||
// Здесь можно сохранить отредактированные данные перед переходом дальше
|
||||
if (event.data.data) {
|
||||
// Данные формы можно передать дальше через updateFormData или сохранить
|
||||
console.log('📋 Отредактированные данные формы:', event.data.data);
|
||||
|
||||
// Сохраняем данные формы для последующего сохранения после SMS-апрува
|
||||
setPendingFormData(event.data.data);
|
||||
|
||||
// Получаем телефон пользователя для отправки SMS
|
||||
const phone =
|
||||
claimPlanData?.propertyName?.applicant?.phone ||
|
||||
claimPlanData?.propertyName?.user?.mobile ||
|
||||
claimPlanData?.phone ||
|
||||
'';
|
||||
|
||||
if (!phone) {
|
||||
message.error('Не удалось определить номер телефона для подтверждения');
|
||||
return;
|
||||
}
|
||||
onNext();
|
||||
|
||||
// Показываем модалку SMS-апрува
|
||||
setSmsModalVisible(true);
|
||||
setSmsCodeSent(false);
|
||||
smsForm.resetFields();
|
||||
|
||||
// Автоматически отправляем SMS-код
|
||||
sendSMSCode(phone);
|
||||
} else if (event.data.type === 'claim_cancelled') {
|
||||
message.info('Подтверждение отменено');
|
||||
onPrev();
|
||||
@@ -134,7 +231,7 @@ export default function StepClaimConfirmation({
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, [onNext, onPrev]);
|
||||
}, [onNext, onPrev, sendSMSCode, claimPlanData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -147,33 +244,177 @@ export default function StepClaimConfirmation({
|
||||
);
|
||||
}
|
||||
|
||||
// Обработчик отправки SMS-кода из модалки
|
||||
const handleSendCode = useCallback(async () => {
|
||||
const phone =
|
||||
claimPlanData?.propertyName?.applicant?.phone ||
|
||||
claimPlanData?.propertyName?.user?.mobile ||
|
||||
claimPlanData?.phone ||
|
||||
'';
|
||||
|
||||
if (!phone) {
|
||||
message.error('Не удалось определить номер телефона');
|
||||
return;
|
||||
}
|
||||
|
||||
await sendSMSCode(phone);
|
||||
}, [claimPlanData, sendSMSCode]);
|
||||
|
||||
// Обработчик проверки SMS-кода из модалки
|
||||
const handleVerifyCode = useCallback(async () => {
|
||||
try {
|
||||
const values = await smsForm.validateFields();
|
||||
const phone =
|
||||
claimPlanData?.propertyName?.applicant?.phone ||
|
||||
claimPlanData?.propertyName?.user?.mobile ||
|
||||
claimPlanData?.phone ||
|
||||
'';
|
||||
|
||||
if (!phone) {
|
||||
message.error('Не удалось определить номер телефона');
|
||||
return;
|
||||
}
|
||||
|
||||
await verifySMSCode(phone, values.code);
|
||||
} catch (error) {
|
||||
// Валидация не прошла
|
||||
}
|
||||
}, [claimPlanData, smsForm, verifySMSCode]);
|
||||
|
||||
// Обработчик отмены SMS-апрува
|
||||
const handleCancelSMS = () => {
|
||||
setSmsModalVisible(false);
|
||||
setSmsCodeSent(false);
|
||||
setPendingFormData(null);
|
||||
smsForm.resetFields();
|
||||
message.info('Подтверждение отменено');
|
||||
};
|
||||
|
||||
const phone =
|
||||
claimPlanData?.propertyName?.applicant?.phone ||
|
||||
claimPlanData?.propertyName?.user?.mobile ||
|
||||
claimPlanData?.phone ||
|
||||
'';
|
||||
const displayPhone = phone ? (phone.length > 4 ? `${phone.slice(0, -4)}****` : '****') : '****';
|
||||
|
||||
return (
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
height: 'calc(100vh - 200px)',
|
||||
minHeight: '800px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={htmlContent}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '800px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
flex: 1,
|
||||
<>
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
height: 'calc(100vh - 200px)',
|
||||
minHeight: '800px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}
|
||||
}}
|
||||
title="Форма подтверждения заявления"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
||||
/>
|
||||
</Card>
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={htmlContent}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '800px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
flex: 1,
|
||||
}}
|
||||
title="Форма подтверждения заявления"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Модальное окно SMS-апрува */}
|
||||
<Modal
|
||||
title="Подтверждение отправки заявления"
|
||||
open={smsModalVisible}
|
||||
onCancel={handleCancelSMS}
|
||||
footer={null}
|
||||
closable={!smsCodeSent}
|
||||
maskClosable={!smsCodeSent}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<p style={{ marginBottom: '16px', fontSize: '14px', color: '#666' }}>
|
||||
Для завершения отправки заявления необходимо подтвердить номер телефона.
|
||||
</p>
|
||||
|
||||
{phone && (
|
||||
<p style={{ marginBottom: '16px', fontSize: '14px' }}>
|
||||
Код отправлен на номер: <strong>{displayPhone}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!smsCodeSent ? (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={smsLoading}
|
||||
onClick={handleSendCode}
|
||||
block
|
||||
>
|
||||
Отправить код подтверждения
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Form
|
||||
form={smsForm}
|
||||
layout="vertical"
|
||||
onFinish={handleVerifyCode}
|
||||
>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="Введите код из SMS"
|
||||
rules={[
|
||||
{ required: true, message: 'Введите код' },
|
||||
{ len: 6, message: 'Код должен состоять из 6 цифр' },
|
||||
{ pattern: /^\d+$/, message: 'Код должен содержать только цифры' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="000000"
|
||||
maxLength={6}
|
||||
style={{ fontSize: '18px', textAlign: 'center', letterSpacing: '4px' }}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Button
|
||||
onClick={handleCancelSMS}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={smsVerifyLoading}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: '8px' }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleSendCode}
|
||||
loading={smsLoading}
|
||||
size="small"
|
||||
>
|
||||
Отправить код повторно
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user