Улучшен 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,
.payment-form.deleted {
.order-item-form.deleted {
opacity: 0.5;
pointer-events: none;
}
@@ -95,6 +94,20 @@
background-color: #0d6efd;
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>
{% endblock %}
@@ -605,7 +618,9 @@
{% elif order.amount_paid > 0 %}
<span class="badge bg-warning text-dark fs-5">Частично оплачено</span>
{% 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 %}
</div>
@@ -693,8 +708,8 @@
</button>
{% endfor %}
</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;">
<i class="bi bi-exclamation-triangle-fill"></i> На личном счете клиента нет остатка, используйте другой способ оплаты
@@ -710,6 +725,7 @@
min="0.01"
class="form-control"
id="unified-amount"
name="amount"
value="{{ order.amount_due|default:0|unlocalize }}"
required>
<span class="input-group-text">руб.</span>
@@ -728,7 +744,7 @@
<!-- Примечание (одно поле для обоих режимов) -->
<div class="mb-3">
<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>
<button type="submit" class="btn w-100" id="unified-submit">
@@ -908,7 +924,6 @@ document.addEventListener('DOMContentLoaded', function() {
function applyMode() {
if (currentMode === 'payment') {
form.action = paymentUrl;
// Имена полей под платеж
methodHidden.name = 'payment_method';
amountInput.name = 'amount';
notesInput.name = 'notes';
@@ -916,18 +931,13 @@ document.addEventListener('DOMContentLoaded', function() {
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> Оплатить';
// Значение суммы по умолчанию — неоплаченная сумма
amountInput.value = amountDue || '';
// Лимиты: только если выбран кошелёк
updateLimitsForPayment();
} else {
form.action = refundUrl;
// Имена полей под возврат
methodHidden.name = 'refund_payment_method';
amountInput.name = 'refund_amount';
notesInput.name = 'refund_notes';
@@ -935,13 +945,10 @@ document.addEventListener('DOMContentLoaded', function() {
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> Вернуть средства';
// Значение суммы — пусто, лимит по оплаченным средствам
amountInput.value = '';
updateLimitsForRefund();
}
}
@@ -972,44 +979,89 @@ document.addEventListener('DOMContentLoaded', function() {
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();
});
});
// Выбор способа (не активируем по умолчанию)
// Выбор способа
methodButtons.forEach(btn => {
btn.addEventListener('click', () => {
const methodCode = btn.dataset.code;
// Скрываем подсказку перед проверкой
if (walletEmptyHint) walletEmptyHint.style.display = 'none';
// Скрываем подсказку
walletEmptyHint.style.display = 'none';
// Проверяем баланс кошелька для платежа
// Проверяем баланс кошелька
if (currentMode === 'payment' && methodCode === 'account_balance' && walletBalance === 0) {
if (walletEmptyHint) walletEmptyHint.style.display = 'block';
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') {
// Для кошелька: подставляем максимум из (остаток к оплате, баланс)
if (methodCode === 'account_balance') {
const max = Math.min(amountDue, walletBalance);
amountInput.value = max.toFixed(2);
}
updateLimitsForPayment();
} else {
updateLimitsForRefund();
// Для кошелька подставляем максимальную сумму
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>
@@ -1290,7 +1342,10 @@ document.addEventListener('DOMContentLoaded', function() {
const pickupFields = document.getElementById('pickup-fields');
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') {
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
});
</script>