- Добавлен pre_save сигнал для Order вместо django-simple-history - Переписаны все функции signals.py без использования instance.history - Заменены .username на .name|default:.email для CustomUser в шаблонах - Исправлен CSRF-токен в POS для работы с CSRF_USE_SESSIONS=True Теперь создание заказов работает корректно в мультитенантной архитектуре.
2778 lines
103 KiB
JavaScript
2778 lines
103 KiB
JavaScript
// POS Terminal JavaScript
|
||
|
||
/**
|
||
* Округляет число до N знаков после запятой для корректного отображения.
|
||
* Решает проблему погрешности float arithmetic в JavaScript.
|
||
* @param {number} value - Число для округления
|
||
* @param {number} decimals - Количество знаков после запятой (по умолчанию 3)
|
||
* @returns {number} Округлённое число
|
||
*/
|
||
function roundQuantity(value, decimals = 3) {
|
||
if (value === null || value === undefined || isNaN(value)) return 0;
|
||
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||
}
|
||
|
||
const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent);
|
||
let ITEMS = []; // Будем загружать через API
|
||
let showcaseKits = JSON.parse(document.getElementById('showcaseKitsData').textContent);
|
||
|
||
let currentCategoryId = null;
|
||
let isShowcaseView = false;
|
||
const cart = new Map();
|
||
|
||
// Переменные для пагинации
|
||
let currentPage = 1;
|
||
let hasMoreItems = false;
|
||
let isLoadingItems = false;
|
||
let currentSearchQuery = ''; // Текущий поисковый запрос
|
||
let searchDebounceTimer = null;
|
||
|
||
// Переменные для режима редактирования
|
||
let isEditMode = false;
|
||
let editingKitId = null;
|
||
|
||
// Временная корзина для модального окна создания/редактирования комплекта
|
||
const tempCart = new Map();
|
||
|
||
// ===== ПЕРЕМЕННЫЕ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||
let unitModalProduct = null; // Текущий товар для модального окна
|
||
let unitModalSalesUnits = []; // Список единиц продажи
|
||
let selectedSalesUnit = null; // Выбранная единица продажи
|
||
let unitModalInstance = null; // Bootstrap Modal instance
|
||
|
||
// ===== СОХРАНЕНИЕ КОРЗИНЫ В REDIS =====
|
||
|
||
let saveCartTimeout = null;
|
||
|
||
/**
|
||
* Сохраняет корзину в Redis с debounce 500ms
|
||
*/
|
||
function saveCartToRedis() {
|
||
// Отменяем предыдущий таймер
|
||
if (saveCartTimeout) {
|
||
clearTimeout(saveCartTimeout);
|
||
}
|
||
|
||
// Устанавливаем новый таймер
|
||
saveCartTimeout = setTimeout(() => {
|
||
// Конвертируем Map в обычный объект
|
||
const cartObj = {};
|
||
cart.forEach((value, key) => {
|
||
cartObj[key] = value;
|
||
});
|
||
|
||
// Отправляем на сервер
|
||
fetch('/pos/api/save-cart/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCsrfToken(),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ cart: cartObj })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (!data.success) {
|
||
console.error('Ошибка сохранения корзины:', data.error);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка при сохранении корзины в Redis:', error);
|
||
});
|
||
}, 500); // Debounce 500ms
|
||
}
|
||
|
||
// ===== УПРАВЛЕНИЕ КЛИЕНТОМ И СКЛАДОМ =====
|
||
// Загружаем данные системного клиента
|
||
const SYSTEM_CUSTOMER = JSON.parse(document.getElementById('systemCustomerData').textContent);
|
||
|
||
// Текущий выбранный клиент (загружается из Redis или системный)
|
||
let selectedCustomer = JSON.parse(document.getElementById('selectedCustomerData').textContent);
|
||
|
||
// Текущий склад
|
||
const currentWarehouse = JSON.parse(document.getElementById('currentWarehouseData').textContent);
|
||
|
||
function formatMoney(v) {
|
||
return (Number(v)).toFixed(2);
|
||
}
|
||
|
||
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
||
|
||
/**
|
||
* Обновляет отображение выбранного клиента в UI
|
||
* Обновляет:
|
||
* - Кнопку "Выбрать клиента" в корзине (показывает имя клиента)
|
||
* - Кнопку "Выбрать клиента" в модалке продажи (показывает имя клиента)
|
||
* - Видимость кнопок сброса в обоих местах (показываем только для не-системного клиента)
|
||
* - Ссылку на анкету клиента (показываем только для не-системного клиента)
|
||
* - Баланс кошелька в модальном окне продажи (если оно открыто)
|
||
*/
|
||
function updateCustomerDisplay() {
|
||
// Обновляем текст кнопки в корзине
|
||
const btnText = document.getElementById('customerSelectBtnText');
|
||
if (btnText) {
|
||
btnText.textContent = selectedCustomer.name;
|
||
}
|
||
|
||
// Обновляем текст кнопки в модалке продажи
|
||
const checkoutBtnText = document.getElementById('checkoutCustomerSelectBtnText');
|
||
if (checkoutBtnText) {
|
||
checkoutBtnText.textContent = selectedCustomer.name;
|
||
}
|
||
|
||
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
|
||
// Приводим к числу для надёжного сравнения (JSON может вернуть разные типы)
|
||
const isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id);
|
||
|
||
[document.getElementById('resetCustomerBtn'),
|
||
document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
|
||
if (resetBtn) {
|
||
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
|
||
}
|
||
});
|
||
|
||
// Обновляем ссылку на анкету клиента
|
||
const profileLink = document.getElementById('customerProfileLink');
|
||
if (profileLink) {
|
||
if (isSystemCustomer) {
|
||
profileLink.style.display = 'none';
|
||
} else {
|
||
profileLink.href = `/customers/${selectedCustomer.id}/`;
|
||
profileLink.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Обновляем баланс кошелька в модальном окне продажи (если оно открыто)
|
||
updateCheckoutWalletBalance();
|
||
}
|
||
|
||
/**
|
||
* Обновляет баланс кошелька клиента в модальном окне продажи
|
||
*/
|
||
function updateCheckoutWalletBalance() {
|
||
const walletDiv = document.getElementById('checkoutWalletBalance');
|
||
if (!walletDiv) return; // Модалка еще не инициализирована
|
||
|
||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||
const walletBalance = customer.wallet_balance || 0;
|
||
const isSystemCustomer = Number(customer.id) === Number(SYSTEM_CUSTOMER.id);
|
||
|
||
if (!isSystemCustomer) {
|
||
document.getElementById('checkoutWalletBalanceAmount').textContent = walletBalance.toFixed(2);
|
||
walletDiv.style.display = 'block';
|
||
} else {
|
||
walletDiv.style.display = 'none';
|
||
}
|
||
|
||
// Переинициализируем виджет оплаты, если модалка открыта
|
||
updatePaymentWidgetCustomer();
|
||
}
|
||
|
||
/**
|
||
* Обновляет данные клиента в виджете оплаты (если он инициализирован)
|
||
*/
|
||
function updatePaymentWidgetCustomer() {
|
||
// Проверяем, открыта ли модалка checkout и инициализирован ли виджет
|
||
const checkoutModal = document.getElementById('checkoutModal');
|
||
if (!checkoutModal || !checkoutModal.classList.contains('show')) return;
|
||
if (!paymentWidget) return;
|
||
|
||
// Получаем текущий режим оплаты
|
||
const isMixedMode = document.getElementById('mixedPaymentMode').classList.contains('active');
|
||
const mode = isMixedMode ? 'mixed' : 'single';
|
||
|
||
// Переинициализируем виджет с новыми данными клиента
|
||
reinitPaymentWidget(mode);
|
||
}
|
||
|
||
/**
|
||
* Устанавливает нового клиента и сохраняет в Redis
|
||
* @param {number} customerId - ID клиента
|
||
* @param {string} customerName - Имя клиента
|
||
* @param {number} walletBalance - Баланс кошелька клиента (опционально)
|
||
*/
|
||
function selectCustomer(customerId, customerName, walletBalance = 0) {
|
||
selectedCustomer = {
|
||
id: customerId,
|
||
name: customerName,
|
||
wallet_balance: walletBalance
|
||
};
|
||
updateCustomerDisplay();
|
||
|
||
// Сохраняем выбор в Redis через AJAX
|
||
fetch(`/pos/api/set-customer/${customerId}/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCsrfToken(),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (!data.success) {
|
||
console.error('Ошибка сохранения клиента:', data.error);
|
||
} else {
|
||
// Обновляем баланс из ответа сервера
|
||
selectedCustomer.wallet_balance = data.wallet_balance || 0;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка при сохранении клиента в Redis:', error);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Инициализация Select2 для поиска клиента
|
||
*/
|
||
function initCustomerSelect2() {
|
||
const $searchInput = $('#customerSearchInput');
|
||
|
||
$searchInput.select2({
|
||
theme: 'bootstrap-5',
|
||
dropdownParent: $('#selectCustomerModal'),
|
||
placeholder: 'Начните вводить имя, телефон или email (минимум 3 символа)',
|
||
minimumInputLength: 3,
|
||
allowClear: true,
|
||
ajax: {
|
||
url: '/customers/api/search/',
|
||
dataType: 'json',
|
||
delay: 300,
|
||
data: function(params) {
|
||
return {
|
||
q: params.term
|
||
};
|
||
},
|
||
processResults: function(data) {
|
||
return {
|
||
results: data.results
|
||
};
|
||
},
|
||
cache: true
|
||
},
|
||
templateResult: formatCustomerOption, // Форматирование результатов в выпадающем списке
|
||
templateSelection: formatCustomerSelection // Форматирование выбранного значения
|
||
});
|
||
|
||
// Обработка выбора клиента из списка
|
||
$searchInput.on('select2:select', function(e) {
|
||
const data = e.params.data;
|
||
|
||
// Проверяем это не опция "Создать нового клиента"
|
||
if (data.id === 'create_new') {
|
||
// Открываем модалку создания
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
|
||
modal.hide();
|
||
openCreateCustomerModal(data.text);
|
||
return;
|
||
}
|
||
|
||
// Выбираем клиента с балансом
|
||
selectCustomer(parseInt(data.id), data.name, data.wallet_balance || 0);
|
||
|
||
// Закрываем модалку
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
|
||
modal.hide();
|
||
|
||
// Очищаем Select2
|
||
$searchInput.val(null).trigger('change');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Форматирование опции клиента в выпадающем списке Select2
|
||
* Показывает: Имя, телефон, email в одну строку
|
||
*/
|
||
function formatCustomerOption(customer) {
|
||
if (customer.loading) {
|
||
return customer.text;
|
||
}
|
||
|
||
// Если это опция "Создать нового клиента"
|
||
if (customer.id === 'create_new') {
|
||
return $('<span><i class="bi bi-person-plus"></i> ' + customer.text + '</span>');
|
||
}
|
||
|
||
// Формируем текст в одну строку: Имя (жирным) + контакты (мелким)
|
||
const parts = [];
|
||
|
||
// Имя
|
||
const name = customer.name || customer.text;
|
||
parts.push('<span class="fw-bold">' + $('<div>').text(name).html() + '</span>');
|
||
|
||
// Телефон и Email
|
||
const contactInfo = [];
|
||
if (customer.phone) {
|
||
contactInfo.push($('<div>').text(customer.phone).html());
|
||
}
|
||
if (customer.email) {
|
||
contactInfo.push($('<div>').text(customer.email).html());
|
||
}
|
||
|
||
if (contactInfo.length > 0) {
|
||
parts.push('<span class="text-muted small"> (' + contactInfo.join(', ') + ')</span>');
|
||
}
|
||
|
||
return $('<span>' + parts.join('') + '</span>');
|
||
}
|
||
|
||
/**
|
||
* Форматирование выбранного клиента в поле Select2
|
||
* Показывает только имя
|
||
*/
|
||
function formatCustomerSelection(customer) {
|
||
return customer.name || customer.text;
|
||
}
|
||
|
||
/**
|
||
* Открывает модальное окно создания нового клиента
|
||
* @param {string} prefillName - Предзаполненное имя (из поиска)
|
||
*/
|
||
function openCreateCustomerModal(prefillName = '') {
|
||
const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
|
||
|
||
// Очищаем форму
|
||
document.getElementById('newCustomerName').value = prefillName || '';
|
||
document.getElementById('newCustomerPhone').value = '';
|
||
document.getElementById('newCustomerEmail').value = '';
|
||
document.getElementById('createCustomerError').classList.add('d-none');
|
||
|
||
modal.show();
|
||
}
|
||
|
||
/**
|
||
* Создаёт нового клиента через API
|
||
*/
|
||
async function createNewCustomer() {
|
||
const name = document.getElementById('newCustomerName').value.trim();
|
||
const phone = document.getElementById('newCustomerPhone').value.trim();
|
||
const email = document.getElementById('newCustomerEmail').value.trim();
|
||
const errorBlock = document.getElementById('createCustomerError');
|
||
|
||
// Валидация
|
||
if (!name) {
|
||
errorBlock.textContent = 'Укажите имя клиента';
|
||
errorBlock.classList.remove('d-none');
|
||
return;
|
||
}
|
||
|
||
// Скрываем ошибку
|
||
errorBlock.classList.add('d-none');
|
||
|
||
try {
|
||
const response = await fetch('/customers/api/create/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': getCsrfToken()
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
phone: phone || null,
|
||
email: email || null
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Выбираем созданного клиента с балансом
|
||
selectCustomer(data.id, data.name, data.wallet_balance || 0);
|
||
|
||
// Закрываем модалку
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
|
||
modal.hide();
|
||
|
||
// Показываем уведомление
|
||
alert(`Клиент "${data.name}" успешно создан!`);
|
||
} else {
|
||
// Показываем ошибку
|
||
errorBlock.textContent = data.error || 'Ошибка при создании клиента';
|
||
errorBlock.classList.remove('d-none');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating customer:', error);
|
||
errorBlock.textContent = 'Ошибка сети при создании клиента';
|
||
errorBlock.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
// ===== ФУНКЦИИ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||
|
||
/**
|
||
* Открывает модальное окно выбора единицы продажи
|
||
* @param {object} product - Объект товара с информацией о единицах продажи
|
||
*/
|
||
async function openProductUnitModal(product) {
|
||
unitModalProduct = product;
|
||
|
||
// Устанавливаем название товара
|
||
document.getElementById('unitModalProductName').textContent =
|
||
`${product.name}${product.sku ? ' (' + product.sku + ')' : ''}`;
|
||
|
||
// Загружаем единицы продажи
|
||
try {
|
||
const response = await fetch(
|
||
`/products/api/products/${product.id}/sales-units/?warehouse=${currentWarehouse.id}`
|
||
);
|
||
const data = await response.json();
|
||
|
||
if (!data.success || !data.sales_units || data.sales_units.length === 0) {
|
||
alert('Не удалось загрузить единицы продажи');
|
||
return;
|
||
}
|
||
|
||
unitModalSalesUnits = data.sales_units;
|
||
|
||
// Отрисовываем список единиц
|
||
renderUnitSelectionList();
|
||
|
||
// Выбираем единицу по умолчанию или первую
|
||
const defaultUnit = unitModalSalesUnits.find(u => u.is_default) || unitModalSalesUnits[0];
|
||
if (defaultUnit) {
|
||
selectUnit(defaultUnit);
|
||
}
|
||
|
||
// Открываем модальное окно
|
||
if (!unitModalInstance) {
|
||
unitModalInstance = new bootstrap.Modal(document.getElementById('selectProductUnitModal'));
|
||
}
|
||
unitModalInstance.show();
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки единиц продажи:', error);
|
||
alert('Ошибка загрузки данных. Попробуйте ещё раз.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрисовывает список единиц продажи
|
||
*/
|
||
function renderUnitSelectionList() {
|
||
const listContainer = document.getElementById('unitSelectionList');
|
||
listContainer.innerHTML = '';
|
||
|
||
unitModalSalesUnits.forEach(unit => {
|
||
const card = document.createElement('div');
|
||
card.className = 'unit-selection-card';
|
||
card.dataset.unitId = unit.id;
|
||
card.onclick = () => selectUnit(unit);
|
||
|
||
// Доступное количество
|
||
const availableQty = parseFloat(unit.available_quantity || 0);
|
||
let stockBadgeClass = 'stock-badge-none';
|
||
let stockText = 'Нет на складе';
|
||
|
||
if (availableQty > 10) {
|
||
stockBadgeClass = 'stock-badge-good';
|
||
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
|
||
} else if (availableQty > 0) {
|
||
stockBadgeClass = 'stock-badge-low';
|
||
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
|
||
}
|
||
|
||
// Бейдж "По умолчанию"
|
||
const defaultBadge = unit.is_default ?
|
||
'<span class="badge bg-primary ms-2">По умолчанию</span>' : '';
|
||
|
||
card.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div class="flex-grow-1">
|
||
<div class="unit-name">${unit.name}${defaultBadge}</div>
|
||
<div class="unit-code text-muted">${unit.unit_code} (${unit.unit_short_name})</div>
|
||
</div>
|
||
<div class="unit-price">${formatMoney(unit.actual_price)} руб</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<span class="badge ${stockBadgeClass}">${stockText}</span>
|
||
</div>
|
||
`;
|
||
|
||
listContainer.appendChild(card);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Выбирает единицу продажи
|
||
* @param {object} unit - Объект единицы продажи
|
||
*/
|
||
function selectUnit(unit) {
|
||
selectedSalesUnit = unit;
|
||
|
||
// Обновляем визуальное выделение
|
||
document.querySelectorAll('.unit-selection-card').forEach(card => {
|
||
if (card.dataset.unitId === String(unit.id)) {
|
||
card.classList.add('selected');
|
||
} else {
|
||
card.classList.remove('selected');
|
||
}
|
||
});
|
||
|
||
// Обновляем отображение выбранной единицы
|
||
document.getElementById('selectedUnitDisplay').textContent =
|
||
`${unit.name} (${unit.unit_short_name})`;
|
||
|
||
// Устанавливаем минимальное количество и шаг
|
||
const qtyInput = document.getElementById('unitModalQuantity');
|
||
qtyInput.value = roundQuantity(unit.min_quantity, 3);
|
||
qtyInput.min = unit.min_quantity;
|
||
qtyInput.step = unit.quantity_step;
|
||
|
||
// Устанавливаем цену
|
||
document.getElementById('unitModalPrice').value = unit.actual_price;
|
||
|
||
// Обновляем подсказку
|
||
const hintEl = document.getElementById('unitQtyHint');
|
||
hintEl.textContent = `Мин. ${unit.min_quantity}, шаг ${unit.quantity_step}`;
|
||
|
||
// Сбрасываем индикатор изменения цены
|
||
document.getElementById('priceOverrideIndicator').style.display = 'none';
|
||
|
||
// Пересчитываем итого
|
||
calculateUnitModalSubtotal();
|
||
|
||
// Валидируем количество
|
||
validateUnitQuantity();
|
||
}
|
||
|
||
/**
|
||
* Проверяет количество на соответствие ограничениям
|
||
* @returns {boolean} - true если валидно
|
||
*/
|
||
function validateUnitQuantity() {
|
||
if (!selectedSalesUnit) return false;
|
||
|
||
const qtyInput = document.getElementById('unitModalQuantity');
|
||
const qty = parseFloat(qtyInput.value);
|
||
const errorEl = document.getElementById('unitQtyError');
|
||
const confirmBtn = document.getElementById('confirmAddUnitToCart');
|
||
|
||
// Проверка минимального количества
|
||
if (qty < parseFloat(selectedSalesUnit.min_quantity)) {
|
||
errorEl.textContent = `Минимальное количество: ${selectedSalesUnit.min_quantity}`;
|
||
errorEl.style.display = 'block';
|
||
confirmBtn.disabled = true;
|
||
return false;
|
||
}
|
||
|
||
// Проверка шага (с учётом погрешности)
|
||
const step = parseFloat(selectedSalesUnit.quantity_step);
|
||
const minQty = parseFloat(selectedSalesUnit.min_quantity);
|
||
const diff = qty - minQty;
|
||
const remainder = diff % step;
|
||
const epsilon = 0.0001;
|
||
|
||
if (remainder > epsilon && (step - remainder) > epsilon) {
|
||
errorEl.textContent = `Количество должно быть кратно ${step}`;
|
||
errorEl.style.display = 'block';
|
||
confirmBtn.disabled = true;
|
||
return false;
|
||
}
|
||
|
||
// Всё ок, скрываем ошибку
|
||
errorEl.style.display = 'none';
|
||
confirmBtn.disabled = false;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Рассчитывает итоговую сумму
|
||
*/
|
||
function calculateUnitModalSubtotal() {
|
||
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value) || 0;
|
||
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
|
||
const price = parseFloat(document.getElementById('unitModalPrice').value) || 0;
|
||
// Округляем до 2 знаков после запятой для корректного отображения
|
||
const subtotal = Math.round(qty * price * 100) / 100;
|
||
|
||
document.getElementById('unitModalSubtotal').textContent = `${formatMoney(subtotal)} руб`;
|
||
|
||
// Проверяем изменение цены
|
||
if (selectedSalesUnit && Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01) {
|
||
document.getElementById('priceOverrideIndicator').style.display = 'block';
|
||
} else {
|
||
document.getElementById('priceOverrideIndicator').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Добавляет товар с выбранной единицей в корзину
|
||
*/
|
||
function addToCartFromModal() {
|
||
if (!validateUnitQuantity()) {
|
||
return;
|
||
}
|
||
|
||
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value);
|
||
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
|
||
const price = parseFloat(document.getElementById('unitModalPrice').value);
|
||
const priceOverridden = Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01;
|
||
|
||
// Формируем ключ корзины: product-{id}-{sales_unit_id}
|
||
const cartKey = `product-${unitModalProduct.id}-${selectedSalesUnit.id}`;
|
||
|
||
// Добавляем или обновляем в корзине
|
||
if (cart.has(cartKey)) {
|
||
const existing = cart.get(cartKey);
|
||
existing.qty = roundQuantity(existing.qty + qty, 3); // Округляем сумму
|
||
existing.price = price; // Обновляем цену
|
||
existing.quantity_step = parseFloat(selectedSalesUnit.quantity_step) || 1; // Обновляем шаг
|
||
existing.price_overridden = priceOverridden;
|
||
} else {
|
||
cart.set(cartKey, {
|
||
id: unitModalProduct.id,
|
||
name: unitModalProduct.name,
|
||
price: price,
|
||
qty: qty,
|
||
type: 'product',
|
||
sales_unit_id: selectedSalesUnit.id,
|
||
unit_name: selectedSalesUnit.name,
|
||
unit_short_name: selectedSalesUnit.unit_short_name,
|
||
quantity_step: parseFloat(selectedSalesUnit.quantity_step) || 1, // Сохраняем шаг количества
|
||
price_overridden: priceOverridden
|
||
});
|
||
}
|
||
|
||
// Обновляем корзину
|
||
renderCart();
|
||
saveCartToRedis();
|
||
|
||
// Перерисовываем товары для обновления визуального остатка
|
||
if (!isShowcaseView) {
|
||
renderProducts();
|
||
}
|
||
|
||
// Закрываем модальное окно
|
||
unitModalInstance.hide();
|
||
}
|
||
|
||
function renderCategories() {
|
||
const grid = document.getElementById('categoryGrid');
|
||
grid.innerHTML = '';
|
||
|
||
// Кнопка "Витрина" - первая в ряду
|
||
const showcaseCol = document.createElement('div');
|
||
showcaseCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||
const showcaseCard = document.createElement('div');
|
||
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
|
||
showcaseCard.style.backgroundColor = '#fff3cd';
|
||
showcaseCard.style.borderColor = '#ffc107';
|
||
showcaseCard.onclick = async () => {
|
||
isShowcaseView = true;
|
||
currentCategoryId = null;
|
||
await refreshShowcaseKits(); // Загружаем свежие данные
|
||
renderCategories();
|
||
renderProducts();
|
||
};
|
||
const showcaseBody = document.createElement('div');
|
||
showcaseBody.className = 'card-body';
|
||
const showcaseName = document.createElement('div');
|
||
showcaseName.className = 'category-name';
|
||
showcaseName.innerHTML = '<i class="bi bi-flower1"></i> <strong>ВИТРИНА</strong>';
|
||
showcaseBody.appendChild(showcaseName);
|
||
showcaseCard.appendChild(showcaseBody);
|
||
showcaseCol.appendChild(showcaseCard);
|
||
grid.appendChild(showcaseCol);
|
||
|
||
// Кнопка "Все"
|
||
const allCol = document.createElement('div');
|
||
allCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||
const allCard = document.createElement('div');
|
||
allCard.className = 'card category-card' + (currentCategoryId === null && !isShowcaseView ? ' active' : '');
|
||
allCard.onclick = async () => {
|
||
currentCategoryId = null;
|
||
isShowcaseView = false;
|
||
currentSearchQuery = ''; // Сбрасываем поиск
|
||
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
|
||
renderCategories();
|
||
await loadItems(); // Загрузка через API
|
||
};
|
||
const allBody = document.createElement('div');
|
||
allBody.className = 'card-body';
|
||
const allName = document.createElement('div');
|
||
allName.className = 'category-name';
|
||
allName.textContent = 'Все товары';
|
||
allBody.appendChild(allName);
|
||
allCard.appendChild(allBody);
|
||
allCol.appendChild(allCard);
|
||
grid.appendChild(allCol);
|
||
|
||
// Категории
|
||
CATEGORIES.forEach(cat => {
|
||
const col = document.createElement('div');
|
||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
|
||
card.onclick = async () => {
|
||
currentCategoryId = cat.id;
|
||
isShowcaseView = false;
|
||
currentSearchQuery = ''; // Сбрасываем поиск
|
||
document.getElementById('searchInput').value = ''; // Очищаем поле поиска
|
||
renderCategories();
|
||
await loadItems(); // Загрузка через API
|
||
};
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'card-body';
|
||
|
||
const name = document.createElement('div');
|
||
name.className = 'category-name';
|
||
name.textContent = cat.name;
|
||
|
||
body.appendChild(name);
|
||
card.appendChild(body);
|
||
col.appendChild(card);
|
||
grid.appendChild(col);
|
||
});
|
||
}
|
||
|
||
function renderProducts() {
|
||
const grid = document.getElementById('productGrid');
|
||
grid.innerHTML = '';
|
||
|
||
let filtered;
|
||
|
||
// Если выбран режим витрины - показываем витринные комплекты
|
||
if (isShowcaseView) {
|
||
filtered = showcaseKits;
|
||
|
||
// Для витрины — клиентская фильтрация по поиску
|
||
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
|
||
if (searchTerm) {
|
||
filtered = filtered.filter(item => {
|
||
const name = (item.name || '').toLowerCase();
|
||
const sku = (item.sku || '').toLowerCase();
|
||
return name.includes(searchTerm) || sku.includes(searchTerm);
|
||
});
|
||
}
|
||
} else {
|
||
// Обычный режим - ITEMS уже отфильтрованы на сервере (категория + поиск)
|
||
filtered = ITEMS;
|
||
}
|
||
|
||
filtered.forEach(item => {
|
||
const col = document.createElement('div');
|
||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'card product-card';
|
||
card.style.position = 'relative';
|
||
card.onclick = () => addToCart(item);
|
||
|
||
// Если это витринный комплект - добавляем кнопку редактирования
|
||
if (item.type === 'showcase_kit') {
|
||
// ИНДИКАЦИЯ БЛОКИРОВКИ
|
||
if (item.is_locked) {
|
||
// Создаем бейдж блокировки
|
||
const lockBadge = document.createElement('div');
|
||
lockBadge.style.position = 'absolute';
|
||
lockBadge.style.top = '5px';
|
||
lockBadge.style.left = '5px';
|
||
lockBadge.style.zIndex = '10';
|
||
|
||
if (item.locked_by_me) {
|
||
// Заблокирован мной - зеленый бейдж
|
||
lockBadge.className = 'badge bg-success';
|
||
lockBadge.innerHTML = '<i class="bi bi-cart-check"></i> В корзине';
|
||
lockBadge.title = 'Добавлен в вашу корзину';
|
||
} else {
|
||
// Заблокирован другим кассиром - красный бейдж + блокируем карточку
|
||
lockBadge.className = 'badge bg-danger';
|
||
lockBadge.innerHTML = '<i class="bi bi-lock-fill"></i> Занят';
|
||
lockBadge.title = `В корзине ${item.locked_by_user}`;
|
||
|
||
// Затемняем карточку и блокируем клики
|
||
card.style.opacity = '0.5';
|
||
card.style.cursor = 'not-allowed';
|
||
card.onclick = (e) => {
|
||
e.stopPropagation();
|
||
alert(`Этот букет уже в корзине кассира "${item.locked_by_user}".\nДождитесь освобождения блокировки.`);
|
||
};
|
||
}
|
||
|
||
card.appendChild(lockBadge);
|
||
}
|
||
|
||
// Кнопка редактирования (только если НЕ заблокирован другим)
|
||
if (!item.is_locked || item.locked_by_me) {
|
||
const editBtn = document.createElement('button');
|
||
editBtn.className = 'btn btn-sm btn-outline-primary';
|
||
editBtn.style.position = 'absolute';
|
||
editBtn.style.top = '5px';
|
||
editBtn.style.right = '5px';
|
||
editBtn.style.zIndex = '10';
|
||
editBtn.innerHTML = '<i class="bi bi-pencil"></i>';
|
||
editBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
openEditKitModal(item.id);
|
||
};
|
||
card.appendChild(editBtn);
|
||
}
|
||
}
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'card-body';
|
||
|
||
// Изображение товара/комплекта
|
||
const imageDiv = document.createElement('div');
|
||
imageDiv.className = 'product-image';
|
||
if (item.image) {
|
||
const img = document.createElement('img');
|
||
img.src = item.image;
|
||
img.alt = item.name;
|
||
img.loading = 'lazy'; // Lazy loading
|
||
imageDiv.appendChild(img);
|
||
} else {
|
||
imageDiv.innerHTML = '<i class="bi bi-image"></i>';
|
||
}
|
||
|
||
// Информация о товаре/комплекте
|
||
const info = document.createElement('div');
|
||
info.className = 'product-info';
|
||
|
||
const name = document.createElement('div');
|
||
name.className = 'product-name';
|
||
name.textContent = item.name;
|
||
|
||
const stock = document.createElement('div');
|
||
stock.className = 'product-stock';
|
||
|
||
// Для витринных комплектов показываем название витрины И количество (доступно/всего)
|
||
if (item.type === 'showcase_kit') {
|
||
const availableCount = item.available_count || 0;
|
||
const totalCount = item.total_count || availableCount;
|
||
const inCart = totalCount - availableCount;
|
||
|
||
// Показываем: доступно / всего (и сколько в корзине)
|
||
let badgeClass = availableCount > 0 ? 'bg-success' : 'bg-secondary';
|
||
let badgeText = totalCount > 1 ? `${availableCount}/${totalCount}` : `${availableCount}`;
|
||
let cartInfo = inCart > 0 ? ` <span class="badge bg-warning text-dark">🛒${inCart}</span>` : '';
|
||
|
||
stock.innerHTML = `🌺 ${item.showcase_name} <span class="badge ${badgeClass} ms-1">${badgeText}</span>${cartInfo}`;
|
||
stock.style.color = '#856404';
|
||
stock.style.fontWeight = 'bold';
|
||
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
|
||
// Для обычных товаров показываем остатки: FREE(-RESERVED-IN_CART)
|
||
// FREE = доступно для продажи (available - reserved - в корзине)
|
||
const available = parseFloat(item.available_qty) || 0;
|
||
const reserved = parseFloat(item.reserved_qty) || 0;
|
||
|
||
// Вычитаем количество из корзины для визуализации
|
||
const cartKey = `product-${item.id}`;
|
||
const inCart = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
|
||
|
||
const free = available - reserved - inCart;
|
||
const freeRounded = roundQuantity(free, 3); // Округляем для отображения
|
||
|
||
// Создаём элементы для стилизации разных размеров
|
||
const freeSpan = document.createElement('span');
|
||
freeSpan.textContent = freeRounded; // Используем округлённое значение
|
||
freeSpan.style.fontSize = '1.1em';
|
||
freeSpan.style.fontWeight = 'bold';
|
||
freeSpan.style.fontStyle = 'normal';
|
||
|
||
// Отображаем резерв и корзину если они есть
|
||
const suffixParts = [];
|
||
if (reserved > 0) {
|
||
suffixParts.push(`−${roundQuantity(reserved, 3)}`);
|
||
}
|
||
if (inCart > 0) {
|
||
suffixParts.push(`−${roundQuantity(inCart, 3)}🛒`);
|
||
}
|
||
|
||
if (suffixParts.length > 0) {
|
||
const suffixSpan = document.createElement('span');
|
||
suffixSpan.textContent = `(${suffixParts.join(' ')})`;
|
||
suffixSpan.style.fontSize = '0.85em';
|
||
suffixSpan.style.marginLeft = '3px';
|
||
suffixSpan.style.fontStyle = 'normal';
|
||
|
||
stock.appendChild(freeSpan);
|
||
stock.appendChild(suffixSpan);
|
||
} else {
|
||
stock.appendChild(freeSpan);
|
||
}
|
||
|
||
// Цветовая индикация: красный если свободных остатков нет или отрицательные
|
||
if (free <= 0) {
|
||
stock.style.color = '#dc3545'; // Красный
|
||
} else if (free < 5) {
|
||
stock.style.color = '#ffc107'; // Жёлтый (мало остатков)
|
||
} else {
|
||
stock.style.color = '#28a745'; // Зелёный (достаточно)
|
||
}
|
||
} else {
|
||
// Комплекты: показываем доступное количество
|
||
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';
|
||
priceSpan.textContent = `${formatMoney(item.price)}`;
|
||
|
||
sku.appendChild(skuText);
|
||
sku.appendChild(priceSpan);
|
||
|
||
info.appendChild(name);
|
||
info.appendChild(stock);
|
||
info.appendChild(sku);
|
||
|
||
body.appendChild(imageDiv);
|
||
body.appendChild(info);
|
||
card.appendChild(body);
|
||
col.appendChild(card);
|
||
grid.appendChild(col);
|
||
});
|
||
}
|
||
|
||
// Загрузка товаров через API
|
||
async function loadItems(append = false) {
|
||
if (isLoadingItems) return;
|
||
|
||
isLoadingItems = true;
|
||
|
||
if (!append) {
|
||
currentPage = 1;
|
||
ITEMS = [];
|
||
}
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: currentPage,
|
||
page_size: 60
|
||
});
|
||
|
||
if (currentCategoryId) {
|
||
params.append('category_id', currentCategoryId);
|
||
}
|
||
|
||
// Добавляем поисковый запрос, если есть
|
||
if (currentSearchQuery) {
|
||
params.append('query', currentSearchQuery);
|
||
}
|
||
|
||
const response = await fetch(`/pos/api/items/?${params}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
if (append) {
|
||
ITEMS = ITEMS.concat(data.items);
|
||
} else {
|
||
ITEMS = data.items;
|
||
}
|
||
|
||
hasMoreItems = data.has_more;
|
||
|
||
if (data.has_more) {
|
||
currentPage = data.next_page;
|
||
}
|
||
|
||
renderProducts();
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки товаров:', error);
|
||
} finally {
|
||
isLoadingItems = false;
|
||
}
|
||
}
|
||
|
||
// Infinite scroll
|
||
function setupInfiniteScroll() {
|
||
const grid = document.getElementById('productGrid');
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting && hasMoreItems && !isLoadingItems && !isShowcaseView) {
|
||
loadItems(true); // Догрузка
|
||
}
|
||
});
|
||
},
|
||
{
|
||
rootMargin: '200px'
|
||
}
|
||
);
|
||
|
||
// Наблюдаем за концом грида
|
||
const sentinel = document.createElement('div');
|
||
sentinel.id = 'scroll-sentinel';
|
||
sentinel.style.height = '1px';
|
||
grid.parentElement.appendChild(sentinel);
|
||
observer.observe(sentinel);
|
||
}
|
||
|
||
async function addToCart(item) {
|
||
// ПРОВЕРКА НА НАЛИЧИЕ НЕСКОЛЬКИХ ЕДИНИЦ ПРОДАЖИ
|
||
if (item.type === 'product' && item.sales_units_count > 1) {
|
||
// Открываем модальное окно выбора единицы
|
||
await openProductUnitModal(item);
|
||
return;
|
||
}
|
||
|
||
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
|
||
|
||
// СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
|
||
if (item.type === 'showcase_kit') {
|
||
// Пытаемся заблокировать 1 экземпляр через API
|
||
// API сам проверит доступность и вернёт ошибку если нет свободных
|
||
try {
|
||
const response = await fetch(`/pos/api/showcase-kits/${item.id}/add-to-cart/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken'),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ quantity: 1 })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok || !data.success) {
|
||
// Нет доступных экземпляров или другая ошибка
|
||
alert(data.error || 'Не удалось добавить букет в корзину');
|
||
// Обновляем витрину чтобы показать актуальное состояние
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Успешно заблокировали - добавляем/обновляем в корзине
|
||
const lockedItemIds = data.locked_item_ids || [];
|
||
|
||
if (cart.has(cartKey)) {
|
||
// Добавляем к существующим
|
||
const existing = cart.get(cartKey);
|
||
existing.qty += lockedItemIds.length;
|
||
existing.showcase_item_ids = [...(existing.showcase_item_ids || []), ...lockedItemIds];
|
||
} else {
|
||
// Создаём новую запись
|
||
cart.set(cartKey, {
|
||
id: item.id,
|
||
name: item.name,
|
||
price: Number(item.price),
|
||
qty: lockedItemIds.length,
|
||
type: item.type,
|
||
showcase_item_ids: lockedItemIds,
|
||
lock_expires_at: data.lock_expires_at
|
||
});
|
||
}
|
||
|
||
// Обновляем список витрины (чтобы показать актуальные available_count)
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при добавлении витринного комплекта:', error);
|
||
alert('Ошибка сервера. Попробуйте еще раз.');
|
||
return;
|
||
}
|
||
} else {
|
||
// ОБЫЧНАЯ ЛОГИКА для товаров и комплектов
|
||
if (!cart.has(cartKey)) {
|
||
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
|
||
} else {
|
||
const cartItem = cart.get(cartKey);
|
||
cartItem.qty = roundQuantity(cartItem.qty + 1, 3);
|
||
}
|
||
}
|
||
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем в Redis
|
||
|
||
// Перерисовываем товары для обновления визуального остатка
|
||
if (!isShowcaseView && item.type === 'product') {
|
||
renderProducts();
|
||
}
|
||
|
||
// Автоматический фокус на поле количества (только для обычных товаров)
|
||
if (item.type !== 'showcase_kit') {
|
||
setTimeout(() => {
|
||
const qtyInputs = document.querySelectorAll('.qty-input');
|
||
const itemIndex = Array.from(cart.keys()).indexOf(cartKey);
|
||
|
||
if (itemIndex !== -1 && qtyInputs[itemIndex]) {
|
||
qtyInputs[itemIndex].focus();
|
||
qtyInputs[itemIndex].select(); // Выделяем весь текст
|
||
}
|
||
}, 50);
|
||
}
|
||
}
|
||
|
||
// Вспомогательная функция для обновления количества товара в корзине
|
||
async function updateCartItemQty(cartKey, newQty) {
|
||
const item = cart.get(cartKey);
|
||
if (!item) return;
|
||
|
||
// Округляем новое количество
|
||
const roundedQty = roundQuantity(newQty, 3);
|
||
|
||
if (roundedQty <= 0) {
|
||
await removeFromCart(cartKey);
|
||
} else {
|
||
item.qty = roundedQty;
|
||
renderCart();
|
||
saveCartToRedis();
|
||
|
||
// Перерисовываем товары для обновления визуального остатка
|
||
if (!isShowcaseView && item.type === 'product') {
|
||
renderProducts();
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderCart() {
|
||
const list = document.getElementById('cartList');
|
||
list.innerHTML = '';
|
||
let total = 0;
|
||
|
||
if (cart.size === 0) {
|
||
list.innerHTML = '<p class="text-muted text-center py-4 small">Корзина пуста</p>';
|
||
document.getElementById('cartTotal').textContent = '0.00';
|
||
updateShowcaseButtonState(); // Обновляем состояние кнопки
|
||
return;
|
||
}
|
||
|
||
cart.forEach((item, cartKey) => {
|
||
const row = document.createElement('div');
|
||
row.className = 'cart-item mb-2';
|
||
|
||
// СПЕЦИАЛЬНАЯ СТИЛИЗАЦИЯ для витринных комплектов
|
||
const isShowcaseKit = item.type === 'showcase_kit';
|
||
if (isShowcaseKit) {
|
||
row.style.backgroundColor = '#fff3cd'; // Желтый фон
|
||
row.style.border = '1px solid #ffc107';
|
||
row.style.borderRadius = '4px';
|
||
row.style.padding = '8px';
|
||
}
|
||
|
||
// Левая часть: Название и цена единицы
|
||
const namePrice = document.createElement('div');
|
||
namePrice.className = 'item-name-price';
|
||
|
||
// Иконка только для комплектов
|
||
let typeIcon = '';
|
||
if (item.type === 'kit' || item.type === 'showcase_kit') {
|
||
typeIcon = '<i class="bi bi-box-seam text-info" title="Комплект"></i> ';
|
||
}
|
||
|
||
// Единица продажи (если есть)
|
||
let unitInfo = '';
|
||
if (item.sales_unit_id && item.unit_name) {
|
||
unitInfo = ` <span class="badge bg-secondary" style="font-size: 0.7rem;">${item.unit_name}</span>`;
|
||
}
|
||
|
||
namePrice.innerHTML = `
|
||
<div class="fw-semibold small">${typeIcon}${item.name}${unitInfo}</div>
|
||
<div class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)} / ${item.unit_short_name || 'шт'}</div>
|
||
`;
|
||
|
||
// Знак умножения
|
||
const multiplySign = document.createElement('span');
|
||
multiplySign.className = 'multiply-sign';
|
||
multiplySign.textContent = 'x';
|
||
|
||
// Контейнер для кнопок количества
|
||
const qtyControl = document.createElement('div');
|
||
qtyControl.className = 'd-flex align-items-center';
|
||
qtyControl.style.gap = '2px';
|
||
|
||
// СПЕЦИАЛЬНАЯ ЛОГИКА для витринных комплектов
|
||
if (isShowcaseKit) {
|
||
// Кнопка минус
|
||
const minusBtn = document.createElement('button');
|
||
minusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||
minusBtn.innerHTML = '<i class="bi bi-dash-circle" style="font-size: 1.2em;"></i>';
|
||
minusBtn.onclick = async (e) => {
|
||
e.preventDefault();
|
||
await decreaseShowcaseKitQty(cartKey);
|
||
};
|
||
|
||
// Поле количества (только для отображения, readonly)
|
||
const qtyInput = document.createElement('input');
|
||
qtyInput.type = 'number';
|
||
qtyInput.className = 'qty-input form-control form-control-sm';
|
||
qtyInput.style.width = '60px';
|
||
qtyInput.style.textAlign = 'center';
|
||
qtyInput.style.padding = '0.375rem 0.25rem';
|
||
qtyInput.value = roundQuantity(item.qty, 3);
|
||
qtyInput.min = 1;
|
||
qtyInput.readOnly = true; // Только чтение - изменяем только через +/-
|
||
qtyInput.style.backgroundColor = '#fff3cd'; // Желтый фон как у витринных
|
||
|
||
// Кнопка плюс
|
||
const plusBtn = document.createElement('button');
|
||
plusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||
plusBtn.innerHTML = '<i class="bi bi-plus-circle" style="font-size: 1.2em;"></i>';
|
||
plusBtn.onclick = async (e) => {
|
||
e.preventDefault();
|
||
await increaseShowcaseKitQty(cartKey);
|
||
};
|
||
|
||
// Собираем контейнер
|
||
qtyControl.appendChild(minusBtn);
|
||
qtyControl.appendChild(qtyInput);
|
||
qtyControl.appendChild(plusBtn);
|
||
} else {
|
||
// ОБЫЧНАЯ ЛОГИКА для товаров и комплектов
|
||
|
||
// Кнопка минус
|
||
const minusBtn = document.createElement('button');
|
||
minusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||
minusBtn.innerHTML = '<i class="bi bi-dash-circle" style="font-size: 1.2em;"></i>';
|
||
minusBtn.onclick = async (e) => {
|
||
e.preventDefault();
|
||
const currentQty = cart.get(cartKey).qty;
|
||
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
|
||
await updateCartItemQty(cartKey, roundQuantity(currentQty - step, 3));
|
||
};
|
||
|
||
// Поле ввода количества
|
||
const qtyInput = document.createElement('input');
|
||
qtyInput.type = 'number';
|
||
qtyInput.className = 'qty-input form-control form-control-sm';
|
||
qtyInput.style.width = '60px';
|
||
qtyInput.style.textAlign = 'center';
|
||
qtyInput.style.padding = '0.375rem 0.25rem';
|
||
qtyInput.value = roundQuantity(item.qty, 3);
|
||
qtyInput.min = 1;
|
||
qtyInput.step = item.quantity_step || 0.001; // Устанавливаем шаг единицы продажи
|
||
qtyInput.onchange = async (e) => {
|
||
const newQty = parseFloat(e.target.value) || 1;
|
||
await updateCartItemQty(cartKey, newQty);
|
||
};
|
||
// Округление при потере фокуса
|
||
qtyInput.onblur = (e) => {
|
||
const rawValue = parseFloat(e.target.value) || 1;
|
||
e.target.value = roundQuantity(rawValue, 3);
|
||
};
|
||
|
||
// Кнопка плюс
|
||
const plusBtn = document.createElement('button');
|
||
plusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||
plusBtn.innerHTML = '<i class="bi bi-plus-circle" style="font-size: 1.2em;"></i>';
|
||
plusBtn.onclick = async (e) => {
|
||
e.preventDefault();
|
||
const currentQty = cart.get(cartKey).qty;
|
||
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
|
||
await updateCartItemQty(cartKey, roundQuantity(currentQty + step, 3));
|
||
};
|
||
|
||
// Собираем контейнер
|
||
qtyControl.appendChild(minusBtn);
|
||
qtyControl.appendChild(qtyInput);
|
||
qtyControl.appendChild(plusBtn);
|
||
}
|
||
|
||
// Сумма за позицию
|
||
const itemTotal = document.createElement('div');
|
||
itemTotal.className = 'item-total';
|
||
itemTotal.textContent = formatMoney(item.price * item.qty);
|
||
|
||
// Кнопка удаления
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'btn btn-sm btn-link text-danger p-0';
|
||
deleteBtn.innerHTML = '<i class="bi bi-x"></i>';
|
||
deleteBtn.onclick = () => removeFromCart(cartKey);
|
||
|
||
row.appendChild(namePrice);
|
||
row.appendChild(multiplySign);
|
||
row.appendChild(qtyControl);
|
||
row.appendChild(itemTotal);
|
||
row.appendChild(deleteBtn);
|
||
|
||
list.appendChild(row);
|
||
|
||
total += item.qty * item.price;
|
||
});
|
||
|
||
document.getElementById('cartTotal').textContent = formatMoney(total);
|
||
|
||
// Обновляем состояние кнопки "НА ВИТРИНУ"
|
||
updateShowcaseButtonState();
|
||
}
|
||
|
||
async function removeFromCart(cartKey) {
|
||
const item = cart.get(cartKey);
|
||
|
||
// СПЕЦИАЛЬНАЯ ЛОГИКА для витринных комплектов - снимаем блокировку
|
||
if (item && item.type === 'showcase_kit') {
|
||
try {
|
||
// Передаём конкретные showcase_item_ids для снятия блокировки
|
||
const body = {};
|
||
if (item.showcase_item_ids && item.showcase_item_ids.length > 0) {
|
||
body.showcase_item_ids = item.showcase_item_ids;
|
||
}
|
||
|
||
const response = await fetch(`/pos/api/showcase-kits/${item.id}/remove-from-cart/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken'),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
console.error('Ошибка при снятии блокировки:', data.error);
|
||
// Продолжаем удаление из корзины даже при ошибке
|
||
}
|
||
|
||
// Обновляем список витрины (чтобы убрать индикацию блокировки)
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при снятии блокировки витринного комплекта:', error);
|
||
// Продолжаем удаление из корзины
|
||
}
|
||
}
|
||
|
||
cart.delete(cartKey);
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем в Redis
|
||
|
||
// Перерисовываем товары для обновления визуального остатка
|
||
if (!isShowcaseView && item && item.type === 'product') {
|
||
renderProducts();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Увеличивает количество витринного комплекта в корзине
|
||
* Добавляет еще один экземпляр через API (если есть доступные)
|
||
*/
|
||
async function increaseShowcaseKitQty(cartKey) {
|
||
const item = cart.get(cartKey);
|
||
if (!item || item.type !== 'showcase_kit') return;
|
||
|
||
try {
|
||
// Пытаемся заблокировать еще 1 экземпляр
|
||
const response = await fetch(`/pos/api/showcase-kits/${item.id}/add-to-cart/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken'),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ quantity: 1 })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok || !data.success) {
|
||
// Нет доступных экземпляров
|
||
alert(data.error || 'Нет доступных экземпляров этого букета на витрине');
|
||
return;
|
||
}
|
||
|
||
// Успешно заблокировали - обновляем корзину
|
||
const lockedItemIds = data.locked_item_ids || [];
|
||
item.qty += lockedItemIds.length;
|
||
item.showcase_item_ids = [...(item.showcase_item_ids || []), ...lockedItemIds];
|
||
|
||
renderCart();
|
||
saveCartToRedis();
|
||
|
||
// Обновляем список витрины
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при увеличении количества витринного комплекта:', error);
|
||
alert('Ошибка сервера. Попробуйте еще раз.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Уменьшает количество витринного комплекта в корзине
|
||
* Снимает блокировку с одного экземпляра (последнего в списке)
|
||
*/
|
||
async function decreaseShowcaseKitQty(cartKey) {
|
||
const item = cart.get(cartKey);
|
||
if (!item || item.type !== 'showcase_kit') return;
|
||
|
||
// Если количество = 1, удаляем полностью
|
||
if (item.qty <= 1) {
|
||
await removeFromCart(cartKey);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Снимаем блокировку с последнего экземпляра
|
||
const showcaseItemIds = item.showcase_item_ids || [];
|
||
if (showcaseItemIds.length === 0) {
|
||
// Нет ID - просто удаляем
|
||
await removeFromCart(cartKey);
|
||
return;
|
||
}
|
||
|
||
// Берем последний ID из списка
|
||
const itemIdToRelease = showcaseItemIds[showcaseItemIds.length - 1];
|
||
|
||
const response = await fetch(`/pos/api/showcase-kits/${item.id}/remove-from-cart/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken'),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ showcase_item_ids: [itemIdToRelease] })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
console.error('Ошибка при снятии блокировки:', data.error);
|
||
}
|
||
|
||
// Обновляем корзину
|
||
item.qty -= 1;
|
||
item.showcase_item_ids = showcaseItemIds.filter(id => id !== itemIdToRelease);
|
||
|
||
renderCart();
|
||
saveCartToRedis();
|
||
|
||
// Обновляем список витрины
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при уменьшении количества витринного комплекта:', error);
|
||
alert('Ошибка сервера. Попробуйте еще раз.');
|
||
}
|
||
}
|
||
|
||
async function clearCart() {
|
||
// Сбрасываем все свои блокировки витринных букетов
|
||
try {
|
||
await fetch('/pos/api/showcase-kits/release-all-my-locks/', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||
});
|
||
} catch (e) {
|
||
console.error('Ошибка сброса блокировок:', e);
|
||
}
|
||
|
||
// Очищаем корзину
|
||
cart.clear();
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем пустую корзину в Redis
|
||
|
||
// Обновляем отображение товаров/витрины чтобы показать актуальные остатки
|
||
if (isShowcaseView) {
|
||
await loadShowcaseKits();
|
||
} else {
|
||
renderProducts(); // Перерисовать карточки товаров с актуальными остатками
|
||
}
|
||
}
|
||
|
||
document.getElementById('clearCart').onclick = clearCart;
|
||
|
||
/**
|
||
* Обновляет состояние кнопки "НА ВИТРИНУ"
|
||
* Блокирует кнопку если в корзине есть витринный комплект
|
||
*/
|
||
function updateShowcaseButtonState() {
|
||
const showcaseBtn = document.getElementById('addToShowcaseBtn');
|
||
if (!showcaseBtn) return;
|
||
|
||
// Проверяем наличие витринных комплектов в корзине
|
||
let hasShowcaseKit = false;
|
||
for (const [cartKey, item] of cart) {
|
||
if (item.type === 'showcase_kit') {
|
||
hasShowcaseKit = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (hasShowcaseKit) {
|
||
// Блокируем кнопку
|
||
showcaseBtn.disabled = true;
|
||
showcaseBtn.classList.add('disabled');
|
||
showcaseBtn.style.opacity = '0.5';
|
||
showcaseBtn.style.cursor = 'not-allowed';
|
||
showcaseBtn.title = '⚠️ В корзине уже есть витринный комплект. Удалите его перед созданием нового';
|
||
} else {
|
||
// Разблокируем кнопку
|
||
showcaseBtn.disabled = false;
|
||
showcaseBtn.classList.remove('disabled');
|
||
showcaseBtn.style.opacity = '1';
|
||
showcaseBtn.style.cursor = 'pointer';
|
||
showcaseBtn.title = 'Создать букет на витрину из текущей корзины';
|
||
}
|
||
}
|
||
|
||
// Кнопка "На витрину" - функционал будет добавлен позже
|
||
document.getElementById('addToShowcaseBtn').onclick = () => {
|
||
openCreateTempKitModal();
|
||
};
|
||
|
||
// Функция открытия модального окна для создания временного комплекта
|
||
async function openCreateTempKitModal() {
|
||
// Проверяем что корзина не пуста
|
||
if (cart.size === 0) {
|
||
alert('Корзина пуста. Добавьте товары перед созданием комплекта.');
|
||
return;
|
||
}
|
||
|
||
// Проверяем что в корзине НЕТ витринных комплектов
|
||
let hasShowcaseKit = false;
|
||
for (const [cartKey, item] of cart) {
|
||
if (item.type === 'showcase_kit') {
|
||
hasShowcaseKit = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (hasShowcaseKit) {
|
||
alert('⚠️ В корзине уже есть витринный комплект!\n\nНельзя создать новый букет на витрину, пока в корзине находится другой витринный букет.\n\nУдалите витринный букет из корзины или завершите текущую продажу.');
|
||
return;
|
||
}
|
||
|
||
// Проверяем что в корзине только товары (не обычные комплекты)
|
||
let hasKits = false;
|
||
for (const [cartKey, item] of cart) {
|
||
if (item.type === 'kit') {
|
||
hasKits = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (hasKits) {
|
||
alert('В корзине есть комплекты. Для витрины добавляйте только отдельные товары.');
|
||
return;
|
||
}
|
||
|
||
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
|
||
tempCart.clear();
|
||
cart.forEach((item, key) => {
|
||
tempCart.set(key, {...item}); // Глубокая копия объекта
|
||
});
|
||
|
||
// Генерируем название по умолчанию
|
||
const now = new Date();
|
||
const defaultName = `Витрина — ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})}`;
|
||
document.getElementById('tempKitName').value = defaultName;
|
||
|
||
// Загружаем список витрин
|
||
await loadShowcases();
|
||
|
||
// Заполняем список товаров из tempCart
|
||
renderTempKitItems();
|
||
|
||
// Открываем модальное окно
|
||
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// Открытие модального окна для редактирования комплекта
|
||
async function openEditKitModal(kitId) {
|
||
try {
|
||
// Загружаем данные комплекта
|
||
const response = await fetch(`/pos/api/product-kits/${kitId}/`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
alert(`Ошибка: ${data.error}`);
|
||
return;
|
||
}
|
||
|
||
const kit = data.kit;
|
||
|
||
// Устанавливаем режим редактирования
|
||
isEditMode = true;
|
||
editingKitId = kitId;
|
||
|
||
// Загружаем список витрин
|
||
await loadShowcases();
|
||
|
||
// Очищаем tempCart и заполняем составом комплекта
|
||
tempCart.clear();
|
||
kit.items.forEach(item => {
|
||
const cartKey = `product-${item.product_id}`;
|
||
tempCart.set(cartKey, {
|
||
id: item.product_id,
|
||
name: item.name,
|
||
price: Number(item.price),
|
||
qty: Number(item.qty),
|
||
type: 'product'
|
||
});
|
||
});
|
||
renderTempKitItems(); // Отображаем товары в модальном окне
|
||
|
||
// Заполняем поля формы
|
||
document.getElementById('tempKitName').value = kit.name;
|
||
document.getElementById('tempKitDescription').value = kit.description;
|
||
document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type;
|
||
document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value;
|
||
|
||
if (kit.sale_price) {
|
||
document.getElementById('useSalePrice').checked = true;
|
||
document.getElementById('salePrice').value = kit.sale_price;
|
||
document.getElementById('salePriceBlock').style.display = 'block';
|
||
} else {
|
||
document.getElementById('useSalePrice').checked = false;
|
||
document.getElementById('salePrice').value = '';
|
||
document.getElementById('salePriceBlock').style.display = 'none';
|
||
}
|
||
|
||
// Выбираем витрину
|
||
if (kit.showcase_id) {
|
||
document.getElementById('showcaseSelect').value = kit.showcase_id;
|
||
}
|
||
|
||
// Отображаем фото, если есть
|
||
if (kit.photo_url) {
|
||
document.getElementById('photoPreviewImg').src = kit.photo_url;
|
||
document.getElementById('photoPreview').style.display = 'block';
|
||
} else {
|
||
document.getElementById('photoPreview').style.display = 'none';
|
||
}
|
||
|
||
// Обновляем цены
|
||
updatePriceCalculations();
|
||
|
||
// Меняем заголовок и кнопку
|
||
document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета';
|
||
document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения';
|
||
|
||
// Показываем кнопку "Разобрать" и блок добавления товаров
|
||
document.getElementById('disassembleKitBtn').style.display = 'block';
|
||
document.getElementById('showcaseKitQuantityBlock').style.display = 'none';
|
||
document.getElementById('addProductBlock').style.display = 'block';
|
||
|
||
// Инициализируем компонент поиска товаров
|
||
setTimeout(() => {
|
||
if (window.ProductSearchPicker) {
|
||
const picker = ProductSearchPicker.init('#temp-kit-product-picker', {
|
||
onAddSelected: function(product, instance) {
|
||
if (product) {
|
||
// Добавляем товар в tempCart
|
||
const cartKey = `product-${product.id}`;
|
||
if (tempCart.has(cartKey)) {
|
||
// Увеличиваем количество
|
||
const existing = tempCart.get(cartKey);
|
||
existing.qty += 1;
|
||
} else {
|
||
// Добавляем новый товар
|
||
tempCart.set(cartKey, {
|
||
id: product.id,
|
||
name: product.text || product.name,
|
||
price: Number(product.price || 0),
|
||
qty: 1,
|
||
type: 'product'
|
||
});
|
||
}
|
||
|
||
// Обновляем отображение
|
||
renderTempKitItems();
|
||
|
||
// Очищаем выбор в пикере
|
||
instance.clearSelection();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}, 100);
|
||
|
||
// Открываем модальное окно
|
||
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
||
modal.show();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading kit for edit:', error);
|
||
alert('Ошибка при загрузке комплекта');
|
||
}
|
||
}
|
||
|
||
// Обновление списка витринных комплектов
|
||
async function loadShowcaseKits() {
|
||
try {
|
||
const response = await fetch('/pos/api/showcase-kits/');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showcaseKits = data.items;
|
||
// Перерисовываем грид если мы в режиме витрины
|
||
if (isShowcaseView) {
|
||
renderProducts();
|
||
}
|
||
} else {
|
||
console.error('Failed to refresh showcase kits:', data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error refreshing showcase kits:', error);
|
||
}
|
||
}
|
||
|
||
// Алиас для совместимости
|
||
const refreshShowcaseKits = loadShowcaseKits;
|
||
|
||
// Загрузка списка витрин
|
||
async function loadShowcases() {
|
||
try {
|
||
const response = await fetch('/pos/api/get-showcases/');
|
||
const data = await response.json();
|
||
|
||
const select = document.getElementById('showcaseSelect');
|
||
select.innerHTML = '<option value="">Выберите витрину...</option>';
|
||
|
||
if (data.success && data.showcases.length > 0) {
|
||
let defaultShowcaseId = null;
|
||
|
||
data.showcases.forEach(showcase => {
|
||
const option = document.createElement('option');
|
||
option.value = showcase.id;
|
||
option.textContent = `${showcase.name} (${showcase.warehouse_name})`;
|
||
select.appendChild(option);
|
||
|
||
// Запоминаем витрину по умолчанию
|
||
if (showcase.is_default) {
|
||
defaultShowcaseId = showcase.id;
|
||
}
|
||
});
|
||
|
||
// Автовыбор витрины по умолчанию
|
||
if (defaultShowcaseId) {
|
||
select.value = defaultShowcaseId;
|
||
}
|
||
} else {
|
||
select.innerHTML = '<option value="">Нет доступных витрин</option>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading showcases:', error);
|
||
alert('Ошибка загрузки витрин');
|
||
}
|
||
}
|
||
|
||
// Отображение товаров из tempCart в модальном окне
|
||
function renderTempKitItems() {
|
||
const container = document.getElementById('tempKitItemsList');
|
||
container.innerHTML = '';
|
||
|
||
let estimatedTotal = 0;
|
||
|
||
tempCart.forEach((item, cartKey) => {
|
||
// Только товары (не комплекты)
|
||
if (item.type !== 'product') return;
|
||
|
||
const itemDiv = document.createElement('div');
|
||
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
|
||
|
||
// Левая часть: название и цена
|
||
const leftDiv = document.createElement('div');
|
||
leftDiv.className = 'flex-grow-1';
|
||
leftDiv.innerHTML = `
|
||
<strong class="small">${item.name}</strong>
|
||
<br>
|
||
<small class="text-muted">${formatMoney(item.price)} руб. / шт.</small>
|
||
`;
|
||
|
||
// Правая часть: контролы количества и удаление
|
||
const rightDiv = document.createElement('div');
|
||
rightDiv.className = 'd-flex align-items-center gap-2';
|
||
|
||
// Кнопка минус
|
||
const minusBtn = document.createElement('button');
|
||
minusBtn.className = 'btn btn-sm btn-outline-secondary';
|
||
minusBtn.innerHTML = '<i class="bi bi-dash"></i>';
|
||
minusBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
if (item.qty > 1) {
|
||
item.qty--;
|
||
} else {
|
||
tempCart.delete(cartKey);
|
||
}
|
||
renderTempKitItems();
|
||
};
|
||
|
||
// Поле количества
|
||
const qtyInput = document.createElement('input');
|
||
qtyInput.type = 'number';
|
||
qtyInput.className = 'form-control form-control-sm text-center';
|
||
qtyInput.style.width = '60px';
|
||
qtyInput.value = item.qty;
|
||
qtyInput.min = 1;
|
||
qtyInput.onchange = (e) => {
|
||
const newQty = parseInt(e.target.value) || 1;
|
||
item.qty = Math.max(1, newQty);
|
||
renderTempKitItems();
|
||
};
|
||
|
||
// Кнопка плюс
|
||
const plusBtn = document.createElement('button');
|
||
plusBtn.className = 'btn btn-sm btn-outline-secondary';
|
||
plusBtn.innerHTML = '<i class="bi bi-plus"></i>';
|
||
plusBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
item.qty++;
|
||
renderTempKitItems();
|
||
};
|
||
|
||
// Сумма за товар
|
||
const totalDiv = document.createElement('div');
|
||
totalDiv.className = 'text-end ms-2';
|
||
totalDiv.style.minWidth = '80px';
|
||
totalDiv.innerHTML = `<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>`;
|
||
|
||
// Кнопка удаления
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'btn btn-sm btn-outline-danger';
|
||
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
|
||
deleteBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
tempCart.delete(cartKey);
|
||
renderTempKitItems();
|
||
};
|
||
|
||
rightDiv.appendChild(minusBtn);
|
||
rightDiv.appendChild(qtyInput);
|
||
rightDiv.appendChild(plusBtn);
|
||
rightDiv.appendChild(totalDiv);
|
||
rightDiv.appendChild(deleteBtn);
|
||
|
||
itemDiv.appendChild(leftDiv);
|
||
itemDiv.appendChild(rightDiv);
|
||
container.appendChild(itemDiv);
|
||
|
||
estimatedTotal += item.qty * item.price;
|
||
});
|
||
|
||
// Если корзина пуста
|
||
if (tempCart.size === 0) {
|
||
container.innerHTML = '<p class="text-muted text-center py-3"><i class="bi bi-inbox"></i> Нет товаров</p>';
|
||
}
|
||
|
||
// Обновляем все расчеты цен
|
||
updatePriceCalculations(estimatedTotal);
|
||
}
|
||
|
||
// Расчет и обновление всех цен
|
||
function updatePriceCalculations(basePrice = null) {
|
||
// Если basePrice не передан, пересчитываем из tempCart
|
||
if (basePrice === null) {
|
||
basePrice = 0;
|
||
tempCart.forEach((item, cartKey) => {
|
||
if (item.type === 'product') {
|
||
basePrice += item.qty * item.price;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Базовая цена
|
||
document.getElementById('tempKitBasePrice').textContent = formatMoney(basePrice) + ' руб.';
|
||
|
||
// Корректировка
|
||
const adjustmentType = document.getElementById('priceAdjustmentType').value;
|
||
const adjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
|
||
|
||
let calculatedPrice = basePrice;
|
||
if (adjustmentType !== 'none' && adjustmentValue > 0) {
|
||
if (adjustmentType === 'increase_percent') {
|
||
calculatedPrice = basePrice + (basePrice * adjustmentValue / 100);
|
||
} else if (adjustmentType === 'increase_amount') {
|
||
calculatedPrice = basePrice + adjustmentValue;
|
||
} else if (adjustmentType === 'decrease_percent') {
|
||
calculatedPrice = Math.max(0, basePrice - (basePrice * adjustmentValue / 100));
|
||
} else if (adjustmentType === 'decrease_amount') {
|
||
calculatedPrice = Math.max(0, basePrice - adjustmentValue);
|
||
}
|
||
}
|
||
|
||
document.getElementById('tempKitCalculatedPrice').textContent = formatMoney(calculatedPrice) + ' руб.';
|
||
|
||
// Финальная цена (с учетом sale_price если задана)
|
||
const useSalePrice = document.getElementById('useSalePrice').checked;
|
||
const salePrice = parseFloat(document.getElementById('salePrice').value) || 0;
|
||
|
||
let finalPrice = calculatedPrice;
|
||
if (useSalePrice && salePrice > 0) {
|
||
finalPrice = salePrice;
|
||
}
|
||
|
||
document.getElementById('tempKitFinalPrice').textContent = formatMoney(finalPrice);
|
||
}
|
||
|
||
// Обработчики для полей цены
|
||
document.getElementById('priceAdjustmentType').addEventListener('change', function() {
|
||
const adjustmentBlock = document.getElementById('adjustmentValueBlock');
|
||
if (this.value === 'none') {
|
||
adjustmentBlock.style.display = 'none';
|
||
document.getElementById('priceAdjustmentValue').value = '0';
|
||
} else {
|
||
adjustmentBlock.style.display = 'block';
|
||
}
|
||
updatePriceCalculations();
|
||
});
|
||
|
||
document.getElementById('priceAdjustmentValue').addEventListener('input', function() {
|
||
updatePriceCalculations();
|
||
});
|
||
|
||
document.getElementById('useSalePrice').addEventListener('change', function() {
|
||
const salePriceBlock = document.getElementById('salePriceBlock');
|
||
if (this.checked) {
|
||
salePriceBlock.style.display = 'block';
|
||
} else {
|
||
salePriceBlock.style.display = 'none';
|
||
document.getElementById('salePrice').value = '';
|
||
}
|
||
updatePriceCalculations();
|
||
});
|
||
|
||
document.getElementById('salePrice').addEventListener('input', function() {
|
||
updatePriceCalculations();
|
||
});
|
||
|
||
// Обработчик загрузки фото
|
||
document.getElementById('tempKitPhoto').addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('Пожалуйста, выберите файл изображения');
|
||
this.value = '';
|
||
return;
|
||
}
|
||
|
||
// Превью
|
||
const reader = new FileReader();
|
||
reader.onload = function(event) {
|
||
document.getElementById('photoPreviewImg').src = event.target.result;
|
||
document.getElementById('photoPreview').style.display = 'block';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
|
||
// Удаление фото
|
||
document.getElementById('removePhoto').addEventListener('click', function() {
|
||
document.getElementById('tempKitPhoto').value = '';
|
||
document.getElementById('photoPreview').style.display = 'none';
|
||
document.getElementById('photoPreviewImg').src = '';
|
||
});
|
||
|
||
// Подтверждение создания/редактирования временного комплекта
|
||
document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||
const kitName = document.getElementById('tempKitName').value.trim();
|
||
const showcaseId = document.getElementById('showcaseSelect').value;
|
||
const description = document.getElementById('tempKitDescription').value.trim();
|
||
const photoFile = document.getElementById('tempKitPhoto').files[0];
|
||
|
||
// Валидация
|
||
if (!kitName) {
|
||
alert('Введите название комплекта');
|
||
return;
|
||
}
|
||
|
||
if (!showcaseId && !isEditMode) {
|
||
alert('Выберите витрину');
|
||
return;
|
||
}
|
||
|
||
// Собираем товары из tempCart (изолированное состояние модалки)
|
||
const items = [];
|
||
tempCart.forEach((item, cartKey) => {
|
||
if (item.type === 'product') {
|
||
items.push({
|
||
product_id: item.id,
|
||
quantity: item.qty
|
||
});
|
||
}
|
||
});
|
||
|
||
if (items.length === 0) {
|
||
alert('Нет товаров для создания комплекта');
|
||
return;
|
||
}
|
||
|
||
// Получаем данные о ценах
|
||
const priceAdjustmentType = document.getElementById('priceAdjustmentType').value;
|
||
const priceAdjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
|
||
const useSalePrice = document.getElementById('useSalePrice').checked;
|
||
const salePrice = useSalePrice ? (parseFloat(document.getElementById('salePrice').value) || 0) : 0;
|
||
|
||
// Получаем количество букетов для создания
|
||
const showcaseKitQuantity = parseInt(document.getElementById('showcaseKitQuantity').value, 10) || 1;
|
||
|
||
// Формируем FormData для отправки с файлом
|
||
const formData = new FormData();
|
||
formData.append('kit_name', kitName);
|
||
if (showcaseId) {
|
||
formData.append('showcase_id', showcaseId);
|
||
formData.append('quantity', showcaseKitQuantity); // Количество экземпляров на витрину
|
||
}
|
||
formData.append('description', description);
|
||
formData.append('items', JSON.stringify(items));
|
||
formData.append('price_adjustment_type', priceAdjustmentType);
|
||
formData.append('price_adjustment_value', priceAdjustmentValue);
|
||
if (useSalePrice && salePrice > 0) {
|
||
formData.append('sale_price', salePrice);
|
||
}
|
||
|
||
// Фото: для редактирования проверяем, удалено ли оно
|
||
if (photoFile) {
|
||
formData.append('photo', photoFile);
|
||
} else if (isEditMode && document.getElementById('photoPreview').style.display === 'none') {
|
||
// Если фото было удалено
|
||
formData.append('remove_photo', '1');
|
||
}
|
||
|
||
// Отправляем запрос на сервер
|
||
const confirmBtn = document.getElementById('confirmCreateTempKit');
|
||
confirmBtn.disabled = true;
|
||
|
||
const url = isEditMode
|
||
? `/pos/api/product-kits/${editingKitId}/update/`
|
||
: '/pos/api/create-temp-kit/';
|
||
|
||
const actionText = isEditMode ? 'Сохранение...' : 'Создание...';
|
||
confirmBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${actionText}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken')
|
||
// Не указываем Content-Type - браузер сам установит multipart/form-data
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Успех!
|
||
const createdCount = data.available_count || 1;
|
||
const qtyInfo = createdCount > 1 ? `\nСоздано экземпляров: ${createdCount}` : '';
|
||
|
||
let successMessage = isEditMode
|
||
? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
|
||
: `✅ ${data.message}
|
||
|
||
Комплект: ${data.kit_name}
|
||
Цена: ${data.kit_price} руб.${qtyInfo}
|
||
Зарезервировано компонентов: ${data.reservations_count}`;
|
||
|
||
// Если есть предупреждение о нехватке товара - добавляем его
|
||
if (data.warnings && data.warnings.length > 0) {
|
||
successMessage += '\n\n⚠️ ВНИМАНИЕ: Нехватка товара на складе!\n';
|
||
data.warnings.forEach(warning => {
|
||
successMessage += `\n• ${warning}`;
|
||
});
|
||
successMessage += '\n\nПроверьте остатки и пополните склад.';
|
||
}
|
||
|
||
alert(successMessage);
|
||
|
||
// Очищаем tempCart (изолированное состояние модалки)
|
||
tempCart.clear();
|
||
|
||
// Сбрасываем поля формы
|
||
document.getElementById('tempKitDescription').value = '';
|
||
document.getElementById('tempKitPhoto').value = '';
|
||
document.getElementById('photoPreview').style.display = 'none';
|
||
document.getElementById('priceAdjustmentType').value = 'none';
|
||
document.getElementById('priceAdjustmentValue').value = '0';
|
||
document.getElementById('adjustmentValueBlock').style.display = 'none';
|
||
document.getElementById('useSalePrice').checked = false;
|
||
document.getElementById('salePrice').value = '';
|
||
document.getElementById('salePriceBlock').style.display = 'none';
|
||
document.getElementById('showcaseKitQuantity').value = '1'; // Сброс количества
|
||
|
||
// Запоминаем, был ли режим редактирования до сброса
|
||
const wasEditMode = isEditMode;
|
||
|
||
// Сбрасываем режим редактирования
|
||
isEditMode = false;
|
||
editingKitId = null;
|
||
|
||
// Закрываем модальное окно
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||
modal.hide();
|
||
|
||
// Если это было СОЗДАНИЕ витринного комплекта из корзины,
|
||
// очищаем основную корзину POS
|
||
if (!wasEditMode) {
|
||
await clearCart();
|
||
}
|
||
|
||
// Обновляем витринные комплекты и переключаемся на вид витрины
|
||
isShowcaseView = true;
|
||
currentCategoryId = null;
|
||
await refreshShowcaseKits();
|
||
renderCategories();
|
||
renderProducts();
|
||
} else {
|
||
alert(`Ошибка: ${data.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving kit:', error);
|
||
alert('Ошибка при сохранении комплекта');
|
||
} finally {
|
||
confirmBtn.disabled = false;
|
||
const btnText = isEditMode
|
||
? '<i class="bi bi-check-circle"></i> Сохранить изменения'
|
||
: '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
||
confirmBtn.innerHTML = btnText;
|
||
}
|
||
};
|
||
|
||
// Обработчик кнопки "Разобрать букет"
|
||
document.getElementById('disassembleKitBtn').addEventListener('click', async () => {
|
||
if (!isEditMode || !editingKitId) {
|
||
alert('Ошибка: режим редактирования не активен');
|
||
return;
|
||
}
|
||
|
||
// Запрос подтверждения
|
||
const confirmed = confirm(
|
||
'Вы уверены?\n\n' +
|
||
'Букет будет разобран:\n' +
|
||
'• Все резервы компонентов будут освобождены\n' +
|
||
'• Комплект будет помечен как "Снят"\n\n' +
|
||
'Это действие нельзя отменить!'
|
||
);
|
||
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/pos/api/product-kits/${editingKitId}/disassemble/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCookie('csrftoken')
|
||
}
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
alert(`✅ ${data.message}\n\nОсвобождено резервов: ${data.released_count}`);
|
||
|
||
// Закрываем модальное окно
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||
modal.hide();
|
||
|
||
// Обновляем витринные комплекты
|
||
isShowcaseView = true;
|
||
currentCategoryId = null;
|
||
await refreshShowcaseKits();
|
||
renderCategories();
|
||
renderProducts();
|
||
} else {
|
||
alert(`❌ Ошибка: ${data.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error disassembling kit:', error);
|
||
alert('Произошла ошибка при разборе букета');
|
||
}
|
||
});
|
||
|
||
// Вспомогательная функция для получения CSRF токена (единая версия)
|
||
function getCookie(name) {
|
||
let cookieValue = null;
|
||
if (document.cookie && document.cookie !== '') {
|
||
const cookies = document.cookie.split(';');
|
||
for (let i = 0; i < cookies.length; i++) {
|
||
const cookie = cookies[i].trim();
|
||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return cookieValue;
|
||
}
|
||
|
||
// Алиас для обратной совместимости
|
||
// ВАЖНО: При 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 = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
||
|
||
// Скрываем кнопку "Разобрать" и блок добавления товаров
|
||
document.getElementById('disassembleKitBtn').style.display = 'none';
|
||
document.getElementById('showcaseKitQuantityBlock').style.display = 'block';
|
||
document.getElementById('addProductBlock').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Открытие модалки "Продажа" и рендер сводки корзины
|
||
document.getElementById('checkoutNow').onclick = () => {
|
||
if (cart.size === 0) {
|
||
alert('Корзина пуста. Добавьте товары перед продажей.');
|
||
return;
|
||
}
|
||
renderCheckoutModal();
|
||
const modal = new bootstrap.Modal(document.getElementById('checkoutModal'));
|
||
modal.show();
|
||
};
|
||
|
||
// Рендер позиций корзины и итога в модалке продажи
|
||
function renderCheckoutModal() {
|
||
const container = document.getElementById('checkoutItems');
|
||
container.innerHTML = '';
|
||
let total = 0;
|
||
|
||
cart.forEach((item) => {
|
||
const row = document.createElement('div');
|
||
row.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
|
||
|
||
// Иконка для комплектов
|
||
let typeIcon = '';
|
||
if (item.type === 'kit' || item.type === 'showcase_kit') {
|
||
typeIcon = '<i class="bi bi-box-seam text-info me-1" title="Комплект"></i>';
|
||
} else {
|
||
typeIcon = '<i class="bi bi-box text-success me-1" title="Товар"></i>';
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<div>
|
||
<div class="fw-semibold">${typeIcon}${item.name}</div>
|
||
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
||
</div>
|
||
<div class="fw-bold text-end">${formatMoney(item.qty * item.price)} руб.</div>
|
||
`;
|
||
container.appendChild(row);
|
||
total += item.qty * item.price;
|
||
});
|
||
|
||
// Обновляем информацию о клиенте
|
||
updateCustomerDisplay();
|
||
}
|
||
|
||
// ===== CHECKOUT: ПОДТВЕРЖДЕНИЕ ПРОДАЖИ =====
|
||
|
||
let paymentWidget = null;
|
||
|
||
// При открытии модалки checkout
|
||
document.getElementById('checkoutModal').addEventListener('show.bs.modal', () => {
|
||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||
const walletBalance = customer.wallet_balance || 0;
|
||
|
||
// Показываем баланс кошелька
|
||
const walletDiv = document.getElementById('checkoutWalletBalance');
|
||
if (customer.id !== SYSTEM_CUSTOMER.id) {
|
||
document.getElementById('checkoutWalletBalanceAmount').textContent = walletBalance.toFixed(2);
|
||
walletDiv.style.display = 'block';
|
||
} else {
|
||
walletDiv.style.display = 'none';
|
||
}
|
||
|
||
// Вычисляем итоговую сумму
|
||
let totalAmount = 0;
|
||
cart.forEach((item) => {
|
||
totalAmount += item.qty * item.price;
|
||
});
|
||
|
||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(totalAmount) + ' руб.';
|
||
|
||
// Инициализируем виджет в single mode
|
||
initPaymentWidget('single', {
|
||
order: { total: totalAmount, amount_due: totalAmount, amount_paid: 0 },
|
||
customer: { id: customer.id, name: customer.name, wallet_balance: walletBalance }
|
||
});
|
||
});
|
||
|
||
// Переключение режима оплаты
|
||
document.getElementById('singlePaymentMode').addEventListener('click', function() {
|
||
document.getElementById('singlePaymentMode').classList.add('active');
|
||
document.getElementById('mixedPaymentMode').classList.remove('active');
|
||
reinitPaymentWidget('single');
|
||
});
|
||
|
||
document.getElementById('mixedPaymentMode').addEventListener('click', function() {
|
||
document.getElementById('mixedPaymentMode').classList.add('active');
|
||
document.getElementById('singlePaymentMode').classList.remove('active');
|
||
reinitPaymentWidget('mixed');
|
||
});
|
||
|
||
function reinitPaymentWidget(mode) {
|
||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||
const totalAmountText = document.getElementById('checkoutFinalPrice').textContent;
|
||
const totalAmount = parseFloat(totalAmountText.replace(/[^\d.]/g, ''));
|
||
|
||
initPaymentWidget(mode, {
|
||
order: { total: totalAmount, amount_due: totalAmount, amount_paid: 0 },
|
||
customer: { id: customer.id, name: customer.name, wallet_balance: customer.wallet_balance || 0 }
|
||
});
|
||
}
|
||
|
||
async function initPaymentWidget(mode, data) {
|
||
const paymentMethods = [
|
||
{ id: 1, code: 'account_balance', name: 'С баланса счёта' },
|
||
{ id: 2, code: 'cash', name: 'Наличными' },
|
||
{ id: 3, code: 'card', name: 'Картой' },
|
||
{ id: 4, code: 'online', name: 'Онлайн' }
|
||
];
|
||
|
||
// Динамически загружаем PaymentWidget если еще не загружен
|
||
if (!window.PaymentWidget) {
|
||
try {
|
||
const module = await import('/static/orders/js/payment_widget.js');
|
||
window.PaymentWidget = module.PaymentWidget;
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки PaymentWidget:', error);
|
||
alert('Ошибка загрузки модуля оплаты. Перезагрузите страницу.');
|
||
return;
|
||
}
|
||
}
|
||
|
||
paymentWidget = new window.PaymentWidget({
|
||
containerId: 'paymentWidgetContainer',
|
||
mode: mode,
|
||
order: data.order,
|
||
customer: data.customer,
|
||
paymentMethods: paymentMethods,
|
||
onSubmit: (paymentsData) => handleCheckoutSubmit(paymentsData)
|
||
});
|
||
}
|
||
|
||
// Обработчик кнопки "Подтвердить продажу"
|
||
document.getElementById('confirmCheckoutBtn').onclick = () => {
|
||
if (paymentWidget) {
|
||
paymentWidget.submit();
|
||
}
|
||
};
|
||
|
||
// Отправка заказа на сервер
|
||
async function handleCheckoutSubmit(paymentsData) {
|
||
try {
|
||
// Блокируем кнопку
|
||
const btn = document.getElementById('confirmCheckoutBtn');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
|
||
|
||
// Собираем данные
|
||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||
const orderData = {
|
||
customer_id: customer.id,
|
||
warehouse_id: currentWarehouse.id,
|
||
items: Array.from(cart.values()).map(item => {
|
||
const itemData = {
|
||
type: item.type,
|
||
id: item.id,
|
||
quantity: item.qty,
|
||
price: item.price
|
||
};
|
||
// Для витринных букетов передаём ID конкретных экземпляров
|
||
if (item.type === 'showcase_kit' && item.showcase_item_ids) {
|
||
itemData.showcase_item_ids = item.showcase_item_ids;
|
||
}
|
||
// Для товаров с единицами продажи
|
||
if (item.sales_unit_id) {
|
||
itemData.sales_unit_id = item.sales_unit_id;
|
||
}
|
||
return itemData;
|
||
}),
|
||
payments: paymentsData,
|
||
notes: document.getElementById('orderNote').value.trim()
|
||
};
|
||
|
||
// Отправляем на сервер
|
||
const response = await fetch('/pos/api/checkout/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': getCookie('csrftoken')
|
||
},
|
||
body: JSON.stringify(orderData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
console.log('✅ Заказ успешно создан:', result);
|
||
|
||
// Успех
|
||
alert(`Заказ #${result.order_number} успешно создан!\nСумма: ${result.total_amount.toFixed(2)} руб.`);
|
||
|
||
// Очищаем корзину
|
||
cart.clear();
|
||
renderCart();
|
||
console.log('🧹 Корзина очищена');
|
||
|
||
// Закрываем модалку
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
console.log('❌ Модалка закрыта');
|
||
}
|
||
|
||
// Перезагружаем страницу для обновления остатков товаров
|
||
console.log('🔄 Перезагрузка страницы...');
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 500);
|
||
|
||
} else {
|
||
alert('Ошибка: ' + result.error);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка checkout:', error);
|
||
alert('Ошибка при проведении продажи: ' + error.message);
|
||
} finally {
|
||
// Разблокируем кнопку
|
||
const btn = document.getElementById('confirmCheckoutBtn');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-check2-circle"></i> Подтвердить продажу';
|
||
}
|
||
}
|
||
|
||
// ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
||
|
||
// Кнопка "Выбрать клиента" в корзине
|
||
document.getElementById('customerSelectBtn').addEventListener('click', () => {
|
||
const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
|
||
modal.show();
|
||
});
|
||
|
||
// Кнопка сброса клиента на системного (в корзине)
|
||
document.getElementById('resetCustomerBtn').addEventListener('click', () => {
|
||
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
|
||
});
|
||
|
||
// Кнопка "Выбрать клиента" в модалке продажи
|
||
document.getElementById('checkoutCustomerSelectBtn').addEventListener('click', () => {
|
||
const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
|
||
modal.show();
|
||
});
|
||
|
||
// Кнопка сброса клиента на системного (в модалке продажи)
|
||
document.getElementById('checkoutResetCustomerBtn').addEventListener('click', () => {
|
||
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
|
||
});
|
||
|
||
// Кнопка "Создать нового клиента" в модалке выбора
|
||
document.getElementById('createNewCustomerBtn').addEventListener('click', () => {
|
||
// Закрываем модалку выбора
|
||
const selectModal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
|
||
selectModal.hide();
|
||
|
||
// Открываем модалку создания
|
||
openCreateCustomerModal();
|
||
});
|
||
|
||
// Кнопка "Выбрать системного клиента"
|
||
document.getElementById('selectSystemCustomerBtn').addEventListener('click', () => {
|
||
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name, SYSTEM_CUSTOMER.wallet_balance || 0);
|
||
|
||
// Закрываем модалку
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
|
||
modal.hide();
|
||
});
|
||
|
||
// Кнопка подтверждения создания клиента
|
||
document.getElementById('confirmCreateCustomerBtn').addEventListener('click', () => {
|
||
createNewCustomer();
|
||
});
|
||
|
||
// Инициализация Select2 при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initCustomerSelect2();
|
||
updateCustomerDisplay(); // Обновляем UI с системным клиентом
|
||
|
||
// Восстанавливаем корзину из Redis (если есть сохраненные данные)
|
||
const savedCartData = JSON.parse(document.getElementById('cartData').textContent);
|
||
if (savedCartData && Object.keys(savedCartData).length > 0) {
|
||
// Конвертируем обычный объект обратно в Map
|
||
Object.entries(savedCartData).forEach(([key, value]) => {
|
||
cart.set(key, value);
|
||
});
|
||
renderCart(); // Отрисовываем восстановленную корзину
|
||
}
|
||
|
||
// ===== ОБРАБОТЧИКИ ДЛЯ МОДАЛКИ ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||
|
||
// Кнопки изменения количества
|
||
document.getElementById('unitQtyDecrement').addEventListener('click', () => {
|
||
const input = document.getElementById('unitModalQuantity');
|
||
const step = parseFloat(input.step) || 1;
|
||
const newValue = Math.max(parseFloat(input.min), parseFloat(input.value) - step);
|
||
input.value = roundQuantity(newValue, 3);
|
||
calculateUnitModalSubtotal();
|
||
validateUnitQuantity();
|
||
});
|
||
|
||
document.getElementById('unitQtyIncrement').addEventListener('click', () => {
|
||
const input = document.getElementById('unitModalQuantity');
|
||
const step = parseFloat(input.step) || 1;
|
||
const newValue = parseFloat(input.value) + step;
|
||
input.value = roundQuantity(newValue, 3);
|
||
calculateUnitModalSubtotal();
|
||
validateUnitQuantity();
|
||
});
|
||
|
||
// Изменение количества вручную
|
||
document.getElementById('unitModalQuantity').addEventListener('input', () => {
|
||
calculateUnitModalSubtotal();
|
||
validateUnitQuantity();
|
||
});
|
||
|
||
// Округление количества при потере фокуса
|
||
document.getElementById('unitModalQuantity').addEventListener('blur', (e) => {
|
||
const rawValue = parseFloat(e.target.value) || 0;
|
||
e.target.value = roundQuantity(rawValue, 3);
|
||
calculateUnitModalSubtotal();
|
||
validateUnitQuantity();
|
||
});
|
||
|
||
// Изменение цены
|
||
document.getElementById('unitModalPrice').addEventListener('input', () => {
|
||
calculateUnitModalSubtotal();
|
||
});
|
||
|
||
// Кнопка подтверждения добавления в корзину
|
||
document.getElementById('confirmAddUnitToCart').addEventListener('click', () => {
|
||
addToCartFromModal();
|
||
});
|
||
});
|
||
|
||
// Смена склада
|
||
const changeWarehouseBtn = document.getElementById('changeWarehouseBtn');
|
||
if (changeWarehouseBtn) {
|
||
changeWarehouseBtn.addEventListener('click', () => {
|
||
const modal = new bootstrap.Modal(document.getElementById('selectWarehouseModal'));
|
||
modal.show();
|
||
});
|
||
}
|
||
|
||
// Обработка выбора склада из списка
|
||
document.addEventListener('click', async (e) => {
|
||
const warehouseItem = e.target.closest('.warehouse-item');
|
||
if (!warehouseItem) return;
|
||
|
||
const warehouseId = warehouseItem.dataset.warehouseId;
|
||
const warehouseName = warehouseItem.dataset.warehouseName;
|
||
|
||
// Проверяем, есть ли товары в корзине
|
||
if (cart.size > 0) {
|
||
const confirmed = confirm(`При смене склада корзина будет очищена.\n\nПереключиться на склад "${warehouseName}"?`);
|
||
if (!confirmed) return;
|
||
}
|
||
|
||
try {
|
||
// Отправляем запрос на смену склада
|
||
const response = await fetch(`/pos/api/set-warehouse/${warehouseId}/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCsrfToken()
|
||
}
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Перезагружаем страницу для обновления данных
|
||
location.reload();
|
||
} else {
|
||
alert(`Ошибка: ${data.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при смене склада:', error);
|
||
alert('Произошла ошибка при смене склада');
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// Обработчик поиска с debounce
|
||
const searchInput = document.getElementById('searchInput');
|
||
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
||
|
||
searchInput.addEventListener('input', (e) => {
|
||
const query = e.target.value.trim();
|
||
|
||
// Показываем/скрываем кнопку очистки
|
||
if (e.target.value.length > 0) {
|
||
clearSearchBtn.style.display = 'block';
|
||
} else {
|
||
clearSearchBtn.style.display = 'none';
|
||
}
|
||
|
||
// Отменяем предыдущий таймер
|
||
if (searchDebounceTimer) {
|
||
clearTimeout(searchDebounceTimer);
|
||
}
|
||
|
||
// Если поле пустое — очищаем экран
|
||
if (query === '') {
|
||
currentSearchQuery = '';
|
||
ITEMS = []; // Очистка
|
||
renderProducts(); // Пустой экран
|
||
return;
|
||
}
|
||
|
||
// Минимальная длина поиска — 3 символа
|
||
if (query.length < 3) {
|
||
// Не реагируем на ввод менее 3 символов
|
||
return;
|
||
}
|
||
|
||
// Для витрины — мгновенная клиентская фильтрация
|
||
if (isShowcaseView) {
|
||
renderProducts();
|
||
return;
|
||
}
|
||
|
||
// Для обычных товаров/комплектов — серверный поиск с debounce 300мс
|
||
searchDebounceTimer = setTimeout(async () => {
|
||
currentSearchQuery = query;
|
||
await loadItems(); // Перезагрузка с серверным поиском
|
||
}, 300);
|
||
});
|
||
|
||
// Обработчик кнопки очистки поиска
|
||
clearSearchBtn.addEventListener('click', () => {
|
||
searchInput.value = '';
|
||
clearSearchBtn.style.display = 'none';
|
||
currentSearchQuery = '';
|
||
ITEMS = [];
|
||
renderProducts(); // Пустой экран
|
||
});
|
||
|
||
// Инициализация
|
||
renderCategories();
|
||
renderProducts(); // Сначала пустая сетка
|
||
renderCart();
|
||
setupInfiniteScroll(); // Установка infinite scroll
|
||
|
||
// Установить фокус на строку поиска
|
||
document.getElementById('searchInput').focus();
|
||
|
||
// ===== ОТЛОЖЕННЫЙ ЗАКАЗ =====
|
||
|
||
/**
|
||
* Создаёт отложенный заказ (черновик) и резервирует витринные букеты
|
||
*
|
||
* 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);
|
||
}
|