496 lines
17 KiB
TypeScript
496 lines
17 KiB
TypeScript
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>
|
||
);
|
||
}
|