refactor(orders): extract unified transaction form to reusable module
Moved payment/refund form logic from order_form.html to a dedicated unified_transaction_form.js module for better code organization. Changes: - Created unified_transaction_form.js with initUnifiedTransactionForm() (~233 lines) - Dual mode: payment and refund switching - Dynamic form action and field names - Payment method selection with validation - Wallet balance limits for account_balance method - Amount constraints based on mode - Real-time UI updates and validation - Updated order_form.html: - Added unified_transaction_form.js include - Added initialization call with Django template data - Removed inline transaction form code (~175 lines) - Passes URLs and amounts via options Benefits: - Cleaner template (175 lines removed) - Reusable transaction form logic - Easier to test and maintain - Configurable via options or data-attributes - No duplication between payment/refund modes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -883,186 +883,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Unified Transaction Form -->
|
||||
<script src="{% static 'orders/js/unified_transaction_form.js' %}"></script>
|
||||
{% if order.pk %}
|
||||
<script>
|
||||
// Унифицированная форма платежа/возврата
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('unified-transaction-form');
|
||||
if (!form) return; // Форма есть только для существующих заказов
|
||||
|
||||
{% if order.pk %}
|
||||
// URLs для смены action
|
||||
const paymentUrl = "{% url 'orders:transaction-add-payment' order.order_number %}";
|
||||
const refundUrl = "{% url 'orders:transaction-add-refund' order.order_number %}";
|
||||
|
||||
// Элементы
|
||||
const modeButtons = document.querySelectorAll('#mode-toggle [data-mode]');
|
||||
const methodButtons = document.querySelectorAll('.method-btn');
|
||||
const methodHidden = document.getElementById('unified-method-id');
|
||||
|
||||
const amountInput = document.getElementById('unified-amount');
|
||||
const amountHint = document.getElementById('amount-hint');
|
||||
const amountMaxSpan = document.getElementById('amount-max');
|
||||
const walletEmptyHint = document.getElementById('wallet-empty-hint');
|
||||
|
||||
const refundReasonWrap = document.getElementById('refund-reason-wrap');
|
||||
const refundReasonInput = document.getElementById('refund-reason');
|
||||
const notesInput = document.getElementById('unified-notes');
|
||||
const submitBtn = document.getElementById('unified-submit');
|
||||
|
||||
// Данные
|
||||
const amountDue = {{ order.amount_due|default:0|unlocalize }};
|
||||
const amountPaid = {{ order.amount_paid|default:0|unlocalize }};
|
||||
const walletBalance = {% if order.customer %}{{ order.customer.wallet_balance|default:0|unlocalize }}{% else %}0{% endif %};
|
||||
|
||||
// Состояние
|
||||
let currentMode = 'payment'; // режим по умолчанию — Платёж
|
||||
let currentMethodCode = null;
|
||||
|
||||
// Применить режим
|
||||
function applyMode() {
|
||||
if (currentMode === 'payment') {
|
||||
form.action = paymentUrl;
|
||||
methodHidden.name = 'payment_method';
|
||||
amountInput.name = 'amount';
|
||||
notesInput.name = 'notes';
|
||||
refundReasonWrap.style.display = 'none';
|
||||
refundReasonInput.removeAttribute('name');
|
||||
refundReasonInput.removeAttribute('required');
|
||||
|
||||
submitBtn.classList.remove('btn-warning', 'text-dark');
|
||||
submitBtn.classList.add('btn-success');
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-lg"></i> Оплатить';
|
||||
|
||||
updateLimitsForPayment();
|
||||
} else {
|
||||
form.action = refundUrl;
|
||||
methodHidden.name = 'refund_payment_method';
|
||||
amountInput.name = 'refund_amount';
|
||||
notesInput.name = 'refund_notes';
|
||||
refundReasonWrap.style.display = 'block';
|
||||
refundReasonInput.name = 'refund_reason';
|
||||
refundReasonInput.setAttribute('required', 'required');
|
||||
|
||||
submitBtn.classList.remove('btn-success');
|
||||
submitBtn.classList.add('btn-warning', 'text-dark');
|
||||
submitBtn.innerHTML = '<i class="bi bi-arrow-return-left"></i> Вернуть средства';
|
||||
|
||||
updateLimitsForRefund();
|
||||
}
|
||||
}
|
||||
|
||||
// Лимиты для платежа (кошелёк ограничивается балансом и суммой к оплате)
|
||||
function updateLimitsForPayment() {
|
||||
if (currentMethodCode === 'account_balance') {
|
||||
const max = Math.min(amountDue, walletBalance);
|
||||
amountInput.max = max;
|
||||
amountHint.style.display = 'inline';
|
||||
amountMaxSpan.textContent = (max || 0).toFixed(2);
|
||||
} else {
|
||||
amountInput.removeAttribute('max');
|
||||
amountHint.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Лимиты для возврата (всегда ограничиваем суммой оплаченного)
|
||||
function updateLimitsForRefund() {
|
||||
amountInput.max = amountPaid || 0;
|
||||
amountHint.style.display = 'inline';
|
||||
amountMaxSpan.textContent = (amountPaid || 0).toFixed(2);
|
||||
}
|
||||
|
||||
// Выбор режима
|
||||
modeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
modeButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentMode = btn.dataset.mode;
|
||||
|
||||
// Сбрасываем выбор способа и сумму при смене режима
|
||||
methodButtons.forEach(b => b.classList.remove('active'));
|
||||
methodHidden.value = '';
|
||||
currentMethodCode = null;
|
||||
amountInput.value = currentMode === 'payment' ? (amountDue || '') : '';
|
||||
|
||||
applyMode();
|
||||
});
|
||||
window.initUnifiedTransactionForm('#unified-transaction-form', {
|
||||
paymentUrl: "{% url 'orders:transaction-add-payment' order.order_number %}",
|
||||
refundUrl: "{% url 'orders:transaction-add-refund' order.order_number %}",
|
||||
amountDue: {{ order.amount_due|default:0|unlocalize }},
|
||||
amountPaid: {{ order.amount_paid|default:0|unlocalize }},
|
||||
walletBalance: {% if order.customer %}{{ order.customer.wallet_balance|default:0|unlocalize }}{% else %}0{% endif %}
|
||||
});
|
||||
|
||||
// Выбор способа
|
||||
methodButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const methodCode = btn.dataset.code;
|
||||
|
||||
// Скрываем подсказку
|
||||
walletEmptyHint.style.display = 'none';
|
||||
|
||||
// Проверяем баланс кошелька
|
||||
if (currentMode === 'payment' && methodCode === 'account_balance' && walletBalance === 0) {
|
||||
walletEmptyHint.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Активируем выбранный способ
|
||||
methodButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
methodHidden.value = btn.dataset.id;
|
||||
currentMethodCode = methodCode;
|
||||
|
||||
// Для кошелька подставляем максимальную сумму
|
||||
if (currentMode === 'payment' && methodCode === 'account_balance') {
|
||||
amountInput.value = Math.min(amountDue, walletBalance).toFixed(2);
|
||||
}
|
||||
|
||||
// Обновляем лимиты
|
||||
currentMode === 'payment' ? updateLimitsForPayment() : updateLimitsForRefund();
|
||||
});
|
||||
});
|
||||
|
||||
// Инициализация начального режима
|
||||
modeButtons[0].classList.add('active');
|
||||
amountInput.value = amountDue || '';
|
||||
applyMode();
|
||||
|
||||
// Валидация перед отправкой формы
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Проверяем выбран ли способ оплаты
|
||||
if (!methodHidden.value) {
|
||||
e.preventDefault();
|
||||
|
||||
// Подсвечиваем блок со способами оплаты
|
||||
const methodsContainer = document.getElementById('unified-methods');
|
||||
methodsContainer.classList.add('border', 'border-danger', 'rounded', 'p-2');
|
||||
|
||||
// Показываем подсказку
|
||||
const existingHint = document.getElementById('method-required-hint');
|
||||
if (existingHint) {
|
||||
existingHint.remove();
|
||||
}
|
||||
|
||||
const hint = document.createElement('small');
|
||||
hint.id = 'method-required-hint';
|
||||
hint.className = 'text-danger d-block mt-1';
|
||||
const messageText = currentMode === 'payment' ? 'способ оплаты' : 'способ возврата';
|
||||
hint.innerHTML = '<i class="bi bi-exclamation-circle"></i> Пожалуйста, выберите ' + messageText;
|
||||
methodsContainer.parentElement.appendChild(hint);
|
||||
|
||||
// Убираем подсветку при выборе
|
||||
const removeValidation = () => {
|
||||
methodsContainer.classList.remove('border', 'border-danger', 'rounded', 'p-2');
|
||||
const hint = document.getElementById('method-required-hint');
|
||||
if (hint) hint.remove();
|
||||
};
|
||||
|
||||
methodButtons.forEach(btn => {
|
||||
btn.addEventListener('click', removeValidation, { once: true });
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Customer Select2 Widget -->
|
||||
|
||||
Reference in New Issue
Block a user