feat(pos): добавлен полноценный интерфейс скидок в модальное окно продажи
- Добавлен API endpoint /pos/api/discounts/available/ для получения списка доступных скидок - Добавлен метод DiscountApplier.apply_manual_discount() для применения ручных скидок - Обновлен POS checkout для обработки manual_discount_id - Расширена секция скидок в модальном окне: * Отображение автоматических скидок (read-only) * Dropdown для выбора скидки вручную * Подробная детализация: подитог, общая скидка, скидки на позиции * Поле промокода с иконкой - Увеличен размер модального окна и изменено соотношение колонок (5/7) - Убрана вертикальная прокрутка из модального окна Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2294,6 +2294,8 @@ let paymentWidget = null;
|
||||
|
||||
// Переменные состояния скидок
|
||||
let appliedPromoCode = null; // примененный промокод
|
||||
let appliedManualDiscount = null; // выбранная вручную скидка
|
||||
let availableDiscounts = []; // список доступных скидок
|
||||
let cartDiscounts = {
|
||||
orderDiscount: null, // скидка на заказ
|
||||
itemDiscounts: [], // скидки на позиции
|
||||
@@ -2326,6 +2328,9 @@ document.getElementById('checkoutModal').addEventListener('show.bs.modal', async
|
||||
// Проверяем автоматические скидки
|
||||
await checkAutoDiscounts();
|
||||
|
||||
// Загружаем доступные скидки для ручного выбора
|
||||
await loadAvailableDiscounts();
|
||||
|
||||
// Применяем скидки к итоговой сумме
|
||||
const finalTotal = Math.max(0, totalAmount - cartDiscounts.totalDiscount);
|
||||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.';
|
||||
@@ -2366,6 +2371,8 @@ function reinitPaymentWidget(mode) {
|
||||
// Сброс скидок
|
||||
function resetDiscounts() {
|
||||
appliedPromoCode = null;
|
||||
appliedManualDiscount = null;
|
||||
availableDiscounts = [];
|
||||
cartDiscounts = {
|
||||
orderDiscount: null,
|
||||
itemDiscounts: [],
|
||||
@@ -2379,8 +2386,13 @@ function resetDiscounts() {
|
||||
document.getElementById('promoCodeSuccess').style.display = 'none';
|
||||
document.getElementById('promoCodeSuccess').textContent = '';
|
||||
document.getElementById('removePromoBtn').style.display = 'none';
|
||||
document.getElementById('autoDiscounts').style.display = 'none';
|
||||
document.getElementById('autoDiscountsText').textContent = '';
|
||||
|
||||
// Новые элементы UI
|
||||
document.getElementById('autoDiscountsContainer').style.display = 'none';
|
||||
document.getElementById('autoDiscountsList').innerHTML = '';
|
||||
document.getElementById('manualDiscountContainer').style.display = 'none';
|
||||
document.getElementById('discountsSummary').style.display = 'none';
|
||||
document.getElementById('itemDiscountsBreakdown').innerHTML = '';
|
||||
}
|
||||
|
||||
// Проверить автоматические скидки
|
||||
@@ -2403,29 +2415,168 @@ async function checkAutoDiscounts() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: items,
|
||||
customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null
|
||||
customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null,
|
||||
manual_discount_id: appliedManualDiscount?.id || null
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Сохраняем скидки
|
||||
cartDiscounts.totalDiscount = result.total_discount || 0;
|
||||
cartDiscounts.orderDiscount = result.order_discount;
|
||||
cartDiscounts.itemDiscounts = result.item_discounts || [];
|
||||
|
||||
// Показываем автоматические скидки
|
||||
if (result.order_discount && result.order_discount.discount_id) {
|
||||
document.getElementById('autoDiscounts').style.display = 'block';
|
||||
document.getElementById('autoDiscountsText').textContent =
|
||||
`${result.order_discount.discount_name}: -${result.order_discount.discount_amount.toFixed(2)} руб.`;
|
||||
}
|
||||
updateDiscountsUI(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке автоматических скидок:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить UI скидок
|
||||
function updateDiscountsUI(result) {
|
||||
const autoContainer = document.getElementById('autoDiscountsContainer');
|
||||
const autoList = document.getElementById('autoDiscountsList');
|
||||
const summaryDiv = document.getElementById('discountsSummary');
|
||||
const itemBreakdown = document.getElementById('itemDiscountsBreakdown');
|
||||
|
||||
// Очистка
|
||||
autoList.innerHTML = '';
|
||||
itemBreakdown.innerHTML = '';
|
||||
|
||||
let hasDiscounts = false;
|
||||
|
||||
// 1. Скидка на заказ (автоматическая)
|
||||
if (result.order_discount && result.order_discount.discount_id) {
|
||||
hasDiscounts = true;
|
||||
autoContainer.style.display = 'block';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'd-flex justify-content-between';
|
||||
div.innerHTML = `
|
||||
<span>${result.order_discount.discount_name}</span>
|
||||
<span class="text-success">-${result.order_discount.discount_amount.toFixed(2)} руб.</span>
|
||||
`;
|
||||
autoList.appendChild(div);
|
||||
} else {
|
||||
autoContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// 2. Скидки на позиции
|
||||
if (result.item_discounts && result.item_discounts.length > 0) {
|
||||
hasDiscounts = true;
|
||||
result.item_discounts.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'text-muted';
|
||||
div.innerHTML = `• ${item.discount_name}: -${item.discount_amount.toFixed(2)} руб.`;
|
||||
itemBreakdown.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Ручная скидка
|
||||
if (appliedManualDiscount) {
|
||||
hasDiscounts = true;
|
||||
document.getElementById('manualDiscountContainer').style.display = 'block';
|
||||
document.getElementById('manualDiscountName').textContent = appliedManualDiscount.name;
|
||||
document.getElementById('manualDiscountAmount').textContent =
|
||||
`-${appliedManualDiscount.amount.toFixed(2)} руб.`;
|
||||
} else {
|
||||
document.getElementById('manualDiscountContainer').style.display = 'none';
|
||||
}
|
||||
|
||||
// Показываем/скрываем summary
|
||||
if (hasDiscounts) {
|
||||
summaryDiv.style.display = 'block';
|
||||
document.getElementById('discountsSubtotal').textContent =
|
||||
(result.cart_subtotal || 0).toFixed(2) + ' руб.';
|
||||
document.getElementById('discountsTotalDiscount').textContent =
|
||||
'-' + (result.total_discount || 0).toFixed(2) + ' руб.';
|
||||
} else {
|
||||
summaryDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Обновляем итоговую цену
|
||||
const finalTotal = Math.max(0, (result.cart_subtotal || 0) - (result.total_discount || 0));
|
||||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.';
|
||||
|
||||
// Пересчитываем виджет оплаты
|
||||
reinitPaymentWidget(document.getElementById('singlePaymentMode').classList.contains('active') ? 'single' : 'mixed');
|
||||
}
|
||||
|
||||
// Загрузить доступные скидки
|
||||
async function loadAvailableDiscounts() {
|
||||
try {
|
||||
let cartTotal = 0;
|
||||
cart.forEach((item) => {
|
||||
cartTotal += item.qty * item.price;
|
||||
});
|
||||
|
||||
const response = await fetch(`/pos/api/discounts/available/?cart_total=${cartTotal}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
availableDiscounts = result.order_discounts;
|
||||
renderDiscountsDropdown(result.order_discounts);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки скидок:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Отобразить список скидок в dropdown
|
||||
function renderDiscountsDropdown(discounts) {
|
||||
const list = document.getElementById('discountsDropdownList');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (discounts.length === 0) {
|
||||
list.innerHTML = '<li><span class="dropdown-item-text small text-muted">Нет доступных скидок</span></li>';
|
||||
return;
|
||||
}
|
||||
|
||||
discounts.forEach(d => {
|
||||
const li = document.createElement('li');
|
||||
const valueText = d.discount_type === 'percentage' ? `${d.value}%` : `${d.value} руб.`;
|
||||
const minText = d.min_order_amount ? `(от ${d.min_order_amount} руб.)` : '';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = '#';
|
||||
a.className = 'dropdown-item d-flex justify-content-between';
|
||||
a.innerHTML = `
|
||||
<span>${d.name}</span>
|
||||
<span class="text-success">${valueText} ${minText}</span>
|
||||
`;
|
||||
a.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
applyManualDiscount(d);
|
||||
};
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Применить скидку вручную
|
||||
async function applyManualDiscount(discount) {
|
||||
// Рассчитываем сумму скидки на клиенте (для отображения до ответа сервера)
|
||||
let cartTotal = 0;
|
||||
cart.forEach((item) => {
|
||||
cartTotal += item.qty * item.price;
|
||||
});
|
||||
let discountAmount = discount.discount_type === 'percentage'
|
||||
? cartTotal * (discount.value / 100)
|
||||
: discount.value;
|
||||
|
||||
appliedManualDiscount = { ...discount, amount: discountAmount };
|
||||
await checkAutoDiscounts();
|
||||
await loadAvailableDiscounts();
|
||||
}
|
||||
|
||||
// Удалить ручную скидку
|
||||
document.getElementById('removeManualDiscountBtn').addEventListener('click', async () => {
|
||||
appliedManualDiscount = null;
|
||||
await checkAutoDiscounts();
|
||||
await loadAvailableDiscounts();
|
||||
});
|
||||
|
||||
// Применить промокод
|
||||
async function applyPromoCode() {
|
||||
const code = document.getElementById('promoCodeInput').value.trim().toUpperCase();
|
||||
@@ -2455,7 +2606,7 @@ async function applyPromoCode() {
|
||||
if (result.success) {
|
||||
appliedPromoCode = result.promo_code;
|
||||
|
||||
// Пересчитываем скидки с промокодом
|
||||
// Пересчитываем скидки с промокодом (updateDiscountsUI обновит UI)
|
||||
await recalculateDiscountsWithPromo(code);
|
||||
|
||||
// Показываем успех
|
||||
@@ -2464,9 +2615,6 @@ async function applyPromoCode() {
|
||||
document.getElementById('promoCodeSuccess').style.display = 'block';
|
||||
document.getElementById('promoCodeError').style.display = 'none';
|
||||
document.getElementById('removePromoBtn').style.display = 'inline-block';
|
||||
|
||||
// Обновляем итоговую сумму
|
||||
updateCheckoutTotalWithDiscounts();
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
document.getElementById('promoCodeError').textContent = result.error || 'Неверный промокод';
|
||||
@@ -2511,6 +2659,7 @@ async function recalculateDiscountsWithPromo(promoCode) {
|
||||
cartDiscounts.totalDiscount = result.total_discount || 0;
|
||||
cartDiscounts.orderDiscount = result.order_discount;
|
||||
cartDiscounts.itemDiscounts = result.item_discounts || [];
|
||||
updateDiscountsUI(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при пересчёте скидок:', error);
|
||||
@@ -2539,12 +2688,11 @@ function updateCheckoutTotalWithDiscounts() {
|
||||
function removePromoCode() {
|
||||
appliedPromoCode = null;
|
||||
|
||||
// Пересчитываем без промокода
|
||||
// Пересчитываем без промокода (updateDiscountsUI обновит UI)
|
||||
recalculateDiscountsWithPromo(null).then(() => {
|
||||
document.getElementById('promoCodeInput').value = '';
|
||||
document.getElementById('removePromoBtn').style.display = 'none';
|
||||
document.getElementById('promoCodeSuccess').style.display = 'none';
|
||||
updateCheckoutTotalWithDiscounts();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2629,7 +2777,8 @@ async function handleCheckoutSubmit(paymentsData) {
|
||||
}),
|
||||
payments: paymentsData,
|
||||
notes: document.getElementById('orderNote').value.trim(),
|
||||
promo_code: appliedPromoCode || null
|
||||
promo_code: appliedPromoCode?.code || null,
|
||||
manual_discount_id: appliedManualDiscount?.id || null
|
||||
};
|
||||
|
||||
// Отправляем на сервер
|
||||
|
||||
Reference in New Issue
Block a user