feat: добавлен выбор банка для СБП выплат
Frontend: - Динамическая загрузка 226 банков из NSPK API - Выбор банка добавлен в Step3Payment (новая заявка) - Выбор банка добавлен в generateConfirmationFormHTML (редактирование) - Поля: bank_id (ID из NSPK) и bank_name (название для отображения) Backend: - Добавлено поле bank_id в ClaimCreateRequest API: - http://212.193.27.93/api/payouts/dictionaries/nspk-banks Изменения: - ticket_form/frontend/src/components/form/Step3Payment.tsx - ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts - ticket_form/frontend/src/pages/ClaimForm.tsx - ticket_form/backend/app/api/models.py
This commit is contained in:
@@ -44,7 +44,8 @@ class ClaimCreateRequest(BaseModel):
|
||||
|
||||
# Шаг 3: Данные для выплаты
|
||||
payment_method: str = "sbp" # "sbp", "card", "bank_transfer"
|
||||
bank_name: Optional[str] = None
|
||||
bank_id: Optional[str] = None # ID банка из NSPK API (bankid)
|
||||
bank_name: Optional[str] = None # Название банка для отображения
|
||||
card_number: Optional[str] = None
|
||||
account_number: Optional[str] = None
|
||||
|
||||
|
||||
@@ -432,3 +432,7 @@ return [
|
||||
- `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` — шаблон формы заявления
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -98,3 +98,7 @@ check_all_ready (FALSE) → (конец)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,3 +31,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,3 +20,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8927
ticket_form/frontend/package-lock.json
generated
Normal file
8927
ticket_form/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Form, Input, Button, Select, 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';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface Bank {
|
||||
bankid: string;
|
||||
bankname: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
formData: any;
|
||||
updateFormData: (data: any) => void;
|
||||
@@ -31,6 +37,53 @@ export default function Step3Payment({
|
||||
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 && !formData.bankId) {
|
||||
const foundBank = banksData.find(b =>
|
||||
b.bankname.toLowerCase() === formData.bankName.toLowerCase() ||
|
||||
b.bankname.toLowerCase().includes(formData.bankName.toLowerCase())
|
||||
);
|
||||
if (foundBank) {
|
||||
updateFormData({ bankId: foundBank.bankid });
|
||||
form.setFieldsValue({ bankId: foundBank.bankid });
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
@@ -136,11 +189,21 @@ export default function Step3Payment({
|
||||
}
|
||||
};
|
||||
|
||||
// Инициализация формы с bankId если есть
|
||||
useEffect(() => {
|
||||
if (formData.bankId) {
|
||||
form.setFieldsValue({ bankId: formData.bankId });
|
||||
}
|
||||
}, [formData.bankId, form]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={formData}
|
||||
initialValues={{
|
||||
...formData,
|
||||
bankId: formData.bankId || formData.bankName, // Fallback на bankName для совместимости
|
||||
}}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
{/* Скрытые технические поля */}
|
||||
@@ -316,31 +379,37 @@ export default function Step3Payment({
|
||||
|
||||
<Form.Item
|
||||
label="Выберите ваш банк"
|
||||
name="bankName"
|
||||
name="bankId"
|
||||
rules={[{ required: true, message: 'Выберите банк для получения выплаты' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Выберите банк"
|
||||
placeholder={banksLoading ? "Загрузка списка банков..." : "Выберите банк"}
|
||||
size="large"
|
||||
showSearch
|
||||
loading={banksLoading}
|
||||
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден"}
|
||||
filterOption={(input: string, option: any) => {
|
||||
const children = option?.children;
|
||||
if (typeof children === 'string') {
|
||||
return children.toLowerCase().includes(input.toLowerCase());
|
||||
const label = option?.label || option?.children;
|
||||
if (typeof label === 'string') {
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onChange={(value) => {
|
||||
const selectedBank = banks.find(b => b.bankid === value);
|
||||
if (selectedBank) {
|
||||
updateFormData({
|
||||
bankId: selectedBank.bankid,
|
||||
bankName: selectedBank.bankname
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Option value="sberbank">🟢 Сбербанк</Option>
|
||||
<Option value="tinkoff">🟡 Тинькофф</Option>
|
||||
<Option value="vtb">🔵 ВТБ</Option>
|
||||
<Option value="alfabank">🔴 Альфа-Банк</Option>
|
||||
<Option value="raiffeisen">🟡 Райффайзенбанк</Option>
|
||||
<Option value="gazprombank">🔵 Газпромбанк</Option>
|
||||
<Option value="rosbank">🔴 Росбанк</Option>
|
||||
<Option value="sovcombank">🟢 Совкомбанк</Option>
|
||||
<Option value="otkritie">🔵 Открытие</Option>
|
||||
<Option value="other">💳 Другой банк</Option>
|
||||
{banks.map((bank) => (
|
||||
<Option key={bank.bankid} value={bank.bankid} label={bank.bankname}>
|
||||
{bank.bankname}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@@ -387,7 +456,8 @@ export default function Step3Payment({
|
||||
email: 'test@test.ru',
|
||||
phone: '+79991234567',
|
||||
paymentMethod: 'sbp',
|
||||
bankName: 'sberbank',
|
||||
bankId: banks.length > 0 ? banks[0].bankid : '100000000111', // Сбербанк по умолчанию
|
||||
bankName: banks.length > 0 ? banks[0].bankname : 'Сбербанк',
|
||||
};
|
||||
updateFormData(devData);
|
||||
message.success('DEV: Телефон автоматически подтверждён');
|
||||
@@ -407,7 +477,8 @@ export default function Step3Payment({
|
||||
email: 'test@test.ru',
|
||||
phone: '+79991234567',
|
||||
paymentMethod: 'sbp',
|
||||
bankName: 'sberbank',
|
||||
bankId: banks.length > 0 ? banks[0].bankid : '100000000111', // Сбербанк по умолчанию
|
||||
bankName: banks.length > 0 ? banks[0].bankname : 'Сбербанк',
|
||||
};
|
||||
updateFormData(devData);
|
||||
onSubmit();
|
||||
|
||||
@@ -352,9 +352,9 @@ export default function StepDraftSelection({
|
||||
const docsProgress = getDocsProgress(draft);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '16px',
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '16px',
|
||||
border: `1px solid ${draft.is_legacy ? '#faad14' : '#e8e8e8'}`,
|
||||
borderRadius: 12,
|
||||
marginBottom: 16,
|
||||
@@ -412,7 +412,7 @@ export default function StepDraftSelection({
|
||||
color: '#262626',
|
||||
background: '#f5f5f5',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 8,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid #1890ff',
|
||||
marginTop: 4,
|
||||
wordBreak: 'break-word',
|
||||
@@ -529,27 +529,27 @@ export default function StepDraftSelection({
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
}}>
|
||||
{getActionButton(draft)}
|
||||
<Popconfirm
|
||||
<Popconfirm
|
||||
title="Удалить заявку?"
|
||||
description="Это действие нельзя отменить"
|
||||
description="Это действие нельзя отменить"
|
||||
onConfirm={() => handleDelete(draft.claim_id || draft.id)}
|
||||
okText="Да, удалить"
|
||||
cancelText="Отмена"
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
okText="Да, удалить"
|
||||
cancelText="Отмена"
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === (draft.claim_id || draft.id)}
|
||||
disabled={deletingId === (draft.claim_id || draft.id)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -773,6 +773,14 @@ export function generateConfirmationFormHTML(data: any): string {
|
||||
return '<textarea class="inline-field bind full-width" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '">' + esc(value || '') + '</textarea>';
|
||||
}
|
||||
|
||||
function createBankSelect(root, key, value) {
|
||||
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
|
||||
var selectHtml = '<select class="inline-field bind bank-select" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '">';
|
||||
selectHtml += '<option value="">Загрузка списка банков...</option>';
|
||||
selectHtml += '</select>';
|
||||
return selectHtml;
|
||||
}
|
||||
|
||||
function createCheckbox(root, key, checked, labelText, required) {
|
||||
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
|
||||
var checkedAttr = checked ? ' checked' : '';
|
||||
@@ -900,6 +908,9 @@ export function generateConfirmationFormHTML(data: any): string {
|
||||
// Возмещение
|
||||
html += '<h3 style="font-size:16px;margin:0 0 16px;color:#1f2937">Возмещение:</h3>';
|
||||
html += '<p>Выплата возмещения возможна по системе быстрых платежей (СБП) по номеру телефона заявителя: <strong id="phone-display">' + esc(u.mobile || '') + '</strong></p>';
|
||||
html += '<p><strong>Банк для получения выплаты:</strong> ';
|
||||
html += createBankSelect('user', 'bank_id', u.bank_id || '');
|
||||
html += '</p>';
|
||||
|
||||
html += '<div class="section-break"></div>';
|
||||
|
||||
@@ -1216,8 +1227,8 @@ export function generateConfirmationFormHTML(data: any): string {
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
field.classList.add('filled');
|
||||
} else {
|
||||
field.classList.add('filled');
|
||||
} else {
|
||||
field.classList.add('invalid');
|
||||
}
|
||||
}
|
||||
@@ -1424,6 +1435,59 @@ export function generateConfirmationFormHTML(data: any): string {
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка списка банков СБП
|
||||
function loadBanks() {
|
||||
var bankSelects = document.querySelectorAll('.bank-select');
|
||||
if (bankSelects.length === 0) {
|
||||
console.log('Bank select fields not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading NSPK banks...');
|
||||
|
||||
fetch('http://212.193.27.93/api/payouts/dictionaries/nspk-banks')
|
||||
.then(function(response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(banks) {
|
||||
console.log('Loaded ' + banks.length + ' banks');
|
||||
|
||||
// Сортируем по названию
|
||||
banks.sort(function(a, b) {
|
||||
return a.bankname.localeCompare(b.bankname, 'ru');
|
||||
});
|
||||
|
||||
// Заполняем все bank-select элементы
|
||||
Array.prototype.forEach.call(bankSelects, function(select) {
|
||||
var currentValue = select.getAttribute('data-selected') || state.user?.bank_id || '';
|
||||
select.innerHTML = '<option value="">Выберите банк</option>';
|
||||
|
||||
banks.forEach(function(bank) {
|
||||
var option = document.createElement('option');
|
||||
option.value = bank.bankid;
|
||||
option.textContent = bank.bankname;
|
||||
if (bank.bankid === currentValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Если выбран банк, обновляем стиль
|
||||
if (currentValue && select.value) {
|
||||
select.classList.add('filled');
|
||||
updateFieldStyle(select);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Error loading banks:', error);
|
||||
Array.prototype.forEach.call(bankSelects, function(select) {
|
||||
select.innerHTML = '<option value="">Ошибка загрузки банков. Обновите страницу.</option>';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
try {
|
||||
console.log('=== НАЧАЛО ИНИЦИАЛИЗАЦИИ ===');
|
||||
@@ -1451,6 +1515,12 @@ export function generateConfirmationFormHTML(data: any): string {
|
||||
renderStatement();
|
||||
console.log('renderStatement completed');
|
||||
|
||||
// Загружаем список банков СБП
|
||||
console.log('Loading banks...');
|
||||
setTimeout(function() {
|
||||
loadBanks();
|
||||
}, 100);
|
||||
|
||||
// Валидируем уже заполненные поля
|
||||
setTimeout(function(){
|
||||
console.log('Starting field validation...');
|
||||
|
||||
@@ -76,7 +76,8 @@ interface FormData {
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
paymentMethod?: string;
|
||||
bankName?: string;
|
||||
bankId?: string; // ID банка из NSPK API
|
||||
bankName?: string; // Название банка для отображения
|
||||
cardNumber?: string;
|
||||
accountNumber?: string;
|
||||
}
|
||||
@@ -989,7 +990,8 @@ export default function ClaimForm() {
|
||||
email: formData.email,
|
||||
event_type: formData.eventType,
|
||||
payment_method: formData.paymentMethod,
|
||||
bank_name: formData.bankName,
|
||||
bank_id: formData.bankId, // ID банка из NSPK API
|
||||
bank_name: formData.bankName, // Название банка для отображения
|
||||
card_number: formData.cardNumber,
|
||||
account_number: formData.accountNumber,
|
||||
|
||||
|
||||
@@ -9,7 +9,15 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://host.docker.internal:8200',
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
// SSE support
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyRes', (proxyRes) => {
|
||||
// Disable buffering for SSE
|
||||
proxyRes.headers['cache-control'] = 'no-cache';
|
||||
proxyRes.headers['x-accel-buffering'] = 'no';
|
||||
});
|
||||
}
|
||||
},
|
||||
'/events': {
|
||||
target: 'http://host.docker.internal:8200',
|
||||
|
||||
Reference in New Issue
Block a user