// 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(); // Экспорт корзины для использования в других модулях window.cart = cart; // Переменные для пагинации 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 isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id); const displayName = isSystemCustomer ? 'Выбрать клиента' : selectedCustomer.name; // Обновляем текст кнопки в корзине const btnText = document.getElementById('customerSelectBtnText'); if (btnText) { btnText.textContent = displayName; } // Обновляем текст кнопки в модалке продажи const checkoutBtnText = document.getElementById('checkoutCustomerSelectBtnText'); if (checkoutBtnText) { checkoutBtnText.textContent = displayName; } // Обновляем видимость кнопок сброса (в корзине и в модалке продажи) [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 $(' ' + customer.text + ''); } // Формируем текст в одну строку: Имя (жирным) + контакты (мелким) const parts = []; // Имя const name = customer.name || customer.text; parts.push('' + $('
').text(name).html() + ''); // Телефон и Email const contactInfo = []; if (customer.phone) { contactInfo.push($('
').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); }