diff --git a/myproject/products/static/products/js/catalog.js b/myproject/products/static/products/js/catalog.js index ad05756..0a9a2b1 100644 --- a/myproject/products/static/products/js/catalog.js +++ b/myproject/products/static/products/js/catalog.js @@ -562,6 +562,238 @@ document.addEventListener('DOMContentLoaded', function() { } } + // ======================================== + // Inline-редактирование цен (только для Product, не для ProductKit) + // ======================================== + + // Обработчик клика на цене (редактирование) + document.addEventListener('click', function(e) { + const priceSpan = e.target.closest('.editable-price'); + if (!priceSpan) return; + + // Уже редактируется? + if (priceSpan.querySelector('input')) return; + + const productId = priceSpan.dataset.productId; + const field = priceSpan.dataset.field; // 'price' или 'sale_price' + const currentValue = priceSpan.dataset.currentValue; + + // Создаем input + const input = document.createElement('input'); + input.type = 'number'; + input.step = '0.01'; + input.min = '0'; + input.value = currentValue; + input.className = 'price-edit-input'; + + // Сохраняем оригинальный HTML + const originalHTML = priceSpan.innerHTML; + priceSpan.innerHTML = ''; + priceSpan.appendChild(input); + input.focus(); + input.select(); + + // Функция сохранения + const savePrice = async () => { + const newValue = input.value.trim(); + + // Валидация + if (!newValue || parseFloat(newValue) < 0) { + // Отмена - пустое или отрицательное значение + priceSpan.innerHTML = originalHTML; + return; + } + + // Проверяем, изменилось ли значение + if (parseFloat(newValue) === parseFloat(currentValue)) { + // Значение не изменилось + priceSpan.innerHTML = originalHTML; + return; + } + + // Показываем загрузку + input.disabled = true; + input.style.opacity = '0.5'; + + try { + const response = await fetch(`/products/api/products/${productId}/update-price/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + }, + body: JSON.stringify({ + field: field, + value: newValue + }) + }); + + const data = await response.json(); + + if (data.success) { + // Обновляем весь контейнер с ценами + updatePriceDisplay(productId, data.price, data.sale_price); + } else { + alert(data.error || 'Ошибка при обновлении цены'); + priceSpan.innerHTML = originalHTML; + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка сети'); + priceSpan.innerHTML = originalHTML; + } + }; + + // Функция отмены + const cancelEdit = () => { + priceSpan.innerHTML = originalHTML; + }; + + // Enter - сохранить + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + savePrice(); + } else if (e.key === 'Escape') { + e.preventDefault(); + cancelEdit(); + } + }); + + // Потеря фокуса - сохранить + input.addEventListener('blur', function() { + setTimeout(savePrice, 100); + }); + }); + + // Добавление скидочной цены + document.addEventListener('click', function(e) { + const addBtn = e.target.closest('.add-sale-price'); + if (!addBtn) return; + + const productId = addBtn.dataset.productId; + const priceContainer = addBtn.closest('.price-edit-container'); + const regularPriceSpan = priceContainer.querySelector('.editable-price[data-field="price"]'); + const currentPrice = parseFloat(regularPriceSpan.dataset.currentValue); + + // Запрашиваем скидочную цену + const salePriceInput = prompt('Введите скидочную цену (меньше ' + currentPrice.toFixed(2) + ' руб.):', ''); + + if (!salePriceInput) return; // Отменено + + const salePrice = parseFloat(salePriceInput); + + // Валидация + if (isNaN(salePrice) || salePrice <= 0) { + alert('Некорректная цена'); + return; + } + + if (salePrice >= currentPrice) { + alert('Скидочная цена должна быть меньше обычной цены (' + currentPrice.toFixed(2) + ' руб.)'); + return; + } + + // Сохраняем + updatePriceViaAPI(productId, 'sale_price', salePrice.toFixed(2)); + }); + + // Удаление скидочной цены + document.addEventListener('click', function(e) { + const removeBtn = e.target.closest('.remove-sale-price'); + if (!removeBtn) return; + + const productId = removeBtn.dataset.productId; + + if (!confirm('Убрать скидочную цену?')) return; + + // Отправляем null для удаления sale_price + updatePriceViaAPI(productId, 'sale_price', null); + }); + + // Вспомогательная функция для обновления цены через API + async function updatePriceViaAPI(productId, field, value) { + try { + const response = await fetch(`/products/api/products/${productId}/update-price/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + }, + body: JSON.stringify({ + field: field, + value: value + }) + }); + + const data = await response.json(); + + if (data.success) { + // Обновляем отображение + updatePriceDisplay(productId, data.price, data.sale_price); + } else { + alert(data.error || 'Ошибка при обновлении цены'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка сети'); + } + } + + // Функция обновления отображения цен + function updatePriceDisplay(productId, price, salePrice) { + // Находим контейнер с ценами для этого товара + const catalogItem = document.querySelector(`.catalog-item .editable-price[data-product-id="${productId}"]`)?.closest('.price-edit-container'); + + if (!catalogItem) return; + + // Формируем новый HTML + let newHTML = ''; + + if (salePrice) { + // Есть скидочная цена + newHTML = ` + + ${parseFloat(salePrice).toFixed(2)} руб. + + + ${parseFloat(price).toFixed(2)} руб. + + + `; + } else { + // Только обычная цена + newHTML = ` + + ${parseFloat(price).toFixed(2)} руб. + + + `; + } + + catalogItem.innerHTML = newHTML; + } + // Получение CSRF токена function getCsrfToken() { const cookieValue = document.cookie diff --git a/myproject/products/templates/products/catalog.html b/myproject/products/templates/products/catalog.html index 6f03d27..8bf471b 100644 --- a/myproject/products/templates/products/catalog.html +++ b/myproject/products/templates/products/catalog.html @@ -140,6 +140,56 @@ opacity: 1 !important; color: #198754; } + /* Редактируемые цены */ + .editable-price { + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + transition: background-color 0.15s, border-color 0.15s; + border: 1px solid transparent; + } + .editable-price:hover { + background-color: #f8f9fa; + border: 1px dashed #dee2e6; + } + .editable-price.sale-price:hover { + background-color: #d1f4e0; + border-color: #28a745; + } + .editable-price.regular-price:hover { + background-color: #e7f1ff; + border-color: #0d6efd; + } + .price-edit-input { + width: 90px; + font-size: 0.85rem; + padding: 2px 6px; + border: 2px solid #0d6efd; + border-radius: 3px; + text-align: right; + } + .price-edit-input:focus { + outline: none; + border-color: #0a58ca; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); + } + .remove-sale-price { + opacity: 0; + transition: opacity 0.2s; + } + .price-edit-container:hover .remove-sale-price { + opacity: 0.5; + } + .remove-sale-price:hover { + opacity: 1 !important; + } + .add-sale-price { + opacity: 0; + transition: opacity 0.2s; + } + .price-edit-container:hover .add-sale-price { + opacity: 1; + } {% endblock %} @@ -217,7 +267,52 @@ {% endif %}