feat: Обновления после последнего коммита

Изменения в backend:
- Обновления в n8n_proxy.py
- Изменения в SMS API
- Обновления конфигурации
- Улучшения SMS сервиса

Изменения в frontend:
- Обновления Step1Phone компонента
- Изменения в Step3Payment
- Улучшения generateConfirmationFormHTML
- Обновления ClaimForm страницы
- Изменения в vite.config.ts

Статистика: +242 строки, -81 строка
This commit is contained in:
Fedor
2026-01-02 17:37:37 +03:00
parent f7d27388a0
commit 73524465fd
10 changed files with 341 additions and 81 deletions

View File

@@ -352,7 +352,18 @@ export default function Step1Phone({
icon={<CopyOutlined />}
onClick={() => {
if (debugCode) {
navigator.clipboard.writeText(debugCode);
// Fallback для HTTP (clipboard API требует HTTPS)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(debugCode);
} else {
// Fallback: копируем через textarea
const textArea = document.createElement('textarea');
textArea.value = debugCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
message.success('Код скопирован!');
}
}}

View File

@@ -51,10 +51,17 @@ export default function Step3Payment({
throw new Error(`HTTP ${response.status}`);
}
const banksData: Bank[] = await response.json();
let banksData: Bank[] = await response.json();
// ✅ Фильтруем банки без названия
banksData = banksData.filter(bank => bank && bank.bankname && typeof bank.bankname === 'string');
// Сортируем по названию для удобства
banksData.sort((a, b) => a.bankname.localeCompare(b.bankname, 'ru'));
banksData.sort((a, b) => {
const nameA = (a.bankname || '').toString();
const nameB = (b.bankname || '').toString();
return nameA.localeCompare(nameB, 'ru');
});
setBanks(banksData);
addDebugEvent?.('banks', 'success', `✅ Загружено ${banksData.length} банков`, { count: banksData.length });
@@ -62,29 +69,31 @@ export default function Step3Payment({
// Если есть сохранённый bankName или bankId - восстанавливаем значения
if (formData.bankName) {
const foundBank = banksData.find(b =>
b.bankname.toLowerCase() === formData.bankName.toLowerCase() ||
b.bankname.toLowerCase().includes(formData.bankName.toLowerCase())
b && b.bankname && (
b.bankname.toLowerCase() === formData.bankName.toLowerCase() ||
b.bankname.toLowerCase().includes(formData.bankName.toLowerCase())
)
);
if (foundBank) {
if (foundBank && foundBank.bankname) {
updateFormData({
bankId: foundBank.bankid,
bankId: foundBank.bankid || '',
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankId: foundBank.bankid || '',
bankName: foundBank.bankname
});
}
} else if (formData.bankId) {
// Если есть только bankId, находим по ID
const foundBank = banksData.find(b => b.bankid === formData.bankId);
if (foundBank) {
const foundBank = banksData.find(b => b && b.bankid === formData.bankId);
if (foundBank && foundBank.bankname) {
updateFormData({
bankId: foundBank.bankid,
bankId: foundBank.bankid || '',
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankId: foundBank.bankid || '',
bankName: foundBank.bankname
});
}
@@ -414,7 +423,7 @@ export default function Step3Payment({
return Promise.resolve();
}
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
b && b.bankname && b.bankname.toLowerCase() === value.toLowerCase()
);
if (!foundBank) {
return Promise.reject(new Error('Выберите банк из списка'));
@@ -429,38 +438,40 @@ export default function Step3Payment({
size="large"
loading={banksLoading}
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден. Попробуйте ввести другое название"}
options={banks.map((bank) => ({
value: bank.bankname,
label: bank.bankname,
}))}
options={banks
.filter(bank => bank && bank.bankname)
.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) {
const selectedBank = banks.find(b => b && b.bankname && b.bankname === value);
if (selectedBank && selectedBank.bankname) {
updateFormData({
bankId: selectedBank.bankid,
bankId: selectedBank.bankid || '',
bankName: selectedBank.bankname
});
// Устанавливаем bankId в скрытое поле
form.setFieldsValue({ bankId: selectedBank.bankid });
form.setFieldsValue({ bankId: selectedBank.bankid || '' });
}
}}
onChange={(value) => {
// При вводе текста ищем точное совпадение по названию
if (typeof value === 'string') {
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
b && b.bankname && b.bankname.toLowerCase() === value.toLowerCase()
);
if (foundBank) {
if (foundBank && foundBank.bankname) {
updateFormData({
bankId: foundBank.bankid,
bankId: foundBank.bankid || '',
bankName: foundBank.bankname
});
form.setFieldsValue({ bankId: foundBank.bankid });
form.setFieldsValue({ bankId: foundBank.bankid || '' });
} else if (value === '') {
// Если поле очищено, очищаем и bankId
updateFormData({ bankId: undefined, bankName: undefined });

View File

@@ -1064,17 +1064,7 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
html += createMoneyField('project', 'agrprice', p.agrprice);
html += '<span class="required-marker">*</span></p>';
// Период
html += '<p><strong>Период:</strong> ';
if (p.startdate || p.finishdate) {
html += 'с ';
html += createDateField('project', 'startdate', p.startdate);
html += ' по ';
html += createDateField('project', 'finishdate', p.finishdate);
} else {
html += createField('project', 'period_text', p.period_text, 'Период действия');
}
html += '</p>';
// Период - УДАЛЕНО по требованию
html += '<div class="section-break"></div>';
@@ -1685,11 +1675,49 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
.then(function(banks) {
console.log('Loaded ' + banks.length + ' banks');
// ✅ Нормализуем данные: API возвращает bankId/bankName, приводим к bankid/bankname
if (banks.length > 0) {
console.log('🔍 Первый банк до нормализации:', JSON.stringify(banks[0]));
}
banks = banks.map(function(bank) {
if (!bank) return null;
return {
bankid: bank.bankId || bank.bankid || '',
bankname: bank.bankName || bank.bankname || ''
};
});
if (banks.length > 0 && banks[0]) {
console.log('🔍 Первый банк после нормализации:', JSON.stringify(banks[0]));
}
// ✅ Фильтруем банки без названия и сортируем по названию
var initialCount = banks.length;
banks = banks.filter(function(bank) {
return bank && bank.bankname && typeof bank.bankname === 'string' && bank.bankname.trim() !== '';
});
console.log('✅ Фильтрация банков: было ' + initialCount + ', стало ' + banks.length);
if (banks.length === 0) {
console.error('❌ Нет валидных банков после фильтрации!');
Array.prototype.forEach.call(bankInputs, function(input) {
var datalistId = input.getAttribute('list');
var datalist = document.getElementById(datalistId);
if (datalist) {
datalist.innerHTML = '<option value="">Ошибка: нет валидных банков</option>';
}
});
return;
}
// Сортируем по названию
banks.sort(function(a, b) {
return a.bankname.localeCompare(b.bankname, 'ru');
const nameA = (a.bankname || '').toString();
const nameB = (b.bankname || '').toString();
return nameA.localeCompare(nameB, 'ru');
});
console.log('✅ Банки отфильтрованы и отсортированы: ' + banks.length + ' шт.');
// Сохраняем список банков глобально для поиска
window.__banksList = banks;
@@ -1703,19 +1731,29 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
var currentBankName = '';
if (!datalist) {
console.error('Datalist not found for input:', input.id);
console.error('Datalist not found for input:', input.id, 'datalistId:', datalistId);
return;
}
console.log('📋 Заполняю datalist для input:', input.id, 'datalistId:', datalistId);
// Очищаем datalist
datalist.innerHTML = '';
// Заполняем datalist опциями
var optionsAdded = 0;
banks.forEach(function(bank) {
// ✅ Проверяем наличие bankname перед использованием
if (!bank || !bank.bankname) {
console.warn('⚠️ Пропущен банк без названия:', bank);
return;
}
var option = document.createElement('option');
option.value = bank.bankname;
option.setAttribute('data-bank-id', bank.bankid);
option.setAttribute('data-bank-id', bank.bankid || '');
datalist.appendChild(option);
optionsAdded++;
// Если это текущий банк, устанавливаем значение
if (bank.bankid === currentBankId) {
@@ -1723,6 +1761,22 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
}
});
console.log('✅ Добавлено опций в datalist:', optionsAdded, 'для input:', input.id);
// Проверяем, что опции действительно добавлены
var actualOptionsCount = datalist.querySelectorAll('option').length;
console.log('🔍 Проверка datalist:', {
datalistId: datalistId,
optionsAdded: optionsAdded,
actualOptionsInDOM: actualOptionsCount,
inputId: input.id,
inputListAttr: input.getAttribute('list')
});
if (actualOptionsCount === 0 && optionsAdded > 0) {
console.error('❌ КРИТИЧЕСКАЯ ОШИБКА: опции не добавлены в DOM!');
}
// Устанавливаем текущее значение если есть
if (currentBankName) {
input.value = currentBankName;
@@ -1744,17 +1798,17 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
// Ищем точное совпадение
if (inputValue) {
foundBank = banks.find(function(b) {
return b.bankname.toLowerCase() === inputValue.toLowerCase();
return b && b.bankname && b.bankname.toLowerCase() === inputValue.toLowerCase();
});
}
if (foundBank) {
if (foundBank && foundBank.bankname) {
// Найден банк - сохраняем ID и название
if (hiddenField) {
hiddenField.value = foundBank.bankid;
hiddenField.value = foundBank.bankid || '';
}
state.user = state.user || {};
state.user.bank_id = foundBank.bankid;
state.user.bank_id = foundBank.bankid || '';
state.user.bank_name = foundBank.bankname; // ✅ Сохраняем название банка
this.classList.add('filled');
} else {
@@ -1775,15 +1829,15 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
input.addEventListener('change', function() {
var inputValue = this.value.trim();
var foundBank = banks.find(function(b) {
return b.bankname.toLowerCase() === inputValue.toLowerCase();
return b && b.bankname && b.bankname.toLowerCase() === inputValue.toLowerCase();
});
if (foundBank) {
if (foundBank && foundBank.bankname) {
if (hiddenField) {
hiddenField.value = foundBank.bankid;
hiddenField.value = foundBank.bankid || '';
}
state.user = state.user || {};
state.user.bank_id = foundBank.bankid;
state.user.bank_id = foundBank.bankid || '';
state.user.bank_name = foundBank.bankname; // ✅ Сохраняем название банка
this.classList.add('filled');
updateFieldStyle(this);
@@ -1793,12 +1847,23 @@ export function generateConfirmationFormHTML(data: any, contact_data_confirmed:
})
.catch(function(error) {
console.error('Error loading banks:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
// Показываем сообщение об ошибке пользователю
Array.prototype.forEach.call(bankInputs, function(input) {
var datalistId = input.getAttribute('list');
var datalist = document.getElementById(datalistId);
if (datalist) {
datalist.innerHTML = '<option value="">Ошибка загрузки банков. Обновите страницу.</option>';
}
// Показываем placeholder с ошибкой
if (input.placeholder) {
input.placeholder = 'Ошибка загрузки списка банков';
}
});
});
}

View File

@@ -108,7 +108,7 @@ export default function ClaimForm() {
useEffect(() => {
// 🔥 VERSION CHECK: Если видишь это в консоли - фронт обновился!
console.log('🔥 ClaimForm v3.8 - 2025-11-20 15:10 - Fix session_id priority in loadDraft');
console.log('🔥 ClaimForm v3.9 - 2025-12-29 - Auto redirect to drafts after success');
}, []);
// ✅ Восстановление сессии при загрузке страницы
@@ -998,6 +998,42 @@ export default function ClaimForm() {
setCurrentStep(1); // ✅ Переходим к описанию (индекс 1)
}, [updateFormData, currentStep, isPhoneVerified, formData.unified_id, formData.phone]);
// ✅ Автоматический редирект на экран черновиков после успешной отправки
useEffect(() => {
if (isSubmitted) {
console.log('✅ Обращение успешно отправлено, ждём 2.5 секунды перед редиректом на черновики...');
const redirectTimer = setTimeout(async () => {
console.log('🔄 Выполняем редирект на экран черновиков');
// Проверяем наличие черновиков
const hasDraftsResult = await checkDrafts(
formData.unified_id,
formData.phone,
sessionIdRef.current
);
console.log('🔍 Результат проверки черновиков:', hasDraftsResult);
// Переходим на экран черновиков
setShowDraftSelection(true);
setHasDrafts(hasDraftsResult);
setIsSubmitted(false); // Сбрасываем флаг отправки
setSelectedDraftId(null); // Сбрасываем выбранный черновик
// Переходим на шаг 0 (черновики)
setTimeout(() => {
setCurrentStep(0);
console.log('✅ Переход на экран черновиков выполнен');
}, 100);
}, 2500); // Задержка 2.5 секунды
return () => {
clearTimeout(redirectTimer);
};
}
}, [isSubmitted, formData.unified_id, formData.phone, checkDrafts]);
const handleSubmit = useCallback(async () => {
try {
addDebugEvent('form', 'info', '📤 Отправка заявки в n8n через backend');
@@ -1211,6 +1247,7 @@ export default function ClaimForm() {
});
// Шаг подтверждения заявления (показывается после получения данных из claim:plan)
// ✅ НОВЫЙ ФЛОУ: StepClaimConfirmation с SMS подтверждением
if (formData.showClaimConfirmation && formData.claimPlanData) {
stepsArray.push({
title: 'Подтверждение',
@@ -1225,25 +1262,26 @@ export default function ClaimForm() {
/>
),
});
} else {
// ✅ СТАРЫЙ ФЛОУ: Step3Payment (только если нет StepClaimConfirmation)
// Используется как fallback, если данные claim:plan не получены
stepsArray.push({
title: 'Заявление',
description: 'Подтверждение',
content: (
<Step3Payment
formData={formData}
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
});
}
// Последний шаг: Payment (всегда)
stepsArray.push({
title: 'Заявление',
description: 'Подтверждение',
content: (
<Step3Payment
formData={formData} // ✅ claim_id уже в formData
updateFormData={updateFormData}
onPrev={prevStep}
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
});
return stepsArray;
}, [formData, isPhoneVerified, nextStep, prevStep, updateFormData, handleSubmit, setIsPhoneVerified, addDebugEvent, showDraftSelection, selectedDraftId, hasDrafts, handleSelectDraft, handleNewClaim, checkDrafts]);
@@ -1290,10 +1328,38 @@ export default function ClaimForm() {
// Удаляем session_token из localStorage
localStorage.removeItem('session_token');
// Сбрасываем форму
handleReset();
// ✅ Полный сброс: очищаем все данные авторизации и черновиков
setIsSubmitted(false);
setShowDraftSelection(false);
setHasDrafts(false);
setSelectedDraftId(null);
// ✅ Генерируем новую сессию для нового пользователя
const newSessionId = `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
sessionIdRef.current = newSessionId;
// ✅ Полностью очищаем formData, включая unified_id и phone
setFormData({
voucher: '',
claim_id: undefined,
session_id: newSessionId,
paymentMethod: 'sbp',
unified_id: undefined, // ✅ Очищаем unified_id
phone: undefined, // ✅ Очищаем phone
contact_id: undefined, // ✅ Очищаем contact_id
is_new_contact: undefined,
isPhoneVerified: false,
});
// ✅ Сбрасываем флаг верификации телефона
setIsPhoneVerified(false);
// ✅ Переходим на экран входа (Step1Phone)
// Если showDraftSelection = false и нет unified_id, то шаг 0 будет Step1Phone
setCurrentStep(0);
message.info('Сессия завершена. До свидания!');
addDebugEvent('system', 'info', '🔄 Форма сброшена');
}, [formData.session_id, addDebugEvent]);
return (

View File

@@ -12,7 +12,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://host.docker.internal:8200',
target: 'http://host.docker.internal:8201',
changeOrigin: true,
// SSE support
configure: (proxy) => {
@@ -24,7 +24,7 @@ export default defineConfig({
}
},
'/events': {
target: 'http://host.docker.internal:8200',
target: 'http://host.docker.internal:8201',
changeOrigin: true
}
}