Files
aiform_prod/frontend/src/components/form/Step3Payment.tsx

496 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
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';
interface Bank {
bankid: string;
bankname: string;
}
interface Props {
formData: any;
updateFormData: (data: any) => void;
onPrev: () => void;
onSubmit: () => void;
isPhoneVerified: boolean;
setIsPhoneVerified: (verified: boolean) => void;
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
export default function Step3Payment({
formData,
updateFormData,
onPrev,
onSubmit,
isPhoneVerified,
setIsPhoneVerified,
addDebugEvent
}: Props) {
const [form] = Form.useForm();
const [codeSent, setCodeSent] = useState(false);
const [loading, setLoading] = useState(false);
const [verifyLoading, setVerifyLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [debugCode, setDebugCode] = useState<string | null>(formData.smsDebugCode ?? null);
const [banks, setBanks] = useState<Bank[]>([]);
const [banksLoading, setBanksLoading] = useState(false);
// Загрузка списка банков при монтировании компонента
useEffect(() => {
const loadBanks = async () => {
try {
setBanksLoading(true);
addDebugEvent?.('banks', 'pending', '📋 Загружаю список банков СБП...');
const response = await fetch(NSPK_BANKS_API);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const banksData: Bank[] = await response.json();
// Сортируем по названию для удобства
banksData.sort((a, b) => a.bankname.localeCompare(b.bankname, 'ru'));
setBanks(banksData);
addDebugEvent?.('banks', 'success', `✅ Загружено ${banksData.length} банков`, { count: banksData.length });
// Если есть сохранённый bankName или bankId - восстанавливаем значения
if (formData.bankName) {
const foundBank = banksData.find(b =>
b.bankname.toLowerCase() === formData.bankName.toLowerCase() ||
b.bankname.toLowerCase().includes(formData.bankName.toLowerCase())
);
if (foundBank) {
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
}
} else if (formData.bankId) {
// Если есть только bankId, находим по ID
const foundBank = banksData.find(b => b.bankid === formData.bankId);
if (foundBank) {
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
}
}
} catch (error: any) {
console.error('Ошибка загрузки банков:', error);
addDebugEvent?.('banks', 'error', `❌ Ошибка загрузки банков: ${error.message}`, { error: error.message });
message.error('Не удалось загрузить список банков. Попробуйте обновить страницу.');
} finally {
setBanksLoading(false);
}
};
loadBanks();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Загружаем банки только при монтировании
const sendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('Введите номер телефона');
return;
}
setLoading(true);
addDebugEvent?.('sms', 'pending', `📱 Отправляю SMS на ${phone}...`, { phone });
const response = await fetch(`${API_BASE_URL}/api/v1/sms/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
});
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ SMS отправлен (DEBUG mode)`, {
phone,
debug_code: result.debug_code,
message: result.message
});
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
if (result.debug_code) {
setDebugCode(result.debug_code);
updateFormData({ smsDebugCode: result.debug_code });
message.info(`DEBUG: Код ${result.debug_code}`);
}
} else {
addDebugEvent?.('sms', 'error', `❌ Ошибка SMS: ${result.detail}`, { error: result.detail });
message.error(result.detail || 'Ошибка отправки кода');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setLoading(false);
}
};
const verifyCode = async () => {
try {
const phone = form.getFieldValue('phone');
const code = form.getFieldValue('smsCode');
if (!code) {
message.error('Введите код из SMS');
return;
}
setVerifyLoading(true);
addDebugEvent?.('sms', 'pending', `🔐 Проверяю SMS код...`, { phone, code });
const response = await fetch(`${API_BASE_URL}/api/v1/sms/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code }),
});
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ Телефон подтвержден успешно`, {
phone,
verified: true
});
message.success('Телефон подтвержден!');
setDebugCode(null);
updateFormData({ smsDebugCode: undefined });
setIsPhoneVerified(true);
} else {
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, {
phone,
code,
error: result.detail
});
message.error(result.detail || 'Неверный код');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setVerifyLoading(false);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
updateFormData(values);
setSubmitting(true);
await onSubmit();
} catch (error) {
message.error('Заполните все обязательные поля');
} finally {
setSubmitting(false);
}
};
// Инициализация формы с bankId и bankName если есть
useEffect(() => {
if (formData.bankId || formData.bankName) {
form.setFieldsValue({
bankId: formData.bankId,
bankName: formData.bankName
});
}
}, [formData.bankId, formData.bankName, form]);
return (
<Form
form={form}
layout="vertical"
initialValues={{
...formData,
bankId: formData.bankId,
bankName: formData.bankName,
}}
style={{ marginTop: 24 }}
>
{/* Скрытые технические поля */}
<Form.Item name="clientIp" hidden>
<Input type="hidden" />
</Form.Item>
<Form.Item name="smsCode" hidden>
<Input type="hidden" />
</Form.Item>
{/* Кнопка Назад вверху */}
<div style={{ marginBottom: 16 }}>
<Button onClick={onPrev} size="large">
Назад
</Button>
</div>
{/* Блок верификации телефона */}
<div style={{
padding: 16,
background: '#f6f8fa',
borderRadius: 8,
marginBottom: 24
}}>
<h3 style={{ marginTop: 0 }}>📱 Подтверждение телефона</h3>
<Form.Item
label="Номер телефона"
name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="+79001234567"
disabled={isPhoneVerified}
maxLength={12}
size="large"
/>
</Form.Item>
<Form.Item
label="Электронная почта"
name="email"
rules={[
{ required: true, message: 'Введите email' },
{ type: 'email', message: 'Неверный формат email' }
]}
>
<Input
prefix={<MailOutlined />}
placeholder="example@mail.ru"
size="large"
type="email"
disabled={isPhoneVerified}
/>
</Form.Item>
{!isPhoneVerified && (
<>
<Form.Item>
<Button
type="primary"
onClick={sendCode}
loading={loading}
disabled={codeSent}
block
>
{codeSent ? 'Код отправлен' : 'Отправить код'}
</Button>
</Form.Item>
{codeSent && (
<Form.Item
label="Код из SMS"
name="smsCode"
rules={[
{ required: true, message: 'Введите код' },
{ len: 6, message: '6 цифр' }
]}
>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined />}
placeholder="123456"
maxLength={6}
style={{ width: '70%' }}
size="large"
/>
<Button
type="primary"
onClick={verifyCode}
loading={verifyLoading}
style={{ width: '30%' }}
size="large"
>
Проверить
</Button>
</Space.Compact>
</Form.Item>
)}
{debugCode && !isPhoneVerified && (
<div
style={{
marginTop: 8,
padding: 12,
background: '#fafafa',
borderRadius: 8,
border: '1px dashed #d9d9d9',
display: 'flex',
alignItems: 'center',
gap: 12,
}}
>
<span>
<strong>DEBUG код:</strong> {debugCode}
</span>
<Button
icon={<CopyOutlined />}
size="small"
onClick={() => {
navigator.clipboard.writeText(debugCode);
message.success('Код скопирован');
}}
>
Скопировать
</Button>
</div>
)}
</>
)}
{isPhoneVerified && (
<div style={{
padding: 12,
background: '#fafafa',
borderRadius: 8,
border: '1px solid #d9d9d9'
}}>
Телефон подтвержден
</div>
)}
</div>
{/* Блок выплаты (показывается только после верификации) */}
{isPhoneVerified && (
<>
<Divider />
<h3>💳 Способ получения выплаты</h3>
<Form.Item
label="Способ выплаты"
name="paymentMethod"
initialValue="sbp"
>
<div style={{
padding: '12px',
background: '#fafafa',
borderRadius: '8px',
border: '1px solid #d9d9d9'
}}>
<QrcodeOutlined style={{ fontSize: 20, color: '#595959', marginRight: 8 }} />
<strong>СБП (Система быстрых платежей)</strong>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: 13 }}>
Выплата поступит на ваш счет в течение нескольких минут
</p>
</div>
</Form.Item>
{/* Скрытое поле для bankId */}
<Form.Item name="bankId" hidden>
<Input />
</Form.Item>
<Form.Item
label="Банк для получения выплаты"
name="bankName"
rules={[
{ required: true, message: 'Выберите банк для получения выплаты' },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
);
if (!foundBank) {
return Promise.reject(new Error('Выберите банк из списка'));
}
return Promise.resolve();
}
}
]}
>
<AutoComplete
placeholder={banksLoading ? "Загрузка списка банков..." : "Начните вводить название банка"}
size="large"
loading={banksLoading}
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден. Попробуйте ввести другое название"}
options={banks.map((bank) => ({
value: bank.bankname,
label: bank.bankname,
}))}
filterOption={(inputValue, option) => {
if (!option?.label) return false;
return option.label.toLowerCase().includes(inputValue.toLowerCase());
}}
onSelect={(value) => {
// При выборе из списка находим банк и сохраняем оба поля
const selectedBank = banks.find(b => b.bankname === value);
if (selectedBank) {
updateFormData({
bankId: selectedBank.bankid,
bankName: selectedBank.bankname
});
// Устанавливаем bankId в скрытое поле
form.setFieldsValue({ bankId: selectedBank.bankid });
}
}}
onChange={(value) => {
// При вводе текста ищем точное совпадение по названию
if (typeof value === 'string') {
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
);
if (foundBank) {
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({ bankId: foundBank.bankid });
} else if (value === '') {
// Если поле очищено, очищаем и bankId
updateFormData({ bankId: undefined, bankName: undefined });
form.setFieldsValue({ bankId: undefined });
}
}
}}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev} size="large">Назад</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={submitting}
style={{ flex: 1 }}
size="large"
>
Отправить заявку
</Button>
</div>
</Form.Item>
{/* DEV MODE секция удалена для продакшена */}
</>
)}
</Form>
);
}