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