Files
octopus/myproject/pos/static/pos/js/terminal.js
Andrey Smakotin b7db4cd162 conventional-commit
feat(inventory): introduce stock deficit notifications and base quantity tracking

- Added `quantity_base` field to reservation model for precise inventory calculations
- Implemented non-blocking stock deficit warnings during kit creation process
- Enhanced API responses with warning details for frontend display
- Updated terminal interface to show formatted stock shortage alerts

BREAKING CHANGE: API response structure now includes `warnings` array instead of previous stock warning format
2026-01-04 16:18:57 +03:00

2736 lines
101 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// POS Terminal JavaScript
/**
* Округляет число до N знаков после запятой для корректного отображения.
* Решает проблему погрешности float arithmetic в JavaScript.
* @param {number} value - Число для округления
* @param {number} decimals - Количество знаков после запятой (по умолчанию 3)
* @returns {number} Округлённое число
*/
function roundQuantity(value, decimals = 3) {
if (value === null || value === undefined || isNaN(value)) return 0;
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent);
let ITEMS = []; // Будем загружать через API
let showcaseKits = JSON.parse(document.getElementById('showcaseKitsData').textContent);
let currentCategoryId = null;
let isShowcaseView = false;
const cart = new Map();
// Переменные для пагинации
let currentPage = 1;
let hasMoreItems = false;
let isLoadingItems = false;
let currentSearchQuery = ''; // Текущий поисковый запрос
let searchDebounceTimer = null;
// Переменные для режима редактирования
let isEditMode = false;
let editingKitId = null;
// Временная корзина для модального окна создания/редактирования комплекта
const tempCart = new Map();
// ===== ПЕРЕМЕННЫЕ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
let unitModalProduct = null; // Текущий товар для модального окна
let unitModalSalesUnits = []; // Список единиц продажи
let selectedSalesUnit = null; // Выбранная единица продажи
let unitModalInstance = null; // Bootstrap Modal instance
// ===== СОХРАНЕНИЕ КОРЗИНЫ В REDIS =====
let saveCartTimeout = null;
/**
* Сохраняет корзину в Redis с debounce 500ms
*/
function saveCartToRedis() {
// Отменяем предыдущий таймер
if (saveCartTimeout) {
clearTimeout(saveCartTimeout);
}
// Устанавливаем новый таймер
saveCartTimeout = setTimeout(() => {
// Конвертируем Map в обычный объект
const cartObj = {};
cart.forEach((value, key) => {
cartObj[key] = value;
});
// Отправляем на сервер
fetch('/pos/api/save-cart/', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ cart: cartObj })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Ошибка сохранения корзины:', data.error);
}
})
.catch(error => {
console.error('Ошибка при сохранении корзины в Redis:', error);
});
}, 500); // Debounce 500ms
}
// ===== УПРАВЛЕНИЕ КЛИЕНТОМ И СКЛАДОМ =====
// Загружаем данные системного клиента
const SYSTEM_CUSTOMER = JSON.parse(document.getElementById('systemCustomerData').textContent);
// Текущий выбранный клиент (загружается из Redis или системный)
let selectedCustomer = JSON.parse(document.getElementById('selectedCustomerData').textContent);
// Текущий склад
const currentWarehouse = JSON.parse(document.getElementById('currentWarehouseData').textContent);
function formatMoney(v) {
return (Number(v)).toFixed(2);
}
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
/**
* Обновляет отображение выбранного клиента в UI
* Обновляет:
* - Кнопку "Выбрать клиента" в корзине (показывает имя клиента)
* - Кнопку "Выбрать клиента" в модалке продажи (показывает имя клиента)
* - Видимость кнопок сброса в обоих местах (показываем только для не-системного клиента)
* - Ссылку на анкету клиента (показываем только для не-системного клиента)
* - Баланс кошелька в модальном окне продажи (если оно открыто)
*/
function updateCustomerDisplay() {
// Обновляем текст кнопки в корзине
const btnText = document.getElementById('customerSelectBtnText');
if (btnText) {
btnText.textContent = selectedCustomer.name;
}
// Обновляем текст кнопки в модалке продажи
const checkoutBtnText = document.getElementById('checkoutCustomerSelectBtnText');
if (checkoutBtnText) {
checkoutBtnText.textContent = selectedCustomer.name;
}
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
// Приводим к числу для надёжного сравнения (JSON может вернуть разные типы)
const isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id);
[document.getElementById('resetCustomerBtn'),
document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
if (resetBtn) {
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
}
});
// Обновляем ссылку на анкету клиента
const profileLink = document.getElementById('customerProfileLink');
if (profileLink) {
if (isSystemCustomer) {
profileLink.style.display = 'none';
} else {
profileLink.href = `/customers/${selectedCustomer.id}/`;
profileLink.style.display = 'block';
}
}
// Обновляем баланс кошелька в модальном окне продажи (если оно открыто)
updateCheckoutWalletBalance();
}
/**
* Обновляет баланс кошелька клиента в модальном окне продажи
*/
function updateCheckoutWalletBalance() {
const walletDiv = document.getElementById('checkoutWalletBalance');
if (!walletDiv) return; // Модалка еще не инициализирована
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const walletBalance = customer.wallet_balance || 0;
const isSystemCustomer = Number(customer.id) === Number(SYSTEM_CUSTOMER.id);
if (!isSystemCustomer) {
document.getElementById('checkoutWalletBalanceAmount').textContent = walletBalance.toFixed(2);
walletDiv.style.display = 'block';
} else {
walletDiv.style.display = 'none';
}
// Переинициализируем виджет оплаты, если модалка открыта
updatePaymentWidgetCustomer();
}
/**
* Обновляет данные клиента в виджете оплаты (если он инициализирован)
*/
function updatePaymentWidgetCustomer() {
// Проверяем, открыта ли модалка checkout и инициализирован ли виджет
const checkoutModal = document.getElementById('checkoutModal');
if (!checkoutModal || !checkoutModal.classList.contains('show')) return;
if (!paymentWidget) return;
// Получаем текущий режим оплаты
const isMixedMode = document.getElementById('mixedPaymentMode').classList.contains('active');
const mode = isMixedMode ? 'mixed' : 'single';
// Переинициализируем виджет с новыми данными клиента
reinitPaymentWidget(mode);
}
/**
* Устанавливает нового клиента и сохраняет в Redis
* @param {number} customerId - ID клиента
* @param {string} customerName - Имя клиента
* @param {number} walletBalance - Баланс кошелька клиента (опционально)
*/
function selectCustomer(customerId, customerName, walletBalance = 0) {
selectedCustomer = {
id: customerId,
name: customerName,
wallet_balance: walletBalance
};
updateCustomerDisplay();
// Сохраняем выбор в Redis через AJAX
fetch(`/pos/api/set-customer/${customerId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Ошибка сохранения клиента:', data.error);
} else {
// Обновляем баланс из ответа сервера
selectedCustomer.wallet_balance = data.wallet_balance || 0;
}
})
.catch(error => {
console.error('Ошибка при сохранении клиента в Redis:', error);
});
}
/**
* Инициализация Select2 для поиска клиента
*/
function initCustomerSelect2() {
const $searchInput = $('#customerSearchInput');
$searchInput.select2({
theme: 'bootstrap-5',
dropdownParent: $('#selectCustomerModal'),
placeholder: 'Начните вводить имя, телефон или email (минимум 3 символа)',
minimumInputLength: 3,
allowClear: true,
ajax: {
url: '/customers/api/search/',
dataType: 'json',
delay: 300,
data: function(params) {
return {
q: params.term
};
},
processResults: function(data) {
return {
results: data.results
};
},
cache: true
},
templateResult: formatCustomerOption, // Форматирование результатов в выпадающем списке
templateSelection: formatCustomerSelection // Форматирование выбранного значения
});
// Обработка выбора клиента из списка
$searchInput.on('select2:select', function(e) {
const data = e.params.data;
// Проверяем это не опция "Создать нового клиента"
if (data.id === 'create_new') {
// Открываем модалку создания
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
openCreateCustomerModal(data.text);
return;
}
// Выбираем клиента с балансом
selectCustomer(parseInt(data.id), data.name, data.wallet_balance || 0);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
// Очищаем Select2
$searchInput.val(null).trigger('change');
});
}
/**
* Форматирование опции клиента в выпадающем списке Select2
* Показывает: Имя, телефон, email в одну строку
*/
function formatCustomerOption(customer) {
if (customer.loading) {
return customer.text;
}
// Если это опция "Создать нового клиента"
if (customer.id === 'create_new') {
return $('<span><i class="bi bi-person-plus"></i> ' + customer.text + '</span>');
}
// Формируем текст в одну строку: Имя (жирным) + контакты (мелким)
const parts = [];
// Имя
const name = customer.name || customer.text;
parts.push('<span class="fw-bold">' + $('<div>').text(name).html() + '</span>');
// Телефон и Email
const contactInfo = [];
if (customer.phone) {
contactInfo.push($('<div>').text(customer.phone).html());
}
if (customer.email) {
contactInfo.push($('<div>').text(customer.email).html());
}
if (contactInfo.length > 0) {
parts.push('<span class="text-muted small"> (' + contactInfo.join(', ') + ')</span>');
}
return $('<span>' + parts.join('') + '</span>');
}
/**
* Форматирование выбранного клиента в поле 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} ${unit.unit_short_name} доступно`;
} else if (availableQty > 0) {
stockBadgeClass = 'stock-badge-low';
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
}
// Бейдж "По умолчанию"
const defaultBadge = unit.is_default ?
'<span class="badge bg-primary ms-2">По умолчанию</span>' : '';
card.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="unit-name">${unit.name}${defaultBadge}</div>
<div class="unit-code text-muted">${unit.unit_code} (${unit.unit_short_name})</div>
</div>
<div class="unit-price">${formatMoney(unit.actual_price)} руб</div>
</div>
<div class="mt-2">
<span class="badge ${stockBadgeClass}">${stockText}</span>
</div>
`;
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} (${unit.unit_short_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,
unit_short_name: selectedSalesUnit.unit_short_name,
quantity_step: parseFloat(selectedSalesUnit.quantity_step) || 1, // Сохраняем шаг количества
price_overridden: priceOverridden
});
}
// Обновляем корзину
renderCart();
saveCartToRedis();
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView) {
renderProducts();
}
// Закрываем модальное окно
unitModalInstance.hide();
}
function renderCategories() {
const grid = document.getElementById('categoryGrid');
grid.innerHTML = '';
// Кнопка "Витрина" - первая в ряду
const showcaseCol = document.createElement('div');
showcaseCol.className = 'col-6 col-sm-4 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 = '<i class="bi bi-flower1"></i> <strong>ВИТРИНА</strong>';
showcaseBody.appendChild(showcaseName);
showcaseCard.appendChild(showcaseBody);
showcaseCol.appendChild(showcaseCard);
grid.appendChild(showcaseCol);
// Кнопка "Все"
const allCol = document.createElement('div');
allCol.className = 'col-6 col-sm-4 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-sm-4 col-md-3 col-lg-2';
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) {
filtered = filtered.filter(item => {
const name = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return name.includes(searchTerm) || sku.includes(searchTerm);
});
}
} else {
// Обычный режим - ITEMS уже отфильтрованы на сервере (категория + поиск)
filtered = ITEMS;
}
filtered.forEach(item => {
const col = document.createElement('div');
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
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 = '<i class="bi bi-cart-check"></i> В корзине';
lockBadge.title = 'Добавлен в вашу корзину';
} else {
// Заблокирован другим кассиром - красный бейдж + блокируем карточку
lockBadge.className = 'badge bg-danger';
lockBadge.innerHTML = '<i class="bi bi-lock-fill"></i> Занят';
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 = '<i class="bi bi-pencil"></i>';
editBtn.onclick = (e) => {
e.stopPropagation();
openEditKitModal(item.id);
};
card.appendChild(editBtn);
}
}
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 = '<i class="bi bi-image"></i>';
}
// Информация о товаре/комплекте
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 ? ` <span class="badge bg-warning text-dark">🛒${inCart}</span>` : '';
stock.innerHTML = `🌺 ${item.showcase_name} <span class="badge ${badgeClass} ms-1">${badgeText}</span>${cartInfo}`;
stock.style.color = '#856404';
stock.style.fontWeight = 'bold';
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
// Для обычных товаров показываем остатки: FREE(-RESERVED-IN_CART)
// FREE = доступно для продажи (available - reserved - в корзине)
const available = parseFloat(item.available_qty) || 0;
const reserved = parseFloat(item.reserved_qty) || 0;
// Вычитаем количество из корзины для визуализации
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';
freeSpan.style.fontStyle = 'normal';
// Отображаем резерв и корзину если они есть
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';
suffixSpan.style.fontStyle = 'normal';
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 {
// 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';
priceSpan.textContent = `${formatMoney(item.price)}`;
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) {
// ПРОВЕРКА НА НАЛИЧИЕ НЕСКОЛЬКИХ ЕДИНИЦ ПРОДАЖИ
if (item.type === 'product' && item.sales_units_count > 1) {
// Открываем модальное окно выбора единицы
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': getCookie('csrftoken'),
'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]) {
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 = '<p class="text-muted text-center py-4 small">Корзина пуста</p>';
document.getElementById('cartTotal').textContent = '0.00';
updateShowcaseButtonState(); // Обновляем состояние кнопки
return;
}
cart.forEach((item, cartKey) => {
const row = document.createElement('div');
row.className = 'cart-item mb-2';
// СПЕЦИАЛЬНАЯ СТИЛИЗАЦИЯ для витринных комплектов
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 = '<i class="bi bi-box-seam text-info" title="Комплект"></i> ';
}
// Единица продажи (если есть)
let unitInfo = '';
if (item.sales_unit_id && item.unit_name) {
unitInfo = ` <span class="badge bg-secondary" style="font-size: 0.7rem;">${item.unit_name}</span>`;
}
namePrice.innerHTML = `
<div class="fw-semibold small">${typeIcon}${item.name}${unitInfo}</div>
<div class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)} / ${item.unit_short_name || 'шт'}</div>
`;
// Знак умножения
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 = '<i class="bi bi-dash-circle" style="font-size: 1.2em;"></i>';
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 = '<i class="bi bi-plus-circle" style="font-size: 1.2em;"></i>';
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 = '<i class="bi bi-dash-circle" style="font-size: 1.2em;"></i>';
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 = '<i class="bi bi-plus-circle" style="font-size: 1.2em;"></i>';
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 = '<i class="bi bi-x"></i>';
deleteBtn.onclick = () => removeFromCart(cartKey);
row.appendChild(namePrice);
row.appendChild(multiplySign);
row.appendChild(qtyControl);
row.appendChild(itemTotal);
row.appendChild(deleteBtn);
list.appendChild(row);
total += item.qty * item.price;
});
document.getElementById('cartTotal').textContent = formatMoney(total);
// Обновляем состояние кнопки "НА ВИТРИНУ"
updateShowcaseButtonState();
}
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': getCookie('csrftoken'),
'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': getCookie('csrftoken'),
'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': getCookie('csrftoken'),
'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': getCookie('csrftoken') }
});
} 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),
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();
} catch (error) {
console.error('Error loading kit for edit:', error);
alert('Ошибка при загрузке комплекта');
}
}
// Обновление списка витринных комплектов
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 = '<option value="">Выберите витрину...</option>';
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 = '<option value="">Нет доступных витрин</option>';
}
} 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 = `
<strong class="small">${item.name}</strong>
<br>
<small class="text-muted">${formatMoney(item.price)} руб. / шт.</small>
`;
// Правая часть: контролы количества и удаление
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 = '<i class="bi bi-dash"></i>';
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 = '<i class="bi bi-plus"></i>';
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 = `<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>`;
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-outline-danger';
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
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 = '<p class="text-muted text-center py-3"><i class="bi bi-inbox"></i> Нет товаров</p>';
}
// Обновляем все расчеты цен
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;
// Формируем 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);
if (useSalePrice && salePrice > 0) {
formData.append('sale_price', salePrice);
}
// Фото: для редактирования проверяем, удалено ли оно
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 = `<span class="spinner-border spinner-border-sm me-2"></span>${actionText}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
// Не указываем 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
? '<i class="bi bi-check-circle"></i> Сохранить изменения'
: '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
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': getCookie('csrftoken')
}
});
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('Произошла ошибка при разборе букета');
}
});
// Вспомогательная функция для получения 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;
}
// Алиас для обратной совместимости
const getCsrfToken = () => 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 = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
// Скрываем кнопку "Разобрать" и блок добавления товаров
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 = '<i class="bi bi-box-seam text-info me-1" title="Комплект"></i>';
} else {
typeIcon = '<i class="bi bi-box text-success me-1" title="Товар"></i>';
}
row.innerHTML = `
<div>
<div class="fw-semibold">${typeIcon}${item.name}</div>
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
</div>
<div class="fw-bold text-end">${formatMoney(item.qty * item.price)} руб.</div>
`;
container.appendChild(row);
total += item.qty * item.price;
});
// Обновляем информацию о клиенте
updateCustomerDisplay();
}
// ===== CHECKOUT: ПОДТВЕРЖДЕНИЕ ПРОДАЖИ =====
let paymentWidget = null;
// При открытии модалки checkout
document.getElementById('checkoutModal').addEventListener('show.bs.modal', () => {
const customer = selectedCustomer || SYSTEM_CUSTOMER;
const walletBalance = customer.wallet_balance || 0;
// Показываем баланс кошелька
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;
});
document.getElementById('checkoutFinalPrice').textContent = formatMoney(totalAmount) + ' руб.';
// Инициализируем виджет в single mode
initPaymentWidget('single', {
order: { total: totalAmount, amount_due: totalAmount, 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 }
});
}
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 = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
// Собираем данные
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()
};
// Отправляем на сервер
const response = await fetch('/pos/api/checkout/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
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 = '<i class="bi bi-check2-circle"></i> Подтвердить продажу';
}
}
// ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
// Кнопка "Выбрать клиента" в корзине
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 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);
});
// Обработчик кнопки очистки поиска
clearSearchBtn.addEventListener('click', () => {
searchInput.value = '';
clearSearchBtn.style.display = 'none';
currentSearchQuery = '';
ITEMS = [];
renderProducts(); // Пустой экран
});
// Инициализация
renderCategories();
renderProducts(); // Сначала пустая сетка
renderCart();
setupInfiniteScroll(); // Установка infinite scroll
// Установить фокус на строку поиска
document.getElementById('searchInput').focus();
// ===== ОТЛОЖЕННЫЙ ЗАКАЗ =====
/**
* Открывает страницу создания заказа с предзаполненными товарами и клиентом
*/
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 draftData = {
customer_id: customer.id,
items: items
};
// Отправляем на сервер
const response = await fetch('/pos/api/create-draft/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(draftData)
});
const result = await response.json();
if (result.success) {
// Открываем в новой вкладке
window.open(`/orders/create/?draft=${result.token}`, '_blank');
} else {
alert(`Ошибка: ${result.error}`);
}
} catch (error) {
console.error('Ошибка при создании отложенного заказа:', error);
alert('Произошла ошибка при создании черновика заказа');
}
}
// Обработчик кнопки "ОТЛОЖЕННЫЙ заказ"
const scheduleLaterBtn = document.getElementById('scheduleLater');
if (scheduleLaterBtn) {
scheduleLaterBtn.addEventListener('click', createDeferredOrder);
}