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:
Fedor
2025-12-02 11:06:15 +03:00
parent 834520a045
commit ee1c4af5c3
33 changed files with 10175 additions and 74 deletions

View File

@@ -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

View File

@@ -432,3 +432,7 @@ return [
- `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` — шаблон формы заявления

View File

@@ -98,3 +98,7 @@ check_all_ready (FALSE) → (конец)

View File

@@ -31,3 +31,7 @@

View File

@@ -18,3 +18,7 @@

View File

@@ -20,3 +20,7 @@

View File

@@ -19,3 +19,7 @@

8927
ticket_form/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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();

View File

@@ -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>
);
}}
/>

View File

@@ -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...');

View File

@@ -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,

View File

@@ -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',