Улучшен UI формы оплаты: красивый бейдж НЕ ОПЛАЧЕНО с пульсирующей анимацией, убран устаревший код смешанной оплаты, исправлены критичные ошибки JavaScript

This commit is contained in:
2025-11-29 22:47:56 +03:00
parent 9c6092262c
commit 4343b2eb5b

View File

@@ -12,8 +12,7 @@
} }
/* Визуально помечаем удаленные формы */ /* Визуально помечаем удаленные формы */
.order-item-form.deleted, .order-item-form.deleted {
.payment-form.deleted {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
@@ -95,6 +94,20 @@
background-color: #0d6efd; background-color: #0d6efd;
color: white; color: white;
} }
/* Пульсирующий бейдж для неоплаченных заказов */
.unpaid-badge {
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(220, 53, 69, 0.8), 0 0 30px rgba(220, 53, 69, 0.4);
}
}
</style> </style>
{% endblock %} {% endblock %}
@@ -605,7 +618,9 @@
{% elif order.amount_paid > 0 %} {% elif order.amount_paid > 0 %}
<span class="badge bg-warning text-dark fs-5">Частично оплачено</span> <span class="badge bg-warning text-dark fs-5">Частично оплачено</span>
{% else %} {% else %}
<span class="badge bg-secondary fs-5">Не оплачено</span> <span class="badge bg-danger text-white fs-5 unpaid-badge">
<i class="bi bi-exclamation-triangle-fill"></i> НЕ ОПЛАЧЕНО
</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -693,8 +708,8 @@
</button> </button>
{% endfor %} {% endfor %}
</div> </div>
<!-- Один скрытый инпут: имя будет меняться в зависимости от режима --> <!-- Скрытое поле для способа оплаты -->
<input type="hidden" id="unified-method-id" required> <input type="hidden" id="unified-method-id" name="payment_method" required>
<!-- Подсказка при пустом кошельке --> <!-- Подсказка при пустом кошельке -->
<div class="alert alert-warning py-2 mt-2 mb-0" id="wallet-empty-hint" style="display:none;"> <div class="alert alert-warning py-2 mt-2 mb-0" id="wallet-empty-hint" style="display:none;">
<i class="bi bi-exclamation-triangle-fill"></i> На личном счете клиента нет остатка, используйте другой способ оплаты <i class="bi bi-exclamation-triangle-fill"></i> На личном счете клиента нет остатка, используйте другой способ оплаты
@@ -710,6 +725,7 @@
min="0.01" min="0.01"
class="form-control" class="form-control"
id="unified-amount" id="unified-amount"
name="amount"
value="{{ order.amount_due|default:0|unlocalize }}" value="{{ order.amount_due|default:0|unlocalize }}"
required> required>
<span class="input-group-text">руб.</span> <span class="input-group-text">руб.</span>
@@ -728,7 +744,7 @@
<!-- Примечание (одно поле для обоих режимов) --> <!-- Примечание (одно поле для обоих режимов) -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold">Примечание</label> <label class="form-label fw-bold">Примечание</label>
<input type="text" class="form-control" id="unified-notes" placeholder="Опционально"> <input type="text" class="form-control" id="unified-notes" name="notes" placeholder="Опционально">
</div> </div>
<button type="submit" class="btn w-100" id="unified-submit"> <button type="submit" class="btn w-100" id="unified-submit">
@@ -908,7 +924,6 @@ document.addEventListener('DOMContentLoaded', function() {
function applyMode() { function applyMode() {
if (currentMode === 'payment') { if (currentMode === 'payment') {
form.action = paymentUrl; form.action = paymentUrl;
// Имена полей под платеж
methodHidden.name = 'payment_method'; methodHidden.name = 'payment_method';
amountInput.name = 'amount'; amountInput.name = 'amount';
notesInput.name = 'notes'; notesInput.name = 'notes';
@@ -916,18 +931,13 @@ document.addEventListener('DOMContentLoaded', function() {
refundReasonInput.removeAttribute('name'); refundReasonInput.removeAttribute('name');
refundReasonInput.removeAttribute('required'); refundReasonInput.removeAttribute('required');
// Стили кнопки
submitBtn.classList.remove('btn-warning', 'text-dark'); submitBtn.classList.remove('btn-warning', 'text-dark');
submitBtn.classList.add('btn-success'); submitBtn.classList.add('btn-success');
submitBtn.innerHTML = '<i class="bi bi-check-lg"></i> Оплатить'; submitBtn.innerHTML = '<i class="bi bi-check-lg"></i> Оплатить';
// Значение суммы по умолчанию — неоплаченная сумма
amountInput.value = amountDue || '';
// Лимиты: только если выбран кошелёк
updateLimitsForPayment(); updateLimitsForPayment();
} else { } else {
form.action = refundUrl; form.action = refundUrl;
// Имена полей под возврат
methodHidden.name = 'refund_payment_method'; methodHidden.name = 'refund_payment_method';
amountInput.name = 'refund_amount'; amountInput.name = 'refund_amount';
notesInput.name = 'refund_notes'; notesInput.name = 'refund_notes';
@@ -935,13 +945,10 @@ document.addEventListener('DOMContentLoaded', function() {
refundReasonInput.name = 'refund_reason'; refundReasonInput.name = 'refund_reason';
refundReasonInput.setAttribute('required', 'required'); refundReasonInput.setAttribute('required', 'required');
// Стили кнопки
submitBtn.classList.remove('btn-success'); submitBtn.classList.remove('btn-success');
submitBtn.classList.add('btn-warning', 'text-dark'); submitBtn.classList.add('btn-warning', 'text-dark');
submitBtn.innerHTML = '<i class="bi bi-arrow-return-left"></i> Вернуть средства'; submitBtn.innerHTML = '<i class="bi bi-arrow-return-left"></i> Вернуть средства';
// Значение суммы — пусто, лимит по оплаченным средствам
amountInput.value = '';
updateLimitsForRefund(); updateLimitsForRefund();
} }
} }
@@ -972,44 +979,89 @@ document.addEventListener('DOMContentLoaded', function() {
modeButtons.forEach(b => b.classList.remove('active')); modeButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active'); btn.classList.add('active');
currentMode = btn.dataset.mode; currentMode = btn.dataset.mode;
// Сбрасываем выбор способа и сумму при смене режима
methodButtons.forEach(b => b.classList.remove('active'));
methodHidden.value = '';
currentMethodCode = null;
amountInput.value = currentMode === 'payment' ? (amountDue || '') : '';
applyMode(); applyMode();
}); });
}); });
// Выбор способа (не активируем по умолчанию) // Выбор способа
methodButtons.forEach(btn => { methodButtons.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const methodCode = btn.dataset.code; const methodCode = btn.dataset.code;
// Скрываем подсказку перед проверкой // Скрываем подсказку
if (walletEmptyHint) walletEmptyHint.style.display = 'none'; walletEmptyHint.style.display = 'none';
// Проверяем баланс кошелька для платежа // Проверяем баланс кошелька
if (currentMode === 'payment' && methodCode === 'account_balance' && walletBalance === 0) { if (currentMode === 'payment' && methodCode === 'account_balance' && walletBalance === 0) {
if (walletEmptyHint) walletEmptyHint.style.display = 'block'; walletEmptyHint.style.display = 'block';
return; return;
} }
// Активируем выбранный способ
methodButtons.forEach(b => b.classList.remove('active')); methodButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active'); btn.classList.add('active');
methodHidden.value = btn.dataset.id; methodHidden.value = btn.dataset.id;
currentMethodCode = methodCode; currentMethodCode = methodCode;
if (currentMode === 'payment') { // Для кошелька подставляем максимальную сумму
// Для кошелька: подставляем максимум из (остаток к оплате, баланс) if (currentMode === 'payment' && methodCode === 'account_balance') {
if (methodCode === 'account_balance') { amountInput.value = Math.min(amountDue, walletBalance).toFixed(2);
const max = Math.min(amountDue, walletBalance);
amountInput.value = max.toFixed(2);
}
updateLimitsForPayment();
} else {
updateLimitsForRefund();
} }
// Обновляем лимиты
currentMode === 'payment' ? updateLimitsForPayment() : updateLimitsForRefund();
}); });
}); });
// Инициализация начального режима (без авто-выбора способа) // Инициализация начального режима
modeButtons[0].classList.add('active');
amountInput.value = amountDue || '';
applyMode(); 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 %} {% endif %}
}); });
</script> </script>
@@ -1290,7 +1342,10 @@ document.addEventListener('DOMContentLoaded', function() {
const pickupFields = document.getElementById('pickup-fields'); const pickupFields = document.getElementById('pickup-fields');
function syncDeliveryTypeFromRadio() { function syncDeliveryTypeFromRadio() {
const selectedType = document.querySelector('input[name="delivery-type"]:checked').value; const selectedRadio = document.querySelector('input[name="delivery-type"]:checked');
if (!selectedRadio) return; // Защита от null
const selectedType = selectedRadio.value;
if (selectedType === 'delivery') { if (selectedType === 'delivery') {
isDeliveryCheckbox.checked = true; isDeliveryCheckbox.checked = true;
@@ -1745,131 +1800,6 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
// === УПРАВЛЕНИЕ ПЛАТЕЖАМИ (СМЕШАННАЯ ОПЛАТА) ===
const paymentsContainer = document.getElementById('payments-container');
const addPaymentBtn = document.getElementById('add-payment-btn');
const paymentFormTemplate = document.getElementById('empty-payment-form-template');
let paymentFormCount = parseInt(document.querySelector('[name="payments-TOTAL_FORMS"]').value);
// Функция для расчета итоговой суммы платежей
function calculatePaymentsTotal() {
// 1. Считаем уже сохраненные платежи (информационные блоки)
let existingTotal = 0;
const existingPayments = document.querySelectorAll('.bg-light .text-success');
existingPayments.forEach((el) => {
const text = el.textContent.replace(/[^0-9.,]/g, '').replace(',', '.');
const amount = parseFloat(text) || 0;
existingTotal += amount;
});
// 2. Считаем новые платежи в formset
const visiblePaymentForms = Array.from(document.querySelectorAll('.payment-form'))
.filter(form => !form.classList.contains('deleted'));
let newTotal = 0;
visiblePaymentForms.forEach((form) => {
const amountField = form.querySelector('[name$="-amount"]');
if (amountField) {
const amount = parseFloat(amountField.value.replace(',', '.')) || 0;
newTotal += amount;
}
});
return existingTotal + newTotal;
}
function updatePaymentsTotal() {
const total = calculatePaymentsTotal();
const totalElement = document.getElementById('payments-total-value');
if (totalElement) {
totalElement.textContent = total.toFixed(2);
}
}
// Функция для добавления нового платежа
function addNewPayment() {
const tempContainer = document.createElement('div');
tempContainer.appendChild(paymentFormTemplate.content.cloneNode(true));
const templateHtml = tempContainer.innerHTML;
const replacedHtml = templateHtml.replace(/__prefix__/g, paymentFormCount);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = replacedHtml;
const newPaymentDiv = tempDiv.firstElementChild;
newPaymentDiv.setAttribute('data-form-index', paymentFormCount);
paymentsContainer.appendChild(newPaymentDiv);
paymentFormCount++;
document.querySelector('[name="payments-TOTAL_FORMS"]').value = paymentFormCount;
const removeBtn = newPaymentDiv.querySelector('.remove-payment-btn');
removeBtn.addEventListener('click', function() {
removePayment(newPaymentDiv);
});
const amountField = newPaymentDiv.querySelector('[name$="-amount"]');
if (amountField) {
amountField.addEventListener('input', updatePaymentsTotal);
}
loadPaymentMethods(newPaymentDiv.querySelector('select[name$="-payment_method"]'));
updatePaymentsTotal();
return newPaymentDiv;
}
// Функция для удаления платежа
function removePayment(form) {
if (!confirm('Вы действительно хотите удалить этот платеж?')) {
return;
}
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
const idField = form.querySelector('input[name$="-id"]');
if (idField && idField.value) {
deleteCheckbox.checked = true;
form.classList.add('deleted');
form.style.display = 'none';
} else {
form.remove();
}
updatePaymentsTotal();
}
// Функция для загрузки активных payment methods
function loadPaymentMethods(selectElement) {
fetch('/products/api/payment-methods/')
.then(response => response.json())
.then(data => {
selectElement.innerHTML = '<option value="">---------</option>';
data.forEach(method => {
const option = document.createElement('option');
option.value = method.id;
option.textContent = method.name;
selectElement.appendChild(option);
});
})
.catch(error => {
console.error('Error loading payment methods:', error);
});
}
// Обработчик кнопки "Добавить платеж"
if (addPaymentBtn) {
addPaymentBtn.addEventListener('click', addNewPayment);
}
// Инициализируем итоговую сумму
updatePaymentsTotal();
// Закрытие DOMContentLoaded // Закрытие DOMContentLoaded
}); });
</script> </script>