style(pos): улучшить адаптивность сетки товаров

- Изменить брейкпоинт для 5 колонок с 992px на 1100px
- Увеличить ширину правой панели с 4/12 до 5/12 колонок

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 03:11:00 +03:00
parent 18cca326af
commit 928b340486
4 changed files with 437 additions and 41 deletions

View File

@@ -0,0 +1,277 @@
/**
* Inline Price Editing Module
* Переиспользуемый модуль для редактирования цен товаров прямо на странице
* Используется на страницах: products_list.html, catalog.html
*/
(function() {
'use strict';
/**
* Нормализует ввод цены: заменяет запятую на точку
* @param {string} value - введенное значение
* @returns {string} - нормализованное значение
*/
function normalizePrice(value) {
if (typeof value === 'string') {
return value.replace(',', '.');
}
return value;
}
/**
* Форматирует число для отображения (с запятой как разделителем)
* @param {string|number} value - значение
* @returns {string} - отформатированное значение с запятой
*/
function formatPrice(value) {
return parseFloat(value).toFixed(2).replace('.', ',');
}
/**
* Получает CSRF токен из DOM или cookies
* @returns {string} - CSRF токен
*/
function getCsrfToken() {
// Сначала пытаемся найти токен в DOM (из {% csrf_token %})
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (csrfInput) {
return csrfInput.value;
}
// Fallback: пытаемся прочитать из cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === 'csrftoken') {
return decodeURIComponent(value);
}
}
return '';
}
/**
* Обновляет отображение цен для товара
* @param {number} productId - ID товара
* @param {string} price - основная цена
* @param {string|null} salePrice - цена со скидкой
*/
function updatePriceDisplay(productId, price, salePrice) {
// Находим все контейнеры с ценами для этого товара
const priceContainers = document.querySelectorAll(
`.editable-price[data-product-id="${productId}"]`
);
priceContainers.forEach(container => {
const parentContainer = container.closest('.price-edit-container, td');
if (!parentContainer) return;
// Формируем новый HTML
let newHTML = '';
if (salePrice) {
// Есть скидочная цена
newHTML = `
<div class="text-decoration-line-through text-muted small">${formatPrice(price)} руб.</div>
<strong class="text-danger editable-price"
data-product-id="${productId}"
data-field="sale_price"
data-current-value="${salePrice}"
title="Цена со скидкой (клик для редактирования)"
style="cursor: pointer;">
${formatPrice(salePrice)} руб.
</strong>
`;
} else {
// Только обычная цена
newHTML = `
<strong class="editable-price"
data-product-id="${productId}"
data-field="price"
data-current-value="${price}"
title="Цена продажи (клик для редактирования)"
style="cursor: pointer;">
${formatPrice(price)} руб.
</strong>
`;
}
parentContainer.innerHTML = newHTML;
});
}
/**
* Отправляет обновление цены на сервер
* @param {number} productId - ID товара
* @param {string} field - поле (price или sale_price)
* @param {string|null} value - новое значение
* @returns {Promise<Object>} - результат обновления
*/
async function updatePriceViaAPI(productId, field, value) {
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
})
});
return await response.json();
}
/**
* Инициализирует inline-редактирование цен
*/
function initInlinePriceEdit() {
// Проверяем, есть ли на странице редактируемые цены
const editablePrices = document.querySelectorAll('.editable-price');
if (editablePrices.length === 0) {
return; // Нет элементов для редактирования
}
// Обработчик клика на редактируемую цену
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;
const currentValue = priceSpan.dataset.currentValue;
// Сохраняем оригинальный HTML
const originalHTML = priceSpan.innerHTML;
// Создаем input для редактирования
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm';
input.style.width = '100px';
input.style.display = 'inline-block';
input.value = parseFloat(currentValue).toFixed(2);
input.placeholder = 'Цена';
// Заменяем содержимое на input
priceSpan.innerHTML = '';
priceSpan.appendChild(input);
input.focus();
input.select();
// Функция сохранения
const savePrice = async () => {
let newValue = input.value.trim();
// Нормализуем ввод (запятая -> точка)
newValue = normalizePrice(newValue);
// Валидация
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
})
});
// Проверяем статус ответа
if (!response.ok) {
// Обработка HTTP ошибок
if (response.status === 403) {
alert('У вас нет прав для изменения цен товаров');
} else if (response.status === 404) {
alert('Товар не найден');
} else {
// Пытаемся получить текст ошибки из JSON
try {
const errorData = await response.json();
alert(errorData.error || `Ошибка ${response.status}: ${response.statusText}`);
} catch {
alert(`Ошибка ${response.status}: ${response.statusText}`);
}
}
priceSpan.innerHTML = originalHTML;
return;
}
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);
});
});
}
// Инициализация при загрузке DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initInlinePriceEdit);
} else {
initInlinePriceEdit();
}
// Экспортируем функции для использования извне
window.InlinePriceEdit = {
init: initInlinePriceEdit,
updateDisplay: updatePriceDisplay,
normalizePrice: normalizePrice
};
})();