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:
AI Assistant
2025-11-25 11:40:32 +03:00
parent de56bc13cd
commit c9a2b95983

View File

@@ -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 {
@@ -16,6 +16,14 @@ export default function StepClaimConfirmation({
const [loading, setLoading] = useState(true);
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) {
@@ -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>
</>
);
}