Улучшен UI формы оплаты: красивый бейдж НЕ ОПЛАЧЕНО с пульсирующей анимацией, убран устаревший код смешанной оплаты, исправлены критичные ошибки JavaScript
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user