').text(customer.phone).html());
}
if (customer.email) {
contactInfo.push($('
').text(customer.email).html());
}
if (contactInfo.length > 0) {
parts.push('
(' + contactInfo.join(', ') + ')');
}
return $('
' + parts.join('') + '');
}
/**
* Форматирование выбранного клиента в поле Select2
* Показывает только имя
*/
function formatCustomerSelection(customer) {
return customer.name || customer.text;
}
/**
* Открывает модальное окно создания нового клиента
* @param {string} prefillName - Предзаполненное имя (из поиска)
*/
function openCreateCustomerModal(prefillName = '') {
const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
// Очищаем форму
document.getElementById('newCustomerName').value = prefillName || '';
document.getElementById('newCustomerPhone').value = '';
document.getElementById('newCustomerEmail').value = '';
document.getElementById('createCustomerError').classList.add('d-none');
modal.show();
}
/**
* Создаёт нового клиента через API
*/
async function createNewCustomer() {
const name = document.getElementById('newCustomerName').value.trim();
const phone = document.getElementById('newCustomerPhone').value.trim();
const email = document.getElementById('newCustomerEmail').value.trim();
const errorBlock = document.getElementById('createCustomerError');
// Валидация
if (!name) {
errorBlock.textContent = 'Укажите имя клиента';
errorBlock.classList.remove('d-none');
return;
}
// Скрываем ошибку
errorBlock.classList.add('d-none');
try {
const response = await fetch('/customers/api/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
name: name,
phone: phone || null,
email: email || null
})
});
const data = await response.json();
if (data.success) {
// Выбираем созданного клиента с балансом
selectCustomer(data.id, data.name, data.wallet_balance || 0);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
modal.hide();
// Показываем уведомление
alert(`Клиент "${data.name}" успешно создан!`);
} else {
// Показываем ошибку
errorBlock.textContent = data.error || 'Ошибка при создании клиента';
errorBlock.classList.remove('d-none');
}
} catch (error) {
console.error('Error creating customer:', error);
errorBlock.textContent = 'Ошибка сети при создании клиента';
errorBlock.classList.remove('d-none');
}
}
// ===== ФУНКЦИИ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
/**
* Открывает модальное окно выбора единицы продажи
* @param {object} product - Объект товара с информацией о единицах продажи
*/
async function openProductUnitModal(product) {
unitModalProduct = product;
// Устанавливаем название товара
document.getElementById('unitModalProductName').textContent =
`${product.name}${product.sku ? ' (' + product.sku + ')' : ''}`;
// Загружаем единицы продажи
try {
const response = await fetch(
`/products/api/products/${product.id}/sales-units/?warehouse=${currentWarehouse.id}`
);
const data = await response.json();
if (!data.success || !data.sales_units || data.sales_units.length === 0) {
alert('Не удалось загрузить единицы продажи');
return;
}
unitModalSalesUnits = data.sales_units;
// Отрисовываем список единиц
renderUnitSelectionList();
// Выбираем единицу по умолчанию или первую
const defaultUnit = unitModalSalesUnits.find(u => u.is_default) || unitModalSalesUnits[0];
if (defaultUnit) {
selectUnit(defaultUnit);
}
// Открываем модальное окно
if (!unitModalInstance) {
unitModalInstance = new bootstrap.Modal(document.getElementById('selectProductUnitModal'));
}
unitModalInstance.show();
} catch (error) {
console.error('Ошибка загрузки единиц продажи:', error);
alert('Ошибка загрузки данных. Попробуйте ещё раз.');
}
}
/**
* Отрисовывает список единиц продажи
*/
function renderUnitSelectionList() {
const listContainer = document.getElementById('unitSelectionList');
listContainer.innerHTML = '';
unitModalSalesUnits.forEach(unit => {
const card = document.createElement('div');
card.className = 'unit-selection-card';
card.dataset.unitId = unit.id;
card.onclick = () => selectUnit(unit);
// Доступное количество
const availableQty = parseFloat(unit.available_quantity || 0);
let stockBadgeClass = 'stock-badge-none';
let stockText = 'Нет на складе';
if (availableQty > 10) {
stockBadgeClass = 'stock-badge-good';
stockText = `${availableQty} шт доступно`;
} else if (availableQty > 0) {
stockBadgeClass = 'stock-badge-low';
stockText = `${availableQty} шт доступно`;
}
// Бейдж "По умолчанию"
const defaultBadge = unit.is_default ?
'
По умолчанию' : '';
card.innerHTML = `
${unit.name}${defaultBadge}
${formatMoney(unit.actual_price)} руб
${stockText}
`;
listContainer.appendChild(card);
});
}
/**
* Выбирает единицу продажи
* @param {object} unit - Объект единицы продажи
*/
function selectUnit(unit) {
selectedSalesUnit = unit;
// Обновляем визуальное выделение
document.querySelectorAll('.unit-selection-card').forEach(card => {
if (card.dataset.unitId === String(unit.id)) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
// Обновляем отображение выбранной единицы
document.getElementById('selectedUnitDisplay').textContent =
unit.name;
// Устанавливаем минимальное количество и шаг
const qtyInput = document.getElementById('unitModalQuantity');
qtyInput.value = roundQuantity(unit.min_quantity, 3);
qtyInput.min = unit.min_quantity;
qtyInput.step = unit.quantity_step;
// Устанавливаем цену
document.getElementById('unitModalPrice').value = unit.actual_price;
// Обновляем подсказку
const hintEl = document.getElementById('unitQtyHint');
hintEl.textContent = `Мин. ${unit.min_quantity}, шаг ${unit.quantity_step}`;
// Сбрасываем индикатор изменения цены
document.getElementById('priceOverrideIndicator').style.display = 'none';
// Пересчитываем итого
calculateUnitModalSubtotal();
// Валидируем количество
validateUnitQuantity();
}
/**
* Проверяет количество на соответствие ограничениям
* @returns {boolean} - true если валидно
*/
function validateUnitQuantity() {
if (!selectedSalesUnit) return false;
const qtyInput = document.getElementById('unitModalQuantity');
const qty = parseFloat(qtyInput.value);
const errorEl = document.getElementById('unitQtyError');
const confirmBtn = document.getElementById('confirmAddUnitToCart');
// Проверка минимального количества
if (qty < parseFloat(selectedSalesUnit.min_quantity)) {
errorEl.textContent = `Минимальное количество: ${selectedSalesUnit.min_quantity}`;
errorEl.style.display = 'block';
confirmBtn.disabled = true;
return false;
}
// Проверка шага (с учётом погрешности)
const step = parseFloat(selectedSalesUnit.quantity_step);
const minQty = parseFloat(selectedSalesUnit.min_quantity);
const diff = qty - minQty;
const remainder = diff % step;
const epsilon = 0.0001;
if (remainder > epsilon && (step - remainder) > epsilon) {
errorEl.textContent = `Количество должно быть кратно ${step}`;
errorEl.style.display = 'block';
confirmBtn.disabled = true;
return false;
}
// Всё ок, скрываем ошибку
errorEl.style.display = 'none';
confirmBtn.disabled = false;
return true;
}
/**
* Рассчитывает итоговую сумму
*/
function calculateUnitModalSubtotal() {
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value) || 0;
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
const price = parseFloat(document.getElementById('unitModalPrice').value) || 0;
// Округляем до 2 знаков после запятой для корректного отображения
const subtotal = Math.round(qty * price * 100) / 100;
document.getElementById('unitModalSubtotal').textContent = `${formatMoney(subtotal)} руб`;
// Проверяем изменение цены
if (selectedSalesUnit && Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01) {
document.getElementById('priceOverrideIndicator').style.display = 'block';
} else {
document.getElementById('priceOverrideIndicator').style.display = 'none';
}
}
/**
* Добавляет товар с выбранной единицей в корзину
*/
function addToCartFromModal() {
if (!validateUnitQuantity()) {
return;
}
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value);
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
const price = parseFloat(document.getElementById('unitModalPrice').value);
const priceOverridden = Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01;
// Формируем ключ корзины: product-{id}-{sales_unit_id}
const cartKey = `product-${unitModalProduct.id}-${selectedSalesUnit.id}`;
// Добавляем или обновляем в корзине
if (cart.has(cartKey)) {
const existing = cart.get(cartKey);
existing.qty = roundQuantity(existing.qty + qty, 3); // Округляем сумму
existing.price = price; // Обновляем цену
existing.quantity_step = parseFloat(selectedSalesUnit.quantity_step) || 1; // Обновляем шаг
existing.price_overridden = priceOverridden;
} else {
cart.set(cartKey, {
id: unitModalProduct.id,
name: unitModalProduct.name,
price: price,
qty: qty,
type: 'product',
sales_unit_id: selectedSalesUnit.id,
unit_name: selectedSalesUnit.name,
quantity_step: parseFloat(selectedSalesUnit.quantity_step) || 1, // Сохраняем шаг количества
price_overridden: priceOverridden
});
}
// Обновляем корзину
renderCart();
saveCartToRedis();
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView) {
renderProducts();
}
// Закрываем модальное окно
unitModalInstance.hide();
}
/**
* Добавляет товар с единицей продажи напрямую в корзину (без модального окна)
* Используется для быстрого добавления когда у товара только одна единица продажи
* @param {object} product - Товар из ITEMS
* @param {object} salesUnit - Единица продажи (default_sales_unit)
* @param {number} qty - Количество для добавления
*/
async function addProductWithUnitToCart(product, salesUnit, qty = 1) {
const cartKey = `product-${product.id}-${salesUnit.id}`;
if (cart.has(cartKey)) {
const existing = cart.get(cartKey);
existing.qty = roundQuantity(existing.qty + qty, 3);
} else {
cart.set(cartKey, {
id: product.id,
name: product.name,
price: Number(salesUnit.price),
qty: qty,
type: 'product',
sales_unit_id: salesUnit.id,
unit_name: salesUnit.name,
quantity_step: parseFloat(salesUnit.quantity_step) || 1
});
}
renderCart();
saveCartToRedis();
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView) {
renderProducts();
}
// Фокус на поле количества
setTimeout(() => {
const qtyInputs = document.querySelectorAll('.qty-input');
const itemIndex = Array.from(cart.keys()).indexOf(cartKey);
if (itemIndex !== -1 && qtyInputs[itemIndex]) {
if (!isMobileDevice()) {
qtyInputs[itemIndex].focus();
qtyInputs[itemIndex].select();
}
}
}, 50);
}
function renderCategories() {
const grid = document.getElementById('categoryGrid');
grid.innerHTML = '';
// Кнопка "Витрина" - первая в ряду
const showcaseCol = document.createElement('div');
showcaseCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
const showcaseCard = document.createElement('div');
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
showcaseCard.style.backgroundColor = '#fff3cd';
showcaseCard.style.borderColor = '#ffc107';
showcaseCard.onclick = async () => {
isShowcaseView = true;
currentCategoryId = null;
await refreshShowcaseKits(); // Загружаем свежие данные
renderCategories();
renderProducts();
};
const showcaseBody = document.createElement('div');
showcaseBody.className = 'card-body';
const showcaseName = document.createElement('div');
showcaseName.className = 'category-name';
showcaseName.innerHTML = '
ВИТРИНА';
showcaseBody.appendChild(showcaseName);
showcaseCard.appendChild(showcaseBody);
showcaseCol.appendChild(showcaseCard);
grid.appendChild(showcaseCol);
// Кнопка "Все"
const allCol = document.createElement('div');
allCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
const allCard = document.createElement('div');
allCard.className = 'card category-card' + (currentCategoryId === null && !isShowcaseView ? ' active' : '');
allCard.onclick = async () => {
currentCategoryId = null;
isShowcaseView = false;
currentSearchQuery = ''; // Сбрасываем поиск
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
renderCategories();
await loadItems(); // Загрузка через API
};
const allBody = document.createElement('div');
allBody.className = 'card-body';
const allName = document.createElement('div');
allName.className = 'category-name';
allName.textContent = 'Все товары';
allBody.appendChild(allName);
allCard.appendChild(allBody);
allCol.appendChild(allCard);
grid.appendChild(allCol);
// Категории
CATEGORIES.forEach(cat => {
const col = document.createElement('div');
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
const card = document.createElement('div');
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
card.onclick = async () => {
currentCategoryId = cat.id;
isShowcaseView = false;
currentSearchQuery = ''; // Сбрасываем поиск
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
renderCategories();
await loadItems(); // Загрузка через API
};
const body = document.createElement('div');
body.className = 'card-body';
const name = document.createElement('div');
name.className = 'category-name';
name.textContent = cat.name;
body.appendChild(name);
card.appendChild(body);
col.appendChild(card);
grid.appendChild(col);
});
}
function renderProducts() {
const grid = document.getElementById('productGrid');
grid.innerHTML = '';
let filtered;
// Если выбран режим витрины - показываем витринные комплекты
if (isShowcaseView) {
filtered = showcaseKits;
// Для витрины — клиентская фильтрация по поиску
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
if (searchTerm) {
const tokens = searchTerm.split(/\s+/).filter(t => t.length > 0);
filtered = filtered.filter(item => {
const name = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
// Каждый токен должен совпадать хотя бы с одним словом в name или sku
return tokens.every(token => name.includes(token) || sku.includes(token));
});
}
} else {
// Обычный режим - ITEMS уже отфильтрованы на сервере (категория + поиск)
filtered = ITEMS;
}
filtered.forEach(item => {
const col = document.createElement('div');
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
const card = document.createElement('div');
card.className = 'card product-card';
card.style.position = 'relative';
card.onclick = () => addToCart(item);
// Если это витринный комплект - добавляем кнопку редактирования
if (item.type === 'showcase_kit') {
// ИНДИКАЦИЯ БЛОКИРОВКИ
if (item.is_locked) {
// Создаем бейдж блокировки
const lockBadge = document.createElement('div');
lockBadge.style.position = 'absolute';
lockBadge.style.top = '5px';
lockBadge.style.left = '5px';
lockBadge.style.zIndex = '10';
if (item.locked_by_me) {
// Заблокирован мной - зеленый бейдж
lockBadge.className = 'badge bg-success';
lockBadge.innerHTML = '
В корзине';
lockBadge.title = 'Добавлен в вашу корзину';
} else {
// Заблокирован другим кассиром - красный бейдж + блокируем карточку
lockBadge.className = 'badge bg-danger';
lockBadge.innerHTML = '
Занят';
lockBadge.title = `В корзине ${item.locked_by_user}`;
// Затемняем карточку и блокируем клики
card.style.opacity = '0.5';
card.style.cursor = 'not-allowed';
card.onclick = (e) => {
e.stopPropagation();
alert(`Этот букет уже в корзине кассира "${item.locked_by_user}".\nДождитесь освобождения блокировки.`);
};
}
card.appendChild(lockBadge);
}
// Кнопка редактирования (только если НЕ заблокирован другим)
if (!item.is_locked || item.locked_by_me) {
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-sm btn-outline-primary';
editBtn.style.position = 'absolute';
editBtn.style.top = '5px';
editBtn.style.right = '5px';
editBtn.style.zIndex = '10';
editBtn.innerHTML = '
';
editBtn.onclick = (e) => {
e.stopPropagation();
openEditKitModal(item.id);
};
card.appendChild(editBtn);
// Индикатор неактуальной цены (красный кружок)
if (item.price_outdated) {
const outdatedBadge = document.createElement('div');
outdatedBadge.className = 'badge bg-danger';
outdatedBadge.style.position = 'absolute';
outdatedBadge.style.top = '5px';
outdatedBadge.style.right = '45px';
outdatedBadge.style.zIndex = '10';
outdatedBadge.style.width = '18px';
outdatedBadge.style.height = '18px';
outdatedBadge.style.padding = '0';
outdatedBadge.style.borderRadius = '50%';
outdatedBadge.style.display = 'flex';
outdatedBadge.style.alignItems = 'center';
outdatedBadge.style.justifyContent = 'center';
outdatedBadge.style.fontSize = '10px';
outdatedBadge.style.minWidth = '18px';
outdatedBadge.title = 'Цена неактуальна';
outdatedBadge.innerHTML = '!';
card.appendChild(outdatedBadge);
}
}
}
const body = document.createElement('div');
body.className = 'card-body';
// Изображение товара/комплекта
const imageDiv = document.createElement('div');
imageDiv.className = 'product-image';
if (item.image) {
const img = document.createElement('img');
img.src = item.image;
img.alt = item.name;
img.loading = 'lazy'; // Lazy loading
imageDiv.appendChild(img);
} else {
imageDiv.innerHTML = '
';
}
// Информация о товаре/комплекте
const info = document.createElement('div');
info.className = 'product-info';
const name = document.createElement('div');
name.className = 'product-name';
name.textContent = item.name;
const stock = document.createElement('div');
stock.className = 'product-stock';
// Для витринных комплектов показываем название витрины И количество (доступно/всего)
if (item.type === 'showcase_kit') {
const availableCount = item.available_count || 0;
const totalCount = item.total_count || availableCount;
const inCart = totalCount - availableCount;
// Показываем: доступно / всего (и сколько в корзине)
let badgeClass = availableCount > 0 ? 'bg-success' : 'bg-secondary';
let badgeText = totalCount > 1 ? `${availableCount}/${totalCount}` : `${availableCount}`;
let cartInfo = inCart > 0 ? `
🛒${inCart}` : '';
stock.innerHTML = `🌺 ${item.showcase_name}
${badgeText}${cartInfo}`;
stock.style.color = '#856404';
stock.style.fontWeight = 'bold';
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
// Для обычных товаров показываем остатки
// Если у товара есть единицы продажи - отображаем в единицах продажи
const available = parseFloat(item.available_qty) || 0;
const reserved = parseFloat(item.reserved_qty) || 0;
// Используем единицу продажи если есть
if (item.default_sales_unit) {
const unit = item.default_sales_unit;
const conversionFactor = parseFloat(unit.conversion_factor) || 1;
// Вычисляем количество в корзине в единицах продажи
const cartKey = `product-${item.id}-${unit.id}`;
const inCartBaseQty = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
// Свободное количество в единицах продажи
const availableInUnit = parseFloat(item.available_qty_in_unit) || 0;
const reservedInUnit = reserved * conversionFactor;
const freeInUnit = availableInUnit - reservedInUnit - inCartBaseQty;
const freeRounded = roundQuantity(freeInUnit, 1); // Округляем для отображения
// Создаём элементы для стилизации
const freeSpan = document.createElement('span');
freeSpan.style.fontSize = '1.1em';
freeSpan.style.fontWeight = 'bold';
const qtyText = document.createElement('span');
qtyText.textContent = freeRounded;
freeSpan.appendChild(qtyText);
const unitBadge = document.createElement('span');
unitBadge.className = 'badge bg-secondary ms-1';
unitBadge.style.fontSize = '0.7rem';
unitBadge.textContent = unit.name;
freeSpan.appendChild(unitBadge);
// Отображаем корзину если есть
if (inCartBaseQty > 0) {
const suffixSpan = document.createElement('span');
suffixSpan.textContent = ` (−${roundQuantity(inCartBaseQty, 1)}🛒)`;
suffixSpan.style.fontSize = '0.85em';
suffixSpan.style.marginLeft = '3px';
stock.appendChild(freeSpan);
stock.appendChild(suffixSpan);
} else {
stock.appendChild(freeSpan);
}
// Цветовая индикация
if (freeInUnit <= 0) {
stock.style.color = '#dc3545'; // Красный
} else if (freeInUnit < 5) {
stock.style.color = '#ffc107'; // Жёлтый
} else {
stock.style.color = '#28a745'; // Зелёный
}
} else {
// Отображение в базовых единицах (старая логика)
const cartKey = `product-${item.id}`;
const inCart = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
const free = available - reserved - inCart;
const freeRounded = roundQuantity(free, 3);
const freeSpan = document.createElement('span');
freeSpan.textContent = freeRounded;
freeSpan.style.fontSize = '1.1em';
freeSpan.style.fontWeight = 'bold';
const suffixParts = [];
if (reserved > 0) {
suffixParts.push(`−${roundQuantity(reserved, 3)}`);
}
if (inCart > 0) {
suffixParts.push(`−${roundQuantity(inCart, 3)}🛒`);
}
if (suffixParts.length > 0) {
const suffixSpan = document.createElement('span');
suffixSpan.textContent = `(${suffixParts.join(' ')})`;
suffixSpan.style.fontSize = '0.85em';
suffixSpan.style.marginLeft = '3px';
stock.appendChild(freeSpan);
stock.appendChild(suffixSpan);
} else {
stock.appendChild(freeSpan);
}
if (free <= 0) {
stock.style.color = '#dc3545';
} else if (free < 5) {
stock.style.color = '#ffc107';
} else {
stock.style.color = '#28a745';
}
}
} else {
// Комплекты: показываем доступное количество
if (item.type === 'kit' && item.free_qty !== undefined) {
const availableKits = parseFloat(item.free_qty) || 0;
if (availableKits > 0) {
stock.textContent = `В наличии: ${Math.floor(availableKits)} компл.`;
stock.style.color = '#28a745'; // Зелёный
} else {
stock.textContent = 'Под заказ';
stock.style.color = '#dc3545'; // Красный
}
} else {
// Fallback для старых данных
stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ';
if (!item.in_stock) {
stock.style.color = '#dc3545';
}
}
}
const sku = document.createElement('div');
sku.className = 'product-sku';
const skuText = document.createElement('span');
skuText.textContent = item.sku || 'н/д';
const priceSpan = document.createElement('span');
priceSpan.className = 'product-price';
// Используем цену из единицы продажи если есть, иначе базовую цену
const itemPrice = item.default_sales_unit ? item.price_in_unit : item.price;
priceSpan.textContent = `${formatMoney(itemPrice)}`;
sku.appendChild(skuText);
sku.appendChild(priceSpan);
info.appendChild(name);
info.appendChild(stock);
info.appendChild(sku);
body.appendChild(imageDiv);
body.appendChild(info);
card.appendChild(body);
col.appendChild(card);
grid.appendChild(col);
});
}
// Загрузка товаров через API
async function loadItems(append = false) {
if (isLoadingItems) return;
isLoadingItems = true;
if (!append) {
currentPage = 1;
ITEMS = [];
}
try {
const params = new URLSearchParams({
page: currentPage,
page_size: 60
});
if (currentCategoryId) {
params.append('category_id', currentCategoryId);
}
// Добавляем поисковый запрос, если есть
if (currentSearchQuery) {
params.append('query', currentSearchQuery);
}
const response = await fetch(`/pos/api/items/?${params}`);
const data = await response.json();
if (data.success) {
if (append) {
ITEMS = ITEMS.concat(data.items);
} else {
ITEMS = data.items;
}
hasMoreItems = data.has_more;
if (data.has_more) {
currentPage = data.next_page;
}
renderProducts();
}
} catch (error) {
console.error('Ошибка загрузки товаров:', error);
} finally {
isLoadingItems = false;
}
}
// Infinite scroll
function setupInfiniteScroll() {
const grid = document.getElementById('productGrid');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && hasMoreItems && !isLoadingItems && !isShowcaseView) {
loadItems(true); // Догрузка
}
});
},
{
rootMargin: '200px'
}
);
// Наблюдаем за концом грида
const sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
sentinel.style.height = '1px';
grid.parentElement.appendChild(sentinel);
observer.observe(sentinel);
}
async function addToCart(item) {
// ПРОВЕРКА НА НАЛИЧИЕ ЕДИНИЦ ПРОДАЖИ
// Если у товара одна единица продажи - добавляем сразу
// Если несколько - показываем модальное окно выбора
console.log('addToCart:', item.name, 'has_sales_units:', item.has_sales_units, 'sales_units_count:', item.sales_units_count);
if (item.type === 'product' && item.has_sales_units) {
// Если одна единица продажи - добавляем сразу
if (item.sales_units_count === 1 && item.default_sales_unit) {
await addProductWithUnitToCart(item, item.default_sales_unit, 1);
return;
}
// Иначе открываем модальное окно выбора единицы
await openProductUnitModal(item);
return;
}
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
// СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
if (item.type === 'showcase_kit') {
// Пытаемся заблокировать 1 экземпляр через API
// API сам проверит доступность и вернёт ошибку если нет свободных
try {
const response = await fetch(`/pos/api/showcase-kits/${item.id}/add-to-cart/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ quantity: 1 })
});
const data = await response.json();
if (!response.ok || !data.success) {
// Нет доступных экземпляров или другая ошибка
alert(data.error || 'Не удалось добавить букет в корзину');
// Обновляем витрину чтобы показать актуальное состояние
if (isShowcaseView) {
await loadShowcaseKits();
}
return;
}
// Успешно заблокировали - добавляем/обновляем в корзине
const lockedItemIds = data.locked_item_ids || [];
if (cart.has(cartKey)) {
// Добавляем к существующим
const existing = cart.get(cartKey);
existing.qty += lockedItemIds.length;
existing.showcase_item_ids = [...(existing.showcase_item_ids || []), ...lockedItemIds];
} else {
// Создаём новую запись
cart.set(cartKey, {
id: item.id,
name: item.name,
price: Number(item.price),
qty: lockedItemIds.length,
type: item.type,
showcase_item_ids: lockedItemIds,
lock_expires_at: data.lock_expires_at
});
}
// Обновляем список витрины (чтобы показать актуальные available_count)
if (isShowcaseView) {
await loadShowcaseKits();
}
} catch (error) {
console.error('Ошибка при добавлении витринного комплекта:', error);
alert('Ошибка сервера. Попробуйте еще раз.');
return;
}
} else {
// ОБЫЧНАЯ ЛОГИКА для товаров и комплектов
if (!cart.has(cartKey)) {
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
} else {
const cartItem = cart.get(cartKey);
cartItem.qty = roundQuantity(cartItem.qty + 1, 3);
}
}
renderCart();
saveCartToRedis(); // Сохраняем в Redis
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView && item.type === 'product') {
renderProducts();
}
// Автоматический фокус на поле количества (только для обычных товаров)
if (item.type !== 'showcase_kit') {
setTimeout(() => {
const qtyInputs = document.querySelectorAll('.qty-input');
const itemIndex = Array.from(cart.keys()).indexOf(cartKey);
if (itemIndex !== -1 && qtyInputs[itemIndex]) {
// Устанавливаем фокус только на десктопных устройствах
if (!isMobileDevice()) {
qtyInputs[itemIndex].focus();
qtyInputs[itemIndex].select(); // Выделяем весь текст
}
}
}, 50);
}
}
// Вспомогательная функция для обновления количества товара в корзине
async function updateCartItemQty(cartKey, newQty) {
const item = cart.get(cartKey);
if (!item) return;
// Округляем новое количество
const roundedQty = roundQuantity(newQty, 3);
if (roundedQty <= 0) {
await removeFromCart(cartKey);
} else {
item.qty = roundedQty;
renderCart();
saveCartToRedis();
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView && item.type === 'product') {
renderProducts();
}
}
}
function renderCart() {
const list = document.getElementById('cartList');
list.innerHTML = '';
let total = 0;
if (cart.size === 0) {
list.innerHTML = '
Корзина пуста
';
document.getElementById('cartTotal').textContent = '0.00';
updateShowcaseButtonState(); // Обновляем состояние кнопки
updateMobileCartBar(); // Обновляем мобильный бар даже когда корзина пуста
return;
}
cart.forEach((item, cartKey) => {
const row = document.createElement('div');
row.className = 'cart-item mb-2';
row.style.cursor = 'pointer';
row.title = 'Нажмите для редактирования';
// Индикатор изменённой цены
if (item.price_overridden) {
row.classList.add('price-overridden');
}
// СПЕЦИАЛЬНАЯ СТИЛИЗАЦИЯ для витринных комплектов
const isShowcaseKit = item.type === 'showcase_kit';
if (isShowcaseKit) {
row.style.backgroundColor = '#fff3cd'; // Желтый фон
row.style.border = '1px solid #ffc107';
row.style.borderRadius = '4px';
row.style.padding = '8px';
}
// Левая часть: Название и цена единицы
const namePrice = document.createElement('div');
namePrice.className = 'item-name-price';
// Иконка только для комплектов
let typeIcon = '';
if (item.type === 'kit' || item.type === 'showcase_kit') {
typeIcon = '
';
}
// Единица продажи (если есть)
let unitInfo = '';
if (item.sales_unit_id && item.unit_name) {
unitInfo = `
${item.unit_name}`;
}
namePrice.innerHTML = `
${typeIcon}${item.name}
${formatMoney(item.price)}${unitInfo}
`;
// Знак умножения
const multiplySign = document.createElement('span');
multiplySign.className = 'multiply-sign';
multiplySign.textContent = 'x';
// Контейнер для кнопок количества
const qtyControl = document.createElement('div');
qtyControl.className = 'd-flex align-items-center';
qtyControl.style.gap = '2px';
// СПЕЦИАЛЬНАЯ ЛОГИКА для витринных комплектов
if (isShowcaseKit) {
// Кнопка минус
const minusBtn = document.createElement('button');
minusBtn.className = 'btn btn-outline-secondary btn-sm';
minusBtn.innerHTML = '
';
minusBtn.onclick = async (e) => {
e.preventDefault();
await decreaseShowcaseKitQty(cartKey);
};
// Поле количества (только для отображения, readonly)
const qtyInput = document.createElement('input');
qtyInput.type = 'number';
qtyInput.className = 'qty-input form-control form-control-sm';
qtyInput.style.width = '60px';
qtyInput.style.textAlign = 'center';
qtyInput.style.padding = '0.375rem 0.25rem';
qtyInput.value = roundQuantity(item.qty, 3);
qtyInput.min = 1;
qtyInput.readOnly = true; // Только чтение - изменяем только через +/-
qtyInput.style.backgroundColor = '#fff3cd'; // Желтый фон как у витринных
// Кнопка плюс
const plusBtn = document.createElement('button');
plusBtn.className = 'btn btn-outline-secondary btn-sm';
plusBtn.innerHTML = '
';
plusBtn.onclick = async (e) => {
e.preventDefault();
await increaseShowcaseKitQty(cartKey);
};
// Собираем контейнер
qtyControl.appendChild(minusBtn);
qtyControl.appendChild(qtyInput);
qtyControl.appendChild(plusBtn);
} else {
// ОБЫЧНАЯ ЛОГИКА для товаров и комплектов
// Кнопка минус
const minusBtn = document.createElement('button');
minusBtn.className = 'btn btn-outline-secondary btn-sm';
minusBtn.innerHTML = '
';
minusBtn.onclick = async (e) => {
e.preventDefault();
const currentQty = cart.get(cartKey).qty;
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
await updateCartItemQty(cartKey, roundQuantity(currentQty - step, 3));
};
// Поле ввода количества
const qtyInput = document.createElement('input');
qtyInput.type = 'number';
qtyInput.className = 'qty-input form-control form-control-sm';
qtyInput.style.width = '60px';
qtyInput.style.textAlign = 'center';
qtyInput.style.padding = '0.375rem 0.25rem';
qtyInput.value = roundQuantity(item.qty, 3);
qtyInput.min = 1;
qtyInput.step = item.quantity_step || 0.001; // Устанавливаем шаг единицы продажи
qtyInput.onchange = async (e) => {
const newQty = parseFloat(e.target.value) || 1;
await updateCartItemQty(cartKey, newQty);
};
// Округление при потере фокуса
qtyInput.onblur = (e) => {
const rawValue = parseFloat(e.target.value) || 1;
e.target.value = roundQuantity(rawValue, 3);
};
// Кнопка плюс
const plusBtn = document.createElement('button');
plusBtn.className = 'btn btn-outline-secondary btn-sm';
plusBtn.innerHTML = '
';
plusBtn.onclick = async (e) => {
e.preventDefault();
const currentQty = cart.get(cartKey).qty;
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
await updateCartItemQty(cartKey, roundQuantity(currentQty + step, 3));
};
// Собираем контейнер
qtyControl.appendChild(minusBtn);
qtyControl.appendChild(qtyInput);
qtyControl.appendChild(plusBtn);
}
// Сумма за позицию
const itemTotal = document.createElement('div');
itemTotal.className = 'item-total';
itemTotal.textContent = formatMoney(item.price * item.qty);
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-link text-danger p-0';
deleteBtn.innerHTML = '
';
deleteBtn.onclick = () => removeFromCart(cartKey);
row.appendChild(namePrice);
row.appendChild(multiplySign);
row.appendChild(qtyControl);
row.appendChild(itemTotal);
row.appendChild(deleteBtn);
// Обработчик клика для редактирования товара
row.addEventListener('click', function(e) {
// Игнорируем клики на кнопки управления количеством и удаления
if (e.target.closest('button') || e.target.closest('input')) {
return;
}
console.log('Cart row clicked, cartKey:', cartKey, 'CartItemEditor:', typeof window.CartItemEditor);
if (window.CartItemEditor) {
window.CartItemEditor.openModal(cartKey);
} else {
console.error('CartItemEditor not available!');
}
});
list.appendChild(row);
total += item.qty * item.price;
});
document.getElementById('cartTotal').textContent = formatMoney(total);
// Обновляем состояние кнопки "НА ВИТРИНУ"
updateShowcaseButtonState();
// Обновляем мобильный бар корзины
updateMobileCartBar();
}
/**
* Склонение слов в зависимости от числа
* @param {number} number - число
* @param {string} one - форма для 1 (товар)
* @param {string} two - форма для 2-4 (товара)
* @param {string} five - форма для 5+ (товаров)
*/
function getNoun(number, one, two, five) {
const n = Math.abs(number);
const n10 = n % 10;
const n100 = n % 100;
if (n100 >= 11 && n100 <= 19) {
return five;
}
if (n10 === 1) {
return one;
}
if (n10 >= 2 && n10 <= 4) {
return two;
}
return five;
}
/**
* Обновляет мобильный бар корзины
*/
function updateMobileCartBar() {
const countEl = document.querySelector('.mobile-cart-count');
const totalEl = document.querySelector('.mobile-cart-total');
if (!countEl || !totalEl) return;
let count = 0;
let total = 0;
cart.forEach((item) => {
count += item.qty;
total += item.qty * item.price;
});
// Округляем количество до целого для отображения
const displayCount = Math.round(count);
countEl.textContent = `${displayCount} ${getNoun(displayCount, 'товар', 'товара', 'товаров')}`;
totalEl.textContent = formatMoney(total);
}
async function removeFromCart(cartKey) {
const item = cart.get(cartKey);
// СПЕЦИАЛЬНАЯ ЛОГИКА для витринных комплектов - снимаем блокировку
if (item && item.type === 'showcase_kit') {
try {
// Передаём конкретные showcase_item_ids для снятия блокировки
const body = {};
if (item.showcase_item_ids && item.showcase_item_ids.length > 0) {
body.showcase_item_ids = item.showcase_item_ids;
}
const response = await fetch(`/pos/api/showcase-kits/${item.id}/remove-from-cart/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const data = await response.json();
if (!response.ok) {
console.error('Ошибка при снятии блокировки:', data.error);
// Продолжаем удаление из корзины даже при ошибке
}
// Обновляем список витрины (чтобы убрать индикацию блокировки)
if (isShowcaseView) {
await loadShowcaseKits();
}
} catch (error) {
console.error('Ошибка при снятии блокировки витринного комплекта:', error);
// Продолжаем удаление из корзины
}
}
cart.delete(cartKey);
renderCart();
saveCartToRedis(); // Сохраняем в Redis
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView && item && item.type === 'product') {
renderProducts();
}
}
/**
* Увеличивает количество витринного комплекта в корзине
* Добавляет еще один экземпляр через API (если есть доступные)
*/
async function increaseShowcaseKitQty(cartKey) {
const item = cart.get(cartKey);
if (!item || item.type !== 'showcase_kit') return;
try {
// Пытаемся заблокировать еще 1 экземпляр
const response = await fetch(`/pos/api/showcase-kits/${item.id}/add-to-cart/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ quantity: 1 })
});
const data = await response.json();
if (!response.ok || !data.success) {
// Нет доступных экземпляров
alert(data.error || 'Нет доступных экземпляров этого букета на витрине');
return;
}
// Успешно заблокировали - обновляем корзину
const lockedItemIds = data.locked_item_ids || [];
item.qty += lockedItemIds.length;
item.showcase_item_ids = [...(item.showcase_item_ids || []), ...lockedItemIds];
renderCart();
saveCartToRedis();
// Обновляем список витрины
if (isShowcaseView) {
await loadShowcaseKits();
}
} catch (error) {
console.error('Ошибка при увеличении количества витринного комплекта:', error);
alert('Ошибка сервера. Попробуйте еще раз.');
}
}
/**
* Уменьшает количество витринного комплекта в корзине
* Снимает блокировку с одного экземпляра (последнего в списке)
*/
async function decreaseShowcaseKitQty(cartKey) {
const item = cart.get(cartKey);
if (!item || item.type !== 'showcase_kit') return;
// Если количество = 1, удаляем полностью
if (item.qty <= 1) {
await removeFromCart(cartKey);
return;
}
try {
// Снимаем блокировку с последнего экземпляра
const showcaseItemIds = item.showcase_item_ids || [];
if (showcaseItemIds.length === 0) {
// Нет ID - просто удаляем
await removeFromCart(cartKey);
return;
}
// Берем последний ID из списка
const itemIdToRelease = showcaseItemIds[showcaseItemIds.length - 1];
const response = await fetch(`/pos/api/showcase-kits/${item.id}/remove-from-cart/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ showcase_item_ids: [itemIdToRelease] })
});
const data = await response.json();
if (!response.ok) {
console.error('Ошибка при снятии блокировки:', data.error);
}
// Обновляем корзину
item.qty -= 1;
item.showcase_item_ids = showcaseItemIds.filter(id => id !== itemIdToRelease);
renderCart();
saveCartToRedis();
// Обновляем список витрины
if (isShowcaseView) {
await loadShowcaseKits();
}
} catch (error) {
console.error('Ошибка при уменьшении количества витринного комплекта:', error);
alert('Ошибка сервера. Попробуйте еще раз.');
}
}
async function clearCart() {
// Сбрасываем все свои блокировки витринных букетов
try {
await fetch('/pos/api/showcase-kits/release-all-my-locks/', {
method: 'POST',
headers: { 'X-CSRFToken': getCsrfToken() }
});
} catch (e) {
console.error('Ошибка сброса блокировок:', e);
}
// Очищаем корзину
cart.clear();
renderCart();
saveCartToRedis(); // Сохраняем пустую корзину в Redis
// Обновляем отображение товаров/витрины чтобы показать актуальные остатки
if (isShowcaseView) {
await loadShowcaseKits();
} else {
renderProducts(); // Перерисовать карточки товаров с актуальными остатками
}
}
document.getElementById('clearCart').onclick = clearCart;
/**
* Обновляет состояние кнопки "НА ВИТРИНУ"
* Блокирует кнопку если в корзине есть витринный комплект
*/
function updateShowcaseButtonState() {
const showcaseBtn = document.getElementById('addToShowcaseBtn');
if (!showcaseBtn) return;
// Проверяем наличие витринных комплектов в корзине
let hasShowcaseKit = false;
for (const [cartKey, item] of cart) {
if (item.type === 'showcase_kit') {
hasShowcaseKit = true;
break;
}
}
if (hasShowcaseKit) {
// Блокируем кнопку
showcaseBtn.disabled = true;
showcaseBtn.classList.add('disabled');
showcaseBtn.style.opacity = '0.5';
showcaseBtn.style.cursor = 'not-allowed';
showcaseBtn.title = '⚠️ В корзине уже есть витринный комплект. Удалите его перед созданием нового';
} else {
// Разблокируем кнопку
showcaseBtn.disabled = false;
showcaseBtn.classList.remove('disabled');
showcaseBtn.style.opacity = '1';
showcaseBtn.style.cursor = 'pointer';
showcaseBtn.title = 'Создать букет на витрину из текущей корзины';
}
}
// Кнопка "На витрину" - функционал будет добавлен позже
document.getElementById('addToShowcaseBtn').onclick = () => {
openCreateTempKitModal();
};
// Функция открытия модального окна для создания временного комплекта
async function openCreateTempKitModal() {
// Проверяем что корзина не пуста
if (cart.size === 0) {
alert('Корзина пуста. Добавьте товары перед созданием комплекта.');
return;
}
// Проверяем что в корзине НЕТ витринных комплектов
let hasShowcaseKit = false;
for (const [cartKey, item] of cart) {
if (item.type === 'showcase_kit') {
hasShowcaseKit = true;
break;
}
}
if (hasShowcaseKit) {
alert('⚠️ В корзине уже есть витринный комплект!\n\nНельзя создать новый букет на витрину, пока в корзине находится другой витринный букет.\n\nУдалите витринный букет из корзины или завершите текущую продажу.');
return;
}
// Проверяем что в корзине только товары (не обычные комплекты)
let hasKits = false;
for (const [cartKey, item] of cart) {
if (item.type === 'kit') {
hasKits = true;
break;
}
}
if (hasKits) {
alert('В корзине есть комплекты. Для витрины добавляйте только отдельные товары.');
return;
}
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
tempCart.clear();
cart.forEach((item, key) => {
tempCart.set(key, {...item}); // Глубокая копия объекта
});
// Генерируем название по умолчанию
const now = new Date();
const defaultName = `Витрина — ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})}`;
document.getElementById('tempKitName').value = defaultName;
// Загружаем список витрин
await loadShowcases();
// Заполняем список товаров из tempCart
renderTempKitItems();
// Открываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
modal.show();
}
// Открытие модального окна для редактирования комплекта
async function openEditKitModal(kitId) {
try {
// Загружаем данные комплекта
const response = await fetch(`/pos/api/product-kits/${kitId}/`);
const data = await response.json();
if (!data.success) {
alert(`Ошибка: ${data.error}`);
return;
}
const kit = data.kit;
// Устанавливаем режим редактирования
isEditMode = true;
editingKitId = kitId;
// Загружаем список витрин
await loadShowcases();
// Очищаем tempCart и заполняем составом комплекта
tempCart.clear();
kit.items.forEach(item => {
const cartKey = `product-${item.product_id}`;
tempCart.set(cartKey, {
id: item.product_id,
name: item.name,
price: Number(item.price),
actual_catalog_price: item.actual_catalog_price ? Number(item.actual_catalog_price) : Number(item.price),
qty: Number(item.qty),
type: 'product'
});
});
renderTempKitItems(); // Отображаем товары в модальном окне
// Заполняем поля формы
document.getElementById('tempKitName').value = kit.name;
document.getElementById('tempKitDescription').value = kit.description;
document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type;
document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value;
if (kit.sale_price) {
document.getElementById('useSalePrice').checked = true;
document.getElementById('salePrice').value = kit.sale_price;
document.getElementById('salePriceBlock').style.display = 'block';
} else {
document.getElementById('useSalePrice').checked = false;
document.getElementById('salePrice').value = '';
document.getElementById('salePriceBlock').style.display = 'none';
}
// Выбираем витрину
if (kit.showcase_id) {
document.getElementById('showcaseSelect').value = kit.showcase_id;
}
// Отображаем фото, если есть
if (kit.photo_url) {
document.getElementById('photoPreviewImg').src = kit.photo_url;
document.getElementById('photoPreview').style.display = 'block';
} else {
document.getElementById('photoPreview').style.display = 'none';
}
// Обновляем цены
updatePriceCalculations();
// Меняем заголовок и кнопку
document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета';
document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения';
// По��азываем кнопку "Разобрать" и блок добавления товаров
document.getElementById('disassembleKitBtn').style.display = 'block';
document.getElementById('showcaseKitQuantityBlock').style.display = 'none';
document.getElementById('addProductBlock').style.display = 'block';
// Инициализируем компонент поиска товаров
setTimeout(() => {
if (window.ProductSearchPicker) {
const picker = ProductSearchPicker.init('#temp-kit-product-picker', {
onAddSelected: function(product, instance) {
if (product) {
// Добавляем товар в tempCart
const cartKey = `product-${product.id}`;
if (tempCart.has(cartKey)) {
// Увеличиваем количество
const existing = tempCart.get(cartKey);
existing.qty += 1;
} else {
// Добавляем новый товар
tempCart.set(cartKey, {
id: product.id,
name: product.text || product.name,
price: Number(product.price || 0),
qty: 1,
type: 'product'
});
}
// Обновляем отображение
renderTempKitItems();
// Очищаем выбор в пикере
instance.clearSelection();
}
}
});
}
}, 100);
// Открываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
modal.show();
// Проверяем актуальность цен (сразу после открытия)
checkPricesActual();
} catch (error) {
console.error('Error loading kit for edit:', error);
alert('Ошибка при загрузке комплекта');
}
}
// Проверка актуальности цен в витринном комплекте
function checkPricesActual() {
// Удаляем старый warning если есть
const existingWarning = document.getElementById('priceOutdatedWarning');
if (existingWarning) existingWarning.remove();
// Проверяем цены используя actual_catalog_price из tempCart (уже загружен с бэкенда)
const outdatedItems = [];
let oldTotalPrice = 0;
let newTotalPrice = 0;
tempCart.forEach((item, cartKey) => {
if (item.type === 'product' && item.actual_catalog_price !== undefined) {
const savedPrice = parseFloat(item.price);
const actualPrice = parseFloat(item.actual_catalog_price);
const qty = parseFloat(item.qty) || 1;
if (Math.abs(savedPrice - actualPrice) > 0.01) {
oldTotalPrice += savedPrice * qty;
newTotalPrice += actualPrice * qty;
outdatedItems.push({
name: item.name,
old: savedPrice,
new: actualPrice,
qty: qty
});
}
}
});
if (outdatedItems.length > 0) {
showPriceOutdatedWarning(oldTotalPrice, newTotalPrice);
}
}
// Показать warning о неактуальных ценах
function showPriceOutdatedWarning(oldTotalPrice, newTotalPrice) {
const modalBody = document.querySelector('#createTempKitModal .modal-body');
const warning = document.createElement('div');
warning.id = 'priceOutdatedWarning';
warning.className = 'alert alert-warning alert-dismissible fade show d-flex align-items-start';
warning.innerHTML = `
Цена неактуальна!
При сохранении комплекта было: ${formatMoney(oldTotalPrice)} руб.
Актуальная цена сейчас: ${formatMoney(newTotalPrice)} руб.
`;
modalBody.insertBefore(warning, modalBody.firstChild);
}
// Актуализировать цены в комплекте
function actualizeKitPrices() {
tempCart.forEach((item) => {
if (item.type === 'product' && item.actual_catalog_price !== undefined) {
item.price = item.actual_catalog_price;
// Удаляем actual_catalog_price чтобы не показывался warning снова
delete item.actual_catalog_price;
}
});
// Перерисовываем товары и пересчитываем цену
renderTempKitItems();
updatePriceCalculations();
// Убираем warning
const warning = document.getElementById('priceOutdatedWarning');
if (warning) warning.remove();
}
// Обновление списка витринных комплектов
async function loadShowcaseKits() {
try {
const response = await fetch('/pos/api/showcase-kits/');
const data = await response.json();
if (data.success) {
showcaseKits = data.items;
// Перерисовываем грид если мы в режиме витрины
if (isShowcaseView) {
renderProducts();
}
} else {
console.error('Failed to refresh showcase kits:', data);
}
} catch (error) {
console.error('Error refreshing showcase kits:', error);
}
}
// Алиас для совместимости
const refreshShowcaseKits = loadShowcaseKits;
// Загрузка списка витрин
async function loadShowcases() {
try {
const response = await fetch('/pos/api/get-showcases/');
const data = await response.json();
const select = document.getElementById('showcaseSelect');
select.innerHTML = '
';
if (data.success && data.showcases.length > 0) {
let defaultShowcaseId = null;
data.showcases.forEach(showcase => {
const option = document.createElement('option');
option.value = showcase.id;
option.textContent = `${showcase.name} (${showcase.warehouse_name})`;
select.appendChild(option);
// Запоминаем витрину по умолчанию
if (showcase.is_default) {
defaultShowcaseId = showcase.id;
}
});
// Автовыбор витрины по умолчанию
if (defaultShowcaseId) {
select.value = defaultShowcaseId;
}
} else {
select.innerHTML = '
';
}
} catch (error) {
console.error('Error loading showcases:', error);
alert('Ошибка загрузки витрин');
}
}
// Отображение товаров из tempCart в модальном окне
function renderTempKitItems() {
const container = document.getElementById('tempKitItemsList');
container.innerHTML = '';
let estimatedTotal = 0;
tempCart.forEach((item, cartKey) => {
// Только товары (не комплекты)
if (item.type !== 'product') return;
const itemDiv = document.createElement('div');
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
// Левая часть: название и цена
const leftDiv = document.createElement('div');
leftDiv.className = 'flex-grow-1';
leftDiv.innerHTML = `
${item.name}
${formatMoney(item.price)} руб. / шт.
`;
// Правая часть: контролы количества и удаление
const rightDiv = document.createElement('div');
rightDiv.className = 'd-flex align-items-center gap-2';
// Кнопка минус
const minusBtn = document.createElement('button');
minusBtn.className = 'btn btn-sm btn-outline-secondary';
minusBtn.innerHTML = '
';
minusBtn.onclick = (e) => {
e.preventDefault();
if (item.qty > 1) {
item.qty--;
} else {
tempCart.delete(cartKey);
}
renderTempKitItems();
};
// Поле количества
const qtyInput = document.createElement('input');
qtyInput.type = 'number';
qtyInput.className = 'form-control form-control-sm text-center';
qtyInput.style.width = '60px';
qtyInput.value = item.qty;
qtyInput.min = 1;
qtyInput.onchange = (e) => {
const newQty = parseInt(e.target.value) || 1;
item.qty = Math.max(1, newQty);
renderTempKitItems();
};
// Кнопка плюс
const plusBtn = document.createElement('button');
plusBtn.className = 'btn btn-sm btn-outline-secondary';
plusBtn.innerHTML = '
';
plusBtn.onclick = (e) => {
e.preventDefault();
item.qty++;
renderTempKitItems();
};
// Сумма за товар
const totalDiv = document.createElement('div');
totalDiv.className = 'text-end ms-2';
totalDiv.style.minWidth = '80px';
totalDiv.innerHTML = `
${formatMoney(item.qty * item.price)} руб.`;
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-outline-danger';
deleteBtn.innerHTML = '
';
deleteBtn.onclick = (e) => {
e.preventDefault();
tempCart.delete(cartKey);
renderTempKitItems();
};
rightDiv.appendChild(minusBtn);
rightDiv.appendChild(qtyInput);
rightDiv.appendChild(plusBtn);
rightDiv.appendChild(totalDiv);
rightDiv.appendChild(deleteBtn);
itemDiv.appendChild(leftDiv);
itemDiv.appendChild(rightDiv);
container.appendChild(itemDiv);
estimatedTotal += item.qty * item.price;
});
// Если корзина пуста
if (tempCart.size === 0) {
container.innerHTML = '
Нет товаров
';
}
// Обновляем все расчеты цен
updatePriceCalculations(estimatedTotal);
}
// Расчет и обновление всех цен
function updatePriceCalculations(basePrice = null) {
// Если basePrice не передан, пересчитываем из tempCart
if (basePrice === null) {
basePrice = 0;
tempCart.forEach((item, cartKey) => {
if (item.type === 'product') {
basePrice += item.qty * item.price;
}
});
}
// Базовая цена
document.getElementById('tempKitBasePrice').textContent = formatMoney(basePrice) + ' руб.';
// Корректировка
const adjustmentType = document.getElementById('priceAdjustmentType').value;
const adjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
let calculatedPrice = basePrice;
if (adjustmentType !== 'none' && adjustmentValue > 0) {
if (adjustmentType === 'increase_percent') {
calculatedPrice = basePrice + (basePrice * adjustmentValue / 100);
} else if (adjustmentType === 'increase_amount') {
calculatedPrice = basePrice + adjustmentValue;
} else if (adjustmentType === 'decrease_percent') {
calculatedPrice = Math.max(0, basePrice - (basePrice * adjustmentValue / 100));
} else if (adjustmentType === 'decrease_amount') {
calculatedPrice = Math.max(0, basePrice - adjustmentValue);
}
}
document.getElementById('tempKitCalculatedPrice').textContent = formatMoney(calculatedPrice) + ' руб.';
// Финальная цена (с учетом sale_price если задана)
const useSalePrice = document.getElementById('useSalePrice').checked;
const salePrice = parseFloat(document.getElementById('salePrice').value) || 0;
let finalPrice = calculatedPrice;
if (useSalePrice && salePrice > 0) {
finalPrice = salePrice;
}
document.getElementById('tempKitFinalPrice').textContent = formatMoney(finalPrice);
}
// Обработчики для полей цены
document.getElementById('priceAdjustmentType').addEventListener('change', function() {
const adjustmentBlock = document.getElementById('adjustmentValueBlock');
if (this.value === 'none') {
adjustmentBlock.style.display = 'none';
document.getElementById('priceAdjustmentValue').value = '0';
} else {
adjustmentBlock.style.display = 'block';
}
updatePriceCalculations();
});
document.getElementById('priceAdjustmentValue').addEventListener('input', function() {
updatePriceCalculations();
});
document.getElementById('useSalePrice').addEventListener('change', function() {
const salePriceBlock = document.getElementById('salePriceBlock');
if (this.checked) {
salePriceBlock.style.display = 'block';
} else {
salePriceBlock.style.display = 'none';
document.getElementById('salePrice').value = '';
}
updatePriceCalculations();
});
document.getElementById('salePrice').addEventListener('input', function() {
updatePriceCalculations();
});
// Обработчик загрузки фото
document.getElementById('tempKitPhoto').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
if (!file.type.startsWith('image/')) {
alert('Пожалуйста, выберите файл изображения');
this.value = '';
return;
}
// Превью
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById('photoPreviewImg').src = event.target.result;
document.getElementById('photoPreview').style.display = 'block';
};
reader.readAsDataURL(file);
}
});
// Удаление фото
document.getElementById('removePhoto').addEventListener('click', function() {
document.getElementById('tempKitPhoto').value = '';
document.getElementById('photoPreview').style.display = 'none';
document.getElementById('photoPreviewImg').src = '';
});
// Подтверждение создания/редактирования временного комплекта
document.getElementById('confirmCreateTempKit').onclick = async () => {
const kitName = document.getElementById('tempKitName').value.trim();
const showcaseId = document.getElementById('showcaseSelect').value;
const description = document.getElementById('tempKitDescription').value.trim();
const photoFile = document.getElementById('tempKitPhoto').files[0];
// Валидация
if (!kitName) {
alert('Введите название комплекта');
return;
}
if (!showcaseId && !isEditMode) {
alert('Выберите витрину');
return;
}
// Собираем товары из tempCart (изолированное состояние модалки)
const items = [];
tempCart.forEach((item, cartKey) => {
if (item.type === 'product') {
items.push({
product_id: item.id,
quantity: item.qty
});
}
});
if (items.length === 0) {
alert('Нет товаров для создания комплекта');
return;
}
// Получаем данные о ценах
const priceAdjustmentType = document.getElementById('priceAdjustmentType').value;
const priceAdjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
const useSalePrice = document.getElementById('useSalePrice').checked;
const salePrice = useSalePrice ? (parseFloat(document.getElementById('salePrice').value) || 0) : 0;
// Получаем количество букетов для создания
const showcaseKitQuantity = parseInt(document.getElementById('showcaseKitQuantity').value, 10) || 1;
// Вычисляем итоговую цену комплекта на основе изменённых цен в корзине
let calculatedPrice = 0;
tempCart.forEach((item) => {
if (item.type === 'product') {
calculatedPrice += item.qty * item.price;
}
});
// Формируем FormData для отправки с файлом
const formData = new FormData();
formData.append('kit_name', kitName);
if (showcaseId) {
formData.append('showcase_id', showcaseId);
formData.append('quantity', showcaseKitQuantity); // Количество экземпляров на витрину
}
formData.append('description', description);
formData.append('items', JSON.stringify(items));
formData.append('price_adjustment_type', priceAdjustmentType);
formData.append('price_adjustment_value', priceAdjustmentValue);
// Если пользователь не задал свою цену, используем вычисленную
const finalSalePrice = useSalePrice ? salePrice : calculatedPrice;
if (finalSalePrice > 0) {
formData.append('sale_price', finalSalePrice);
}
// Фото: для редактирования проверяем, удалено ли оно
if (photoFile) {
formData.append('photo', photoFile);
} else if (isEditMode && document.getElementById('photoPreview').style.display === 'none') {
// Если фото было удалено
formData.append('remove_photo', '1');
}
// Отправляем запрос на сервер
const confirmBtn = document.getElementById('confirmCreateTempKit');
confirmBtn.disabled = true;
const url = isEditMode
? `/pos/api/product-kits/${editingKitId}/update/`
: '/pos/api/create-temp-kit/';
const actionText = isEditMode ? 'Сохранение...' : 'Создание...';
confirmBtn.innerHTML = `
${actionText}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
// Не указываем Content-Type - браузер сам установит multipart/form-data
},
body: formData
});
const data = await response.json();
if (data.success) {
// Успех!
const createdCount = data.available_count || 1;
const qtyInfo = createdCount > 1 ? `\nСоздано экземпляров: ${createdCount}` : '';
let successMessage = isEditMode
? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
: `✅ ${data.message}
Комплект: ${data.kit_name}
Цена: ${data.kit_price} руб.${qtyInfo}
Зарезервировано компонентов: ${data.reservations_count}`;
// Если есть предупреждение о нехватке товара - добавляем его
if (data.warnings && data.warnings.length > 0) {
successMessage += '\n\n⚠️ ВНИМАНИЕ: Нехватка товара на складе!\n';
data.warnings.forEach(warning => {
successMessage += `\n• ${warning}`;
});
successMessage += '\n\nПроверьте остатки и пополните склад.';
}
alert(successMessage);
// Очищаем tempCart (изолированное состояние модалки)
tempCart.clear();
// Сбрасываем поля формы
document.getElementById('tempKitDescription').value = '';
document.getElementById('tempKitPhoto').value = '';
document.getElementById('photoPreview').style.display = 'none';
document.getElementById('priceAdjustmentType').value = 'none';
document.getElementById('priceAdjustmentValue').value = '0';
document.getElementById('adjustmentValueBlock').style.display = 'none';
document.getElementById('useSalePrice').checked = false;
document.getElementById('salePrice').value = '';
document.getElementById('salePriceBlock').style.display = 'none';
document.getElementById('showcaseKitQuantity').value = '1'; // Сброс количества
// Запоминаем, был ли режим редактирования до сброса
const wasEditMode = isEditMode;
// Сбрасываем режим редактирования
isEditMode = false;
editingKitId = null;
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
modal.hide();
// Если это было СОЗДАНИЕ витринного комплекта из корзины,
// очищаем основную корзину POS
if (!wasEditMode) {
await clearCart();
}
// Обновляем витринные комплекты и переключаемся на вид витрины
isShowcaseView = true;
currentCategoryId = null;
await refreshShowcaseKits();
renderCategories();
renderProducts();
} else {
alert(`Ошибка: ${data.error}`);
}
} catch (error) {
console.error('Error saving kit:', error);
alert('Ошибка при сохранении комплекта');
} finally {
confirmBtn.disabled = false;
const btnText = isEditMode
? '
Сохранить изменения'
: '
Создать и зарезервировать';
confirmBtn.innerHTML = btnText;
}
};
// Обработчик кнопки "Разобрать букет"
document.getElementById('disassembleKitBtn').addEventListener('click', async () => {
if (!isEditMode || !editingKitId) {
alert('Ошибка: режим редактирования не активен');
return;
}
// Запрос подтверждения
const confirmed = confirm(
'Вы уверены?\n\n' +
'Букет будет разобран:\n' +
'• Все резервы компонентов будут освобождены\n' +
'• Комплект будет помечен как "Снят"\n\n' +
'Это действие нельзя отменить!'
);
if (!confirmed) {
return;
}
try {
const response = await fetch(`/pos/api/product-kits/${editingKitId}/disassemble/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
});
const data = await response.json();
if (data.success) {
alert(`✅ ${data.message}\n\nОсвобождено резервов: ${data.released_count}`);
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
modal.hide();
// Обновляем витринные комплекты
isShowcaseView = true;
currentCategoryId = null;
await refreshShowcaseKits();
renderCategories();
renderProducts();
} else {
alert(`❌ Ошибка: ${data.error}`);
}
} catch (error) {
console.error('Error disassembling kit:', error);
alert('Произошла ошибка при разборе букета');
}
});
// Вспомогательная функция для определения мобильного устройства
function isMobileDevice() {
// Проверяем по юзер-агенту и размеру экрана
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
// Проверка по юзер-агенту
const mobileKeywords = ['Android', 'webOS', 'iPhone', 'iPad', 'iPod', 'BlackBerry', 'Windows Phone'];
const isMobileUA = mobileKeywords.some(keyword =>
userAgent.indexOf(keyword) > -1
);
// Проверка по размеру экрана (ширина меньше 768px часто указывает на мобильные устройства)
const isSmallScreen = window.innerWidth < 768;
return isMobileUA || isSmallScreen;
}
// Вспомогательная функция для получения CSRF токена (единая версия)
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Алиас для обратной совместимости
// ВАЖНО: При CSRF_USE_SESSIONS=True токен хранится в сессии, а не в cookie
// Извлекаем его из скрытого поля в HTML ({% csrf_token %})
const getCsrfToken = () => {
// Пытаемся найти токен в DOM (из {% csrf_token %})
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (csrfInput) {
return csrfInput.value;
}
// Fallback: пытаемся прочитать из cookie (если CSRF_USE_SESSIONS=False)
return getCookie('csrftoken');
};
// Сброс режима редактирования при закрытии модального окна
document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function() {
// Очищаем tempCart (изолированное состояние модалки)
tempCart.clear();
// Сброс режима редактирования при закрытии модального окна
if (isEditMode) {
// Сбрасываем режим редактирования
isEditMode = false;
editingKitId = null;
// Восстанавливаем заголовок и текст кнопки
document.getElementById('createTempKitModalLabel').textContent = 'Создать витринный букет из корзины';
document.getElementById('confirmCreateTempKit').innerHTML = '
Создать и зарезервировать';
// Скрываем кнопку "Разобрать" и блок добавления товаров
document.getElementById('disassembleKitBtn').style.display = 'none';
document.getElementById('showcaseKitQuantityBlock').style.display = 'block';
document.getElementById('addProductBlock').style.display = 'none';
}
});
// Открытие модалки "Продажа" и рендер сводки корзины
document.getElementById('checkoutNow').onclick = () => {
if (cart.size === 0) {
alert('Корзина пуста. Добавьте товары перед продажей.');
return;
}
renderCheckoutModal();
const modal = new bootstrap.Modal(document.getElementById('checkoutModal'));
modal.show();
};
// Рендер позиций корзины и итога в модалке продажи
function renderCheckoutModal() {
const container = document.getElementById('checkoutItems');
container.innerHTML = '';
let total = 0;
cart.forEach((item) => {
const row = document.createElement('div');
row.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
// Иконка для комплектов
let typeIcon = '';
if (item.type === 'kit' || item.type === 'showcase_kit') {
typeIcon = '
';
} else {
typeIcon = '
';
}
row.innerHTML = `
${typeIcon}${item.name}
${item.qty} шт × ${formatMoney(item.price)} руб.
${formatMoney(item.qty * item.price)} руб.
`;
container.appendChild(row);
total += item.qty * item.price;
});
// Обновляем информацию о клиенте
updateCustomerDisplay();
}
// ===== CHECKOUT: ПОДТВЕРЖДЕНИЕ ПРОДАЖИ =====
let paymentWidget = null;
// Переменные состояния скидок
let appliedPromoCode = null; // примененный промокод
let appliedManualDiscount = null; // выбранная вручную скидка (из списка)
let appliedCustomDiscount = null; // произвольная скидка {value: number, isPercent: boolean}
let availableDiscounts = []; // список доступных скидок
let skipAutoDiscount = false; // флаг отмены авто-скидки
let cartDiscounts = {
orderDiscounts: [], // скидки на заказ (теперь массив)
itemDiscounts: [], // скидки на позиции
totalDiscount: 0, // общая сумма скидки
excludedBy: null // исключающая скидка
};
// При открытии модалки checkout
document.getElementById('checkoutModal').addEventListener('show.bs.modal', async () => {
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const walletBalance = customer.wallet_balance || 0;
// Сбрасываем скидки
resetDiscounts();
// Показываем баланс кошелька
const walletDiv = document.getElementById('checkoutWalletBalance');
if (customer.id !== SYSTEM_CUSTOMER.id) {
document.getElementById('checkoutWalletBalanceAmount').textContent = walletBalance.toFixed(2);
walletDiv.style.display = 'block';
} else {
walletDiv.style.display = 'none';
}
// Вычисляем итоговую сумму
let totalAmount = 0;
cart.forEach((item) => {
totalAmount += item.qty * item.price;
});
// Проверяем автоматические скидки
await checkAutoDiscounts();
// Загружаем доступные скидки для ручного выбора
await loadAvailableDiscounts();
// Применяем скидки к итоговой сумме
const finalTotal = Math.max(0, totalAmount - cartDiscounts.totalDiscount);
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.';
// Инициализируем виджет в single mode
initPaymentWidget('single', {
order: { total: finalTotal, amount_due: finalTotal, amount_paid: 0 },
customer: { id: customer.id, name: customer.name, wallet_balance: walletBalance }
});
});
// Переключение режима оплаты
document.getElementById('singlePaymentMode').addEventListener('click', function() {
document.getElementById('singlePaymentMode').classList.add('active');
document.getElementById('mixedPaymentMode').classList.remove('active');
reinitPaymentWidget('single');
});
document.getElementById('mixedPaymentMode').addEventListener('click', function() {
document.getElementById('mixedPaymentMode').classList.add('active');
document.getElementById('singlePaymentMode').classList.remove('active');
reinitPaymentWidget('mixed');
});
function reinitPaymentWidget(mode) {
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const totalAmountText = document.getElementById('checkoutFinalPrice').textContent;
const totalAmount = parseFloat(totalAmountText.replace(/[^\d.]/g, ''));
initPaymentWidget(mode, {
order: { total: totalAmount, amount_due: totalAmount, amount_paid: 0 },
customer: { id: customer.id, name: customer.name, wallet_balance: customer.wallet_balance || 0 }
});
}
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ СО СКИДКАМИ =====
// Сброс скидок
function resetDiscounts() {
appliedPromoCode = null;
appliedManualDiscount = null;
appliedCustomDiscount = null;
availableDiscounts = [];
skipAutoDiscount = false;
cartDiscounts = {
orderDiscount: null,
itemDiscounts: [],
totalDiscount: 0
};
// Сбрасываем UI
document.getElementById('promoCodeInput').value = '';
document.getElementById('promoCodeError').style.display = 'none';
document.getElementById('promoCodeError').textContent = '';
document.getElementById('promoCodeSuccess').style.display = 'none';
document.getElementById('promoCodeSuccess').textContent = '';
document.getElementById('removePromoBtn').style.display = 'none';
// Новые элементы 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 = '';
// Сбрасываем произвольную скидку
document.getElementById('customDiscountInput').value = '';
document.getElementById('customDiscountIsPercent').checked = true;
document.getElementById('customDiscountError').style.display = 'none';
document.getElementById('customDiscountError').textContent = '';
document.getElementById('applyCustomDiscountBtn').style.display = 'inline-block';
document.getElementById('removeCustomDiscountBtn').style.display = 'none';
}
// Проверить автоматические скидки
async function checkAutoDiscounts() {
try {
const items = Array.from(cart.values()).map(item => ({
type: item.type,
id: item.id,
quantity: item.qty,
price: item.price
}));
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const response = await fetch('/pos/api/discounts/calculate/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
items: items,
customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null,
manual_discount_id: appliedManualDiscount?.id || null,
skip_auto_discount: skipAutoDiscount
})
});
const result = await response.json();
if (result.success) {
cartDiscounts.totalDiscount = result.total_discount || 0;
cartDiscounts.orderDiscounts = result.order_discounts || [];
cartDiscounts.itemDiscounts = result.item_discounts || [];
cartDiscounts.excludedBy = result.excluded_by || null;
updateDiscountsUI(result);
}
} catch (error) {
console.error('Ошибка при проверке автоматических скидок:', error);
}
}
// Получить иконку для режима объединения
function getCombineModeIcon(mode) {
const icons = {
'stack': '
',
'max_only': '
',
'exclusive': '
'
};
return icons[mode] || '';
}
// Получить описание режима объединения
function getCombineModeTitle(mode) {
const titles = {
'stack': 'Складывается с другими скидками',
'max_only': 'Применяется только максимальная из этого типа',
'exclusive': 'Отменяет все другие скидки'
};
return titles[mode] || mode;
}
// Обновить 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. Скидки на заказ (теперь может быть несколько)
const orderDiscounts = result.order_discounts || [];
if (orderDiscounts.length > 0) {
hasDiscounts = true;
autoContainer.style.display = 'block';
orderDiscounts.forEach(disc => {
const div = document.createElement('div');
div.className = 'd-flex justify-content-between align-items-center w-100';
const modeIcon = getCombineModeIcon(disc.combine_mode);
div.innerHTML = `
${modeIcon} ${disc.discount_name}
-${disc.discount_amount.toFixed(2)} руб.
`;
autoList.appendChild(div);
});
// Показываем информацию о комбинировании
if (orderDiscounts.length > 1) {
const infoDiv = document.createElement('div');
infoDiv.className = 'text-muted small mt-1';
infoDiv.innerHTML = '
Скидки скомбинированы';
autoList.appendChild(infoDiv);
}
// Показываем кнопку отмены (только если еще не пропущена)
document.getElementById('skipAutoDiscountBtn').style.display = 'block';
} else {
autoContainer.style.display = 'none';
document.getElementById('skipAutoDiscountBtn').style.display = 'none';
}
// 2. Скидки на позиции (новый формат с массивом discounts)
const itemDiscounts = result.item_discounts || [];
if (itemDiscounts.length > 0) {
hasDiscounts = true;
itemDiscounts.forEach(item => {
if (item.discounts && item.discounts.length > 0) {
const discNames = item.discounts.map(d => {
const modeIcon = getCombineModeIcon(d.combine_mode);
return `${modeIcon} ${d.discount_name}`;
}).join(', ');
const div = document.createElement('div');
div.className = 'text-muted small';
div.innerHTML = `• ${discNames}:
-${item.total_discount.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';
}
// 4. Произвольная скидка
if (appliedCustomDiscount) {
hasDiscounts = true;
const customDiscountAmount = appliedCustomDiscount.isPercent
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
: appliedCustomDiscount.value;
const discountText = appliedCustomDiscount.isPercent
? `-${appliedCustomDiscount.value}% (-${customDiscountAmount.toFixed(2)} руб.)`
: `-${customDiscountAmount.toFixed(2)} руб.`;
// Показываем в summary или добавляем как отдельную строку
const customDiv = document.createElement('div');
customDiv.className = 'd-flex justify-content-between align-items-center mt-1';
customDiv.innerHTML = `
Произвольная скидка ${discountText}
`;
itemBreakdown.appendChild(customDiv);
}
// Показываем/скрываем summary
if (hasDiscounts) {
summaryDiv.style.display = 'block';
document.getElementById('discountsSubtotal').textContent =
(result.cart_subtotal || 0).toFixed(2) + ' руб.';
// Рассчитываем итоговую скидку с учетом произвольной
let totalDiscount = result.total_discount || 0;
if (appliedCustomDiscount) {
const customDiscountAmount = appliedCustomDiscount.isPercent
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
: appliedCustomDiscount.value;
totalDiscount += customDiscountAmount;
}
document.getElementById('discountsTotalDiscount').textContent =
'-' + totalDiscount.toFixed(2) + ' руб.';
} else {
summaryDiv.style.display = 'none';
}
// Обновляем итоговую цену
let totalDiscount = result.total_discount || 0;
if (appliedCustomDiscount) {
const customDiscountAmount = appliedCustomDiscount.isPercent
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
: appliedCustomDiscount.value;
totalDiscount += customDiscountAmount;
}
const finalTotal = Math.max(0, (result.cart_subtotal || 0) - totalDiscount);
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 = '
Нет доступных скидок';
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 modeIcon = getCombineModeIcon(d.combine_mode);
const modeTitle = getCombineModeTitle(d.combine_mode);
const a = document.createElement('a');
a.href = '#';
a.className = 'dropdown-item d-flex justify-content-between';
a.title = modeTitle;
a.innerHTML = `
${modeIcon} ${d.name}
${valueText} ${minText}
`;
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();
});
// Отменить автоматическую скидку
document.getElementById('skipAutoDiscountBtn').addEventListener('click', async () => {
skipAutoDiscount = true;
await checkAutoDiscounts();
});
// Применить промокод
async function applyPromoCode() {
const code = document.getElementById('promoCodeInput').value.trim().toUpperCase();
if (!code) return;
// Вычисляем сумму корзины
let cartTotal = 0;
cart.forEach((item) => {
cartTotal += item.qty * item.price;
});
try {
const response = await fetch('/pos/api/discounts/validate-promo/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
promo_code: code,
cart_total: cartTotal
})
});
const result = await response.json();
if (result.success) {
appliedPromoCode = result.promo_code;
// Пересчитываем скидки с промокодом (updateDiscountsUI обновит UI)
await recalculateDiscountsWithPromo(code);
// Показываем успех
document.getElementById('promoCodeSuccess').textContent =
`Скидка: ${result.promo_code.discount_name} (${result.promo_code.discount_type === 'percentage' ? result.promo_code.discount_value + '%' : result.promo_code.discount_value + ' руб.'})`;
document.getElementById('promoCodeSuccess').style.display = 'block';
document.getElementById('promoCodeError').style.display = 'none';
document.getElementById('removePromoBtn').style.display = 'inline-block';
} else {
// Показываем ошибку
document.getElementById('promoCodeError').textContent = result.error || 'Неверный промокод';
document.getElementById('promoCodeError').style.display = 'block';
document.getElementById('promoCodeSuccess').style.display = 'none';
}
} catch (error) {
console.error('Ошибка при применении промокода:', error);
document.getElementById('promoCodeError').textContent = 'Ошибка при проверке промокода';
document.getElementById('promoCodeError').style.display = 'block';
}
}
// Пересчитать скидки с промокодом
async function recalculateDiscountsWithPromo(promoCode) {
try {
const items = Array.from(cart.values()).map(item => ({
type: item.type,
id: item.id,
quantity: item.qty,
price: item.price
}));
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const response = await fetch('/pos/api/discounts/calculate/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
items: items,
promo_code: promoCode,
customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null
})
});
const result = await response.json();
if (result.success) {
cartDiscounts.totalDiscount = result.total_discount || 0;
cartDiscounts.orderDiscounts = result.order_discounts || [];
cartDiscounts.itemDiscounts = result.item_discounts || [];
cartDiscounts.excludedBy = result.excluded_by || null;
updateDiscountsUI(result);
}
} catch (error) {
console.error('Ошибка при пересчёте скидок:', error);
}
}
// Обновить итоговую сумму с учётом скидок
function updateCheckoutTotalWithDiscounts() {
let cartTotal = 0;
cart.forEach((item) => {
cartTotal += item.qty * item.price;
});
const finalTotal = Math.max(0, cartTotal - cartDiscounts.totalDiscount);
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.';
// Пересоздаём платёжный виджет с новой суммой
const customer = selectedCustomer || SYSTEM_CUSTOMER;
initPaymentWidget('single', {
order: { total: finalTotal, amount_due: finalTotal, amount_paid: 0 },
customer: { id: customer.id, name: customer.name, wallet_balance: customer.wallet_balance || 0 }
});
}
// Удалить промокод
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';
});
}
// Обработчики кнопок промокода
document.getElementById('applyPromoBtn').addEventListener('click', applyPromoCode);
document.getElementById('removePromoBtn').addEventListener('click', removePromoCode);
document.getElementById('promoCodeInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
applyPromoCode();
}
});
// ===== ПРОИЗВОЛЬНАЯ СКИДКА =====
// Применить произвольную скидку
async function applyCustomDiscount() {
const input = document.getElementById('customDiscountInput');
const isPercent = document.getElementById('customDiscountIsPercent').checked;
const errorDiv = document.getElementById('customDiscountError');
const value = parseFloat(input.value);
// Валидация
if (isNaN(value) || value <= 0) {
errorDiv.textContent = 'Введите корректное значение скидки';
errorDiv.style.display = 'block';
return;
}
if (isPercent && value > 100) {
errorDiv.textContent = 'Процент не может превышать 100%';
errorDiv.style.display = 'block';
return;
}
// Проверяем сумму корзины
let cartTotal = 0;
cart.forEach((item) => {
cartTotal += item.qty * item.price;
});
if (!isPercent && value > cartTotal) {
errorDiv.textContent = `Скидка не может превышать сумму заказа (${cartTotal.toFixed(2)} руб.)`;
errorDiv.style.display = 'block';
return;
}
// Сохраняем произвольную скидку
appliedCustomDiscount = { value, isPercent };
// Сбрасываем другие типы скидок (взаимоисключающие)
appliedPromoCode = null;
appliedManualDiscount = null;
// Обновляем UI
errorDiv.style.display = 'none';
document.getElementById('applyCustomDiscountBtn').style.display = 'none';
document.getElementById('removeCustomDiscountBtn').style.display = 'inline-block';
document.getElementById('promoCodeInput').value = '';
document.getElementById('promoCodeSuccess').style.display = 'none';
document.getElementById('promoCodeError').style.display = 'none';
// Пересчитываем скидки
await checkAutoDiscounts();
}
// Удалить произвольную скидку
async function removeCustomDiscount() {
appliedCustomDiscount = null;
// Обновляем UI
document.getElementById('customDiscountInput').value = '';
document.getElementById('applyCustomDiscountBtn').style.display = 'inline-block';
document.getElementById('removeCustomDiscountBtn').style.display = 'none';
document.getElementById('customDiscountError').style.display = 'none';
// Пересчитываем скидки
await checkAutoDiscounts();
}
// Обработчики кнопок произвольной скидки
document.getElementById('applyCustomDiscountBtn').addEventListener('click', applyCustomDiscount);
document.getElementById('removeCustomDiscountBtn').addEventListener('click', removeCustomDiscount);
document.getElementById('customDiscountInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
applyCustomDiscount();
}
});
document.getElementById('customDiscountInput').addEventListener('input', () => {
document.getElementById('customDiscountError').style.display = 'none';
});
async function initPaymentWidget(mode, data) {
const paymentMethods = [
{ id: 1, code: 'account_balance', name: 'С баланса счёта' },
{ id: 2, code: 'cash', name: 'Наличными' },
{ id: 3, code: 'card', name: 'Картой' },
{ id: 4, code: 'online', name: 'Онлайн' }
];
// Динамически загружаем PaymentWidget если еще не загружен
if (!window.PaymentWidget) {
try {
const module = await import('/static/orders/js/payment_widget.js');
window.PaymentWidget = module.PaymentWidget;
} catch (error) {
console.error('Ошибка загрузки PaymentWidget:', error);
alert('Ошибка загрузки модуля оплаты. Перезагрузите страницу.');
return;
}
}
paymentWidget = new window.PaymentWidget({
containerId: 'paymentWidgetContainer',
mode: mode,
order: data.order,
customer: data.customer,
paymentMethods: paymentMethods,
onSubmit: (paymentsData) => handleCheckoutSubmit(paymentsData)
});
}
// Обработчик кнопки "Подтвердить продажу"
document.getElementById('confirmCheckoutBtn').onclick = () => {
if (paymentWidget) {
paymentWidget.submit();
}
};
// Отправка заказа на сервер
async function handleCheckoutSubmit(paymentsData) {
try {
// Блокируем кнопку
const btn = document.getElementById('confirmCheckoutBtn');
btn.disabled = true;
btn.innerHTML = '
Обработка...';
// Собираем данные
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const orderData = {
customer_id: customer.id,
warehouse_id: currentWarehouse.id,
items: Array.from(cart.values()).map(item => {
const itemData = {
type: item.type,
id: item.id,
quantity: item.qty,
price: item.price
};
// Для витринных букетов передаём ID конкретных экземпляров
if (item.type === 'showcase_kit' && item.showcase_item_ids) {
itemData.showcase_item_ids = item.showcase_item_ids;
}
// Для товаров с единицами продажи
if (item.sales_unit_id) {
itemData.sales_unit_id = item.sales_unit_id;
}
return itemData;
}),
payments: paymentsData,
notes: document.getElementById('orderNote').value.trim(),
promo_code: appliedPromoCode?.code || null,
manual_discount_id: appliedManualDiscount?.id || null,
custom_discount: appliedCustomDiscount ? {
value: appliedCustomDiscount.value,
is_percent: appliedCustomDiscount.isPercent
} : null
};
// Отправляем на сервер
const response = await fetch('/pos/api/checkout/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(orderData)
});
const result = await response.json();
if (result.success) {
console.log('✅ Заказ успешно создан:', result);
// Успех
alert(`Заказ #${result.order_number} успешно создан!\nСумма: ${result.total_amount.toFixed(2)} руб.`);
// Очищаем корзину
cart.clear();
renderCart();
console.log('🧹 Корзина очищена');
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
if (modal) {
modal.hide();
console.log('❌ Модалка закрыта');
}
// Перезагружаем страницу для обновления остатков товаров
console.log('🔄 Перезагрузка страницы...');
setTimeout(() => {
window.location.reload();
}, 500);
} else {
alert('Ошибка: ' + result.error);
}
} catch (error) {
console.error('Ошибка checkout:', error);
alert('Ошибка при проведении продажи: ' + error.message);
} finally {
// Разблокируем кнопку
const btn = document.getElementById('confirmCheckoutBtn');
btn.disabled = false;
btn.innerHTML = '
Подтвердить продажу';
}
}
// ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
// Кнопка "Выбрать клиента" в корзине
document.getElementById('customerSelectBtn').addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
modal.show();
});
// Кнопка сброса клиента на системного (в корзине)
document.getElementById('resetCustomerBtn').addEventListener('click', () => {
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
});
// Кнопка "Выбрать клиента" в модалке продажи
document.getElementById('checkoutCustomerSelectBtn').addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
modal.show();
});
// Кнопка сброса клиента на системного (в модалке продажи)
document.getElementById('checkoutResetCustomerBtn').addEventListener('click', () => {
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
});
// Кнопка "Создать нового клиента" в модалке выбора
document.getElementById('createNewCustomerBtn').addEventListener('click', () => {
// Закрываем модалку выбора
const selectModal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
selectModal.hide();
// Открываем модалку создания
openCreateCustomerModal();
});
// Кнопка "Выбрать системного клиента"
document.getElementById('selectSystemCustomerBtn').addEventListener('click', () => {
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
});
// Кнопка подтверждения создания клиента
document.getElementById('confirmCreateCustomerBtn').addEventListener('click', () => {
createNewCustomer();
});
// Инициализация Select2 при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
initCustomerSelect2();
updateCustomerDisplay(); // Обновляем UI с системным клиентом
// Восстанавливаем корзину из Redis (если есть сохраненные данные)
const savedCartData = JSON.parse(document.getElementById('cartData').textContent);
if (savedCartData && Object.keys(savedCartData).length > 0) {
// Конвертируем обычный объект обратно в Map
Object.entries(savedCartData).forEach(([key, value]) => {
cart.set(key, value);
});
renderCart(); // Отрисовываем восстановленную корзину
}
// ===== ОБРАБОТЧИКИ ДЛЯ МОДАЛКИ ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
// Кнопки изменения количества
document.getElementById('unitQtyDecrement').addEventListener('click', () => {
const input = document.getElementById('unitModalQuantity');
const step = parseFloat(input.step) || 1;
const newValue = Math.max(parseFloat(input.min), parseFloat(input.value) - step);
input.value = roundQuantity(newValue, 3);
calculateUnitModalSubtotal();
validateUnitQuantity();
});
document.getElementById('unitQtyIncrement').addEventListener('click', () => {
const input = document.getElementById('unitModalQuantity');
const step = parseFloat(input.step) || 1;
const newValue = parseFloat(input.value) + step;
input.value = roundQuantity(newValue, 3);
calculateUnitModalSubtotal();
validateUnitQuantity();
});
// Изменение количества вручную
document.getElementById('unitModalQuantity').addEventListener('input', () => {
calculateUnitModalSubtotal();
validateUnitQuantity();
});
// Округление количества при потере фокуса
document.getElementById('unitModalQuantity').addEventListener('blur', (e) => {
const rawValue = parseFloat(e.target.value) || 0;
e.target.value = roundQuantity(rawValue, 3);
calculateUnitModalSubtotal();
validateUnitQuantity();
});
// Изменение цены
document.getElementById('unitModalPrice').addEventListener('input', () => {
calculateUnitModalSubtotal();
});
// Кнопка подтверждения добавления в корзину
document.getElementById('confirmAddUnitToCart').addEventListener('click', () => {
addToCartFromModal();
});
// ===== МОБИЛЬНАЯ КОРЗИНА =====
// Тап на бар — открываем корзину
const mobileCartSummary = document.getElementById('mobileCartSummary');
if (mobileCartSummary) {
mobileCartSummary.addEventListener('click', () => {
const overlay = document.getElementById('mobileCartOverlay');
const body = document.getElementById('mobileCartBody');
// Копируем содержимое корзины
if (body && overlay) {
const cartList = document.getElementById('cartList');
body.innerHTML = cartList ? cartList.innerHTML : '
Корзина пуста
';
overlay.classList.add('active');
}
});
}
// Кнопка закрытия мобильной корзины
const mobileCartClose = document.getElementById('mobileCartClose');
if (mobileCartClose) {
mobileCartClose.addEventListener('click', () => {
const overlay = document.getElementById('mobileCartOverlay');
if (overlay) {
overlay.classList.remove('active');
}
});
}
// Закрытие по клику на фон
const mobileCartOverlay = document.getElementById('mobileCartOverlay');
if (mobileCartOverlay) {
mobileCartOverlay.addEventListener('click', (e) => {
if (e.target.id === 'mobileCartOverlay') {
e.target.classList.remove('active');
}
});
}
// Мобильная кнопка "Продать"
const mobileCheckoutBtn = document.getElementById('mobileCheckoutBtn');
if (mobileCheckoutBtn) {
mobileCheckoutBtn.addEventListener('click', () => {
const checkoutBtn = document.getElementById('checkoutNow');
if (checkoutBtn) {
checkoutBtn.click();
}
});
}
// Мобильная кнопка "Очистить"
const mobileClearCartBtn = document.getElementById('mobileClearCartBtn');
if (mobileClearCartBtn) {
mobileClearCartBtn.addEventListener('click', () => {
const clearBtn = document.getElementById('clearCart');
if (clearBtn) {
clearBtn.click();
}
});
}
// ===== СВОРАЧИВАНИЕ КАТЕГОРИЙ НА МОБИЛЬНЫХ =====
const categoriesToggle = document.getElementById('categoriesToggle');
const categoriesContent = document.getElementById('categoriesContent');
if (categoriesToggle && categoriesContent) {
categoriesToggle.addEventListener('click', () => {
categoriesToggle.classList.toggle('collapsed');
categoriesContent.classList.toggle('collapsed');
});
// Автоматически сворачиваем категории на мобильных при загрузке
if (window.innerWidth <= 767) {
categoriesToggle.classList.add('collapsed');
categoriesContent.classList.add('collapsed');
}
}
// ===== МОБИЛЬНЫЙ DROPDOWN "ЕЩЁ" =====
// Мобильная кнопка "Отложенный заказ"
const mobileScheduleLaterBtn = document.getElementById('mobileScheduleLaterBtn');
if (mobileScheduleLaterBtn) {
mobileScheduleLaterBtn.addEventListener('click', () => {
const scheduleBtn = document.getElementById('scheduleLater');
if (scheduleBtn) {
scheduleBtn.click();
}
});
}
// Мобильная кнопка "На витрину"
const mobileAddToShowcaseBtn = document.getElementById('mobileAddToShowcaseBtn');
if (mobileAddToShowcaseBtn) {
mobileAddToShowcaseBtn.addEventListener('click', () => {
const showcaseBtn = document.getElementById('addToShowcaseBtn');
if (showcaseBtn) {
showcaseBtn.click();
}
});
}
});
// Смена склада
const changeWarehouseBtn = document.getElementById('changeWarehouseBtn');
if (changeWarehouseBtn) {
changeWarehouseBtn.addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById('selectWarehouseModal'));
modal.show();
});
}
// Обработка выбора склада из списка
document.addEventListener('click', async (e) => {
const warehouseItem = e.target.closest('.warehouse-item');
if (!warehouseItem) return;
const warehouseId = warehouseItem.dataset.warehouseId;
const warehouseName = warehouseItem.dataset.warehouseName;
// Проверяем, есть ли товары в корзине
if (cart.size > 0) {
const confirmed = confirm(`При смене склада корзина будет очищена.\n\nПереключиться на склад "${warehouseName}"?`);
if (!confirmed) return;
}
try {
// Отправляем запрос на смену склада
const response = await fetch(`/pos/api/set-warehouse/${warehouseId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
});
const data = await response.json();
if (data.success) {
// Перезагружаем страницу для обновления данных
location.reload();
} else {
alert(`Ошибка: ${data.error}`);
}
} catch (error) {
console.error('Ошибка при смене склада:', error);
alert('Произошла ошибка при смене склада');
}
});
// Обработчик поиска с debounce
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearchBtn');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
// Показываем/скрываем кнопку очистки
if (e.target.value.length > 0) {
clearSearchBtn.style.display = 'block';
} else {
clearSearchBtn.style.display = 'none';
}
// Отменяем предыдущий таймер
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer);
}
// Если поле пустое — очищаем экран
if (query === '') {
currentSearchQuery = '';
ITEMS = []; // Очистка
renderProducts(); // Пустой экран
return;
}
// Минимальная длина поиска — 3 символа
if (query.length < 3) {
// Не реагируем на ввод менее 3 символов
return;
}
// Для витрины — мгновенная клиентская фильтрация
if (isShowcaseView) {
renderProducts();
return;
}
// Для обычных товаров/комплектов — серверный поиск с debounce 300мс
searchDebounceTimer = setTimeout(async () => {
currentSearchQuery = query;
await loadItems(); // Перезагрузка с серверным поиском
}, 300);
});
// При нажатии Enter на searchInput - скрываем виртуальную клавиатуру
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
searchInput.blur(); // Скрывает виртуальную клавиатуру на мобильных
}
});
// Обработчик кнопки очистки поиска
clearSearchBtn.addEventListener('click', () => {
searchInput.value = '';
clearSearchBtn.style.display = 'none';
currentSearchQuery = '';
ITEMS = [];
renderProducts(); // Пустой экран
});
// Инициализация
renderCategories();
renderProducts(); // Сначала пустая сетка
renderCart();
setupInfiniteScroll(); // Установка infinite scroll
// Установить фокус на строку поиска только на десктопе
if (!isMobileDevice()) {
document.getElementById('searchInput').focus();
}
// ===== ОТЛОЖЕННЫЙ ЗАКАЗ =====
/**
* Создаёт отложенный заказ (черновик) и резервирует витринные букеты
*
* FLOW:
* 1. Создаём Order (статус 'draft') через API
* 2. ShowcaseItem резервируются в той же транзакции (in_cart → reserved)
* 3. Очищаем корзину POS
* 4. Открываем форму редактирования заказа
*/
async function createDeferredOrder() {
// Проверяем, что корзина не пуста
if (cart.size === 0) {
alert('Корзина пуста! Добавьте товары в корзину.');
return;
}
try {
// Собираем данные для черновика
const items = Array.from(cart.values()).map(item => {
const itemData = {
type: item.type,
id: item.id,
quantity: item.qty,
price: item.price
};
// Для товаров с единицами продажи передаём sales_unit_id
if (item.sales_unit_id) {
itemData.sales_unit_id = item.sales_unit_id;
}
// Для витринных букетов передаём showcase_item_ids
if (item.type === 'showcase_kit' && item.showcase_item_ids) {
itemData.showcase_item_ids = item.showcase_item_ids;
}
return itemData;
});
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const orderData = {
customer_id: customer.id,
items: items
};
// Создаём заказ через новый endpoint
const response = await fetch('/orders/api/create-from-pos/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(orderData)
});
const result = await response.json();
if (result.success) {
console.log(`✅ Заказ #${result.order_number} создан (черновик). ShowcaseItem зарезервированы.`);
// КРИТИЧНО: Очищаем корзину POS (включая витринные ��укеты)
cart.clear();
renderCart();
saveCartToRedis(); // Сохраняем пустую корзину в Redis
// Перезагружаем витрину (чтобы зарезервированные букеты исчезли)
if (isShowcaseView) {
await refreshShowcaseKits();
renderProducts();
}
// Открываем форму редактирования в новой вкладке
window.open(`/orders/${result.order_number}/edit/`, '_blank');
} else {
alert(`Ошибка: ${result.error}`);
}
} catch (error) {
console.error('Ошибка при создании отложенного заказа:', error);
alert('Произошла ошибка при создании черновика заказа');
}
}
// Обработчик кнопки "ОТЛОЖЕННЫЙ заказ"
const scheduleLaterBtn = document.getElementById('scheduleLater');
if (scheduleLaterBtn) {
scheduleLaterBtn.addEventListener('click', createDeferredOrder);
}