Implemented intuitive quantity management in the cart with: - Minus button (-): Decreases quantity by 1, removes item if qty becomes 0 - Plus button (+): Increases quantity by 1 - Quantity input field: Centered, uniform styling with buttons - All controls styled with Bootstrap outline-secondary (gray) for cohesive look - Buttons feature Bootstrap Icons (dash-circle, plus-circle) at 1.2em size - Auto-save to Redis after each quantity change Benefits: ✅ Faster quantity adjustments without keyboard input ✅ Consistent gray styling across all quantity controls ✅ Mobile-friendly touch targets ✅ Automatic cart persistence maintained 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1610 lines
57 KiB
JavaScript
1610 lines
57 KiB
JavaScript
// POS Terminal JavaScript
|
||
|
||
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();
|
||
|
||
// ===== СОХРАНЕНИЕ КОРЗИНЫ В 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);
|
||
|
||
function formatMoney(v) {
|
||
return (Number(v)).toFixed(2);
|
||
}
|
||
|
||
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
||
|
||
/**
|
||
* Обновляет отображение выбранного клиента в UI
|
||
* Обновляет:
|
||
* - Кнопку "Выбрать клиента" в корзине (показывает имя клиента)
|
||
* - Имя клиента в модалке продажи
|
||
* - Видимость кнопки сброса (показываем только для не-системного клиента)
|
||
*/
|
||
function updateCustomerDisplay() {
|
||
// Обновляем текст кнопки - всегда показываем имя клиента
|
||
const btnText = document.getElementById('customerSelectBtnText');
|
||
btnText.textContent = selectedCustomer.name;
|
||
|
||
// Обновляем имя клиента в модалке продажи
|
||
const checkoutCustomerName = document.getElementById('checkoutCustomerName');
|
||
if (checkoutCustomerName) {
|
||
checkoutCustomerName.textContent = selectedCustomer.name;
|
||
}
|
||
|
||
// Показываем/скрываем кнопку сброса
|
||
const resetBtn = document.getElementById('resetCustomerBtn');
|
||
if (resetBtn) {
|
||
// Показываем кнопку сброса только если выбран НЕ системный клиент
|
||
if (selectedCustomer.id !== SYSTEM_CUSTOMER.id) {
|
||
resetBtn.style.display = 'block';
|
||
} else {
|
||
resetBtn.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Устанавливает нового клиента и сохраняет в Redis
|
||
* @param {number} customerId - ID клиента
|
||
* @param {string} customerName - Имя клиента
|
||
*/
|
||
function selectCustomer(customerId, customerName) {
|
||
selectedCustomer = {
|
||
id: customerId,
|
||
name: customerName
|
||
};
|
||
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);
|
||
}
|
||
})
|
||
.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);
|
||
|
||
// Закрываем модалку
|
||
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);
|
||
|
||
// Закрываем модалку
|
||
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');
|
||
}
|
||
}
|
||
|
||
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') {
|
||
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') {
|
||
stock.textContent = `🌺 ${item.showcase_name}`;
|
||
stock.style.color = '#856404';
|
||
stock.style.fontWeight = 'bold';
|
||
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
|
||
// Для обычных товаров показываем остатки: FREE(-RESERVED)
|
||
// FREE = доступно для продажи (available - reserved)
|
||
const available = parseFloat(item.available_qty) || 0;
|
||
const reserved = parseFloat(item.reserved_qty) || 0;
|
||
const free = available - reserved;
|
||
|
||
// Создаём элементы для стилизации разных размеров
|
||
const freeSpan = document.createElement('span');
|
||
freeSpan.textContent = free;
|
||
freeSpan.style.fontSize = '1.1em';
|
||
freeSpan.style.fontWeight = 'bold';
|
||
freeSpan.style.fontStyle = 'normal';
|
||
|
||
// Отображаем резерв только если он есть
|
||
if (reserved > 0) {
|
||
const reservedSpan = document.createElement('span');
|
||
reservedSpan.textContent = `(−${reserved})`;
|
||
reservedSpan.style.fontSize = '0.85em';
|
||
reservedSpan.style.marginLeft = '3px';
|
||
reservedSpan.style.fontStyle = 'normal';
|
||
|
||
stock.appendChild(freeSpan);
|
||
stock.appendChild(reservedSpan);
|
||
} else {
|
||
stock.appendChild(freeSpan);
|
||
}
|
||
|
||
// Цветовая индикация: красный если свободных остатков нет или отрицательные
|
||
if (free <= 0) {
|
||
stock.style.color = '#dc3545'; // Красный
|
||
} else if (free < 5) {
|
||
stock.style.color = '#ffc107'; // Жёлтый (мало остатков)
|
||
} else {
|
||
stock.style.color = '#28a745'; // Зелёный (достаточно)
|
||
}
|
||
} else {
|
||
// Fallback для старых данных или комплектов
|
||
stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ';
|
||
if (!item.in_stock) {
|
||
stock.style.color = '#dc3545';
|
||
}
|
||
}
|
||
|
||
const sku = document.createElement('div');
|
||
sku.className = 'product-sku';
|
||
|
||
const skuText = document.createElement('span');
|
||
skuText.textContent = item.sku || 'н/д';
|
||
|
||
const priceSpan = document.createElement('span');
|
||
priceSpan.className = 'product-price';
|
||
priceSpan.textContent = `${formatMoney(item.price)}`;
|
||
|
||
sku.appendChild(skuText);
|
||
sku.appendChild(priceSpan);
|
||
|
||
info.appendChild(name);
|
||
info.appendChild(stock);
|
||
info.appendChild(sku);
|
||
|
||
body.appendChild(imageDiv);
|
||
body.appendChild(info);
|
||
card.appendChild(body);
|
||
col.appendChild(card);
|
||
grid.appendChild(col);
|
||
});
|
||
}
|
||
|
||
// Загрузка товаров через API
|
||
async function loadItems(append = false) {
|
||
if (isLoadingItems) return;
|
||
|
||
isLoadingItems = true;
|
||
|
||
if (!append) {
|
||
currentPage = 1;
|
||
ITEMS = [];
|
||
}
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: currentPage,
|
||
page_size: 60
|
||
});
|
||
|
||
if (currentCategoryId) {
|
||
params.append('category_id', currentCategoryId);
|
||
}
|
||
|
||
// Добавляем поисковый запрос, если есть
|
||
if (currentSearchQuery) {
|
||
params.append('query', currentSearchQuery);
|
||
}
|
||
|
||
const response = await fetch(`/pos/api/items/?${params}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
if (append) {
|
||
ITEMS = ITEMS.concat(data.items);
|
||
} else {
|
||
ITEMS = data.items;
|
||
}
|
||
|
||
hasMoreItems = data.has_more;
|
||
|
||
if (data.has_more) {
|
||
currentPage = data.next_page;
|
||
}
|
||
|
||
renderProducts();
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки товаров:', error);
|
||
} finally {
|
||
isLoadingItems = false;
|
||
}
|
||
}
|
||
|
||
// Infinite scroll
|
||
function setupInfiniteScroll() {
|
||
const grid = document.getElementById('productGrid');
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting && hasMoreItems && !isLoadingItems && !isShowcaseView) {
|
||
loadItems(true); // Догрузка
|
||
}
|
||
});
|
||
},
|
||
{
|
||
rootMargin: '200px'
|
||
}
|
||
);
|
||
|
||
// Наблюдаем за концом грида
|
||
const sentinel = document.createElement('div');
|
||
sentinel.id = 'scroll-sentinel';
|
||
sentinel.style.height = '1px';
|
||
grid.parentElement.appendChild(sentinel);
|
||
observer.observe(sentinel);
|
||
}
|
||
|
||
function addToCart(item) {
|
||
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
|
||
|
||
if (!cart.has(cartKey)) {
|
||
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
|
||
} else {
|
||
cart.get(cartKey).qty += 1;
|
||
}
|
||
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем в Redis
|
||
|
||
// Автоматический фокус на поле количества
|
||
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);
|
||
}
|
||
|
||
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';
|
||
return;
|
||
}
|
||
|
||
cart.forEach((item, cartKey) => {
|
||
const row = document.createElement('div');
|
||
row.className = 'cart-item mb-2';
|
||
|
||
// Левая часть: Название и цена единицы
|
||
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> ';
|
||
}
|
||
|
||
namePrice.innerHTML = `
|
||
<div class="fw-semibold small">${typeIcon}${item.name}</div>
|
||
<div class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)} / шт</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';
|
||
|
||
// Кнопка минус
|
||
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 = (e) => {
|
||
e.preventDefault();
|
||
const currentQty = cart.get(cartKey).qty;
|
||
if (currentQty <= 1) {
|
||
removeFromCart(cartKey);
|
||
} else {
|
||
cart.get(cartKey).qty = currentQty - 1;
|
||
renderCart();
|
||
saveCartToRedis();
|
||
}
|
||
};
|
||
|
||
// Поле ввода количества
|
||
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 = item.qty;
|
||
qtyInput.min = 1;
|
||
qtyInput.onchange = (e) => {
|
||
const newQty = parseInt(e.target.value) || 1;
|
||
if (newQty <= 0) {
|
||
removeFromCart(cartKey);
|
||
} else {
|
||
cart.get(cartKey).qty = newQty;
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем в Redis при изменении количества
|
||
}
|
||
};
|
||
|
||
// Кнопка плюс
|
||
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 = (e) => {
|
||
e.preventDefault();
|
||
cart.get(cartKey).qty += 1;
|
||
renderCart();
|
||
saveCartToRedis();
|
||
};
|
||
|
||
// Собираем контейнер
|
||
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);
|
||
}
|
||
|
||
function removeFromCart(cartKey) {
|
||
cart.delete(cartKey);
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем в Redis
|
||
}
|
||
|
||
function clearCart() {
|
||
cart.clear();
|
||
renderCart();
|
||
saveCartToRedis(); // Сохраняем пустую корзину в Redis
|
||
}
|
||
|
||
document.getElementById('clearCart').onclick = clearCart;
|
||
|
||
// Кнопка "На витрину" - функционал будет добавлен позже
|
||
document.getElementById('addToShowcaseBtn').onclick = () => {
|
||
openCreateTempKitModal();
|
||
};
|
||
|
||
// Функция открытия модального окна для создания временного комплекта
|
||
async function openCreateTempKitModal() {
|
||
// Проверяем что корзина не пуста
|
||
if (cart.size === 0) {
|
||
alert('Корзина пуста. Добавьте товары перед созданием комплекта.');
|
||
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 = 'Сохранить изменения';
|
||
|
||
// Открываем модальное окно
|
||
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
||
modal.show();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading kit for edit:', error);
|
||
alert('Ошибка при загрузке комплекта');
|
||
}
|
||
}
|
||
|
||
// Обновление списка витринных комплектов
|
||
async function refreshShowcaseKits() {
|
||
try {
|
||
const response = await fetch('/pos/api/showcase-kits/');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showcaseKits = data.items;
|
||
} else {
|
||
console.error('Failed to refresh showcase kits:', data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error refreshing showcase kits:', error);
|
||
}
|
||
}
|
||
|
||
// Загрузка списка витрин
|
||
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_warehouse) {
|
||
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-1 pb-1 border-bottom';
|
||
itemDiv.innerHTML = `
|
||
<div>
|
||
<strong class="small">${item.name}</strong>
|
||
<br>
|
||
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
||
</div>
|
||
<div class="text-end">
|
||
<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>
|
||
</div>
|
||
`;
|
||
container.appendChild(itemDiv);
|
||
|
||
estimatedTotal += item.qty * item.price;
|
||
});
|
||
|
||
// Обновляем все расчеты цен
|
||
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;
|
||
|
||
// Формируем FormData для отправки с файлом
|
||
const formData = new FormData();
|
||
formData.append('kit_name', kitName);
|
||
if (showcaseId) {
|
||
formData.append('showcase_id', showcaseId);
|
||
}
|
||
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 successMessage = isEditMode
|
||
? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
|
||
: `✅ ${data.message}
|
||
|
||
Комплект: ${data.kit_name}
|
||
Цена: ${data.kit_price} руб.
|
||
Зарезервировано компонентов: ${data.reservations_count}`;
|
||
|
||
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';
|
||
|
||
// Сбрасываем режим редактирования
|
||
isEditMode = false;
|
||
editingKitId = null;
|
||
|
||
// Закрываем модальное окно
|
||
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 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;
|
||
}
|
||
};
|
||
|
||
// Вспомогательная функция для получения 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;
|
||
}
|
||
|
||
// Сброс режима редактирования при закрытии модального окна
|
||
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('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();
|
||
|
||
// Обновляем базовую цену и пересчитываем
|
||
updateCheckoutPricing(total);
|
||
}
|
||
|
||
// Пересчёт цен в модалке продажи
|
||
function updateCheckoutPricing(basePrice = null) {
|
||
// Если basePrice не передан, пересчитываем из корзины
|
||
if (basePrice === null) {
|
||
basePrice = 0;
|
||
cart.forEach((item) => {
|
||
basePrice += item.qty * item.price;
|
||
});
|
||
}
|
||
|
||
// Базовая сумма
|
||
document.getElementById('checkoutBasePrice').textContent = formatMoney(basePrice) + ' руб.';
|
||
|
||
// Скидка
|
||
const discountType = document.getElementById('discountType').value;
|
||
const discountValue = parseFloat(document.getElementById('discountValue').value) || 0;
|
||
|
||
let discountedPrice = basePrice;
|
||
if (discountType !== 'none' && discountValue > 0) {
|
||
if (discountType === 'percent') {
|
||
discountedPrice = basePrice - (basePrice * discountValue / 100);
|
||
} else if (discountType === 'amount') {
|
||
discountedPrice = Math.max(0, basePrice - discountValue);
|
||
}
|
||
}
|
||
|
||
document.getElementById('checkoutDiscountedPrice').textContent = formatMoney(discountedPrice) + ' руб.';
|
||
|
||
// Финальная цена (с учётом ручной суммы если задана)
|
||
const useManualPrice = document.getElementById('useManualPrice').checked;
|
||
const manualPrice = parseFloat(document.getElementById('manualPrice').value) || 0;
|
||
|
||
let finalPrice = discountedPrice;
|
||
if (useManualPrice && manualPrice > 0) {
|
||
finalPrice = manualPrice;
|
||
}
|
||
|
||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalPrice);
|
||
}
|
||
|
||
// Обработчики для полей скидки и цены
|
||
document.getElementById('discountType').addEventListener('change', function() {
|
||
const discountBlock = document.getElementById('discountValueBlock');
|
||
if (this.value === 'none') {
|
||
discountBlock.style.display = 'none';
|
||
document.getElementById('discountValue').value = '0';
|
||
} else {
|
||
discountBlock.style.display = 'block';
|
||
}
|
||
updateCheckoutPricing();
|
||
});
|
||
|
||
document.getElementById('discountValue').addEventListener('input', function() {
|
||
updateCheckoutPricing();
|
||
});
|
||
|
||
document.getElementById('useManualPrice').addEventListener('change', function() {
|
||
const manualPriceBlock = document.getElementById('manualPriceBlock');
|
||
if (this.checked) {
|
||
manualPriceBlock.style.display = 'block';
|
||
} else {
|
||
manualPriceBlock.style.display = 'none';
|
||
document.getElementById('manualPrice').value = '';
|
||
}
|
||
updateCheckoutPricing();
|
||
});
|
||
|
||
document.getElementById('manualPrice').addEventListener('input', function() {
|
||
updateCheckoutPricing();
|
||
});
|
||
|
||
// Подтверждение продажи (пока заглушка)
|
||
document.getElementById('confirmCheckoutBtn').onclick = () => {
|
||
const payment = document.getElementById('paymentMethod').value;
|
||
const note = document.getElementById('orderNote').value.trim();
|
||
|
||
const paymentText = {
|
||
'cash': 'Наличные',
|
||
'card': 'Карта',
|
||
'mixed': 'Смешанная оплата'
|
||
}[payment] || payment;
|
||
|
||
// Получаем данные о ценах и скидке
|
||
const basePrice = document.getElementById('checkoutBasePrice').textContent;
|
||
const discountType = document.getElementById('discountType').value;
|
||
const discountValue = document.getElementById('discountValue').value;
|
||
const finalPrice = document.getElementById('checkoutFinalPrice').textContent;
|
||
|
||
let discountInfo = 'нет скидки';
|
||
if (discountType === 'percent' && discountValue > 0) {
|
||
discountInfo = `скидка ${discountValue}%`;
|
||
} else if (discountType === 'amount' && discountValue > 0) {
|
||
discountInfo = `скидка ${discountValue} руб.`;
|
||
}
|
||
|
||
const useManual = document.getElementById('useManualPrice').checked;
|
||
if (useManual) {
|
||
discountInfo += ' (установлена своя сумма)';
|
||
}
|
||
|
||
alert(`Функция проведения продажи будет подключена позже.
|
||
|
||
Базовая сумма: ${basePrice}
|
||
Скидка: ${discountInfo}
|
||
Итого к оплате: ${finalPrice} руб.
|
||
Оплата: ${paymentText}
|
||
Комментарий: ${note || '—'}`);
|
||
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
|
||
modal.hide();
|
||
};
|
||
|
||
document.getElementById('scheduleLater').onclick = async () => {
|
||
alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.');
|
||
};
|
||
|
||
// ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
||
|
||
// Кнопка "Выбрать клиента" в корзине
|
||
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);
|
||
});
|
||
|
||
// Кнопка "Создать нового клиента" в модалке выбора
|
||
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);
|
||
|
||
// Закрываем модалку
|
||
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(); // Отрисовываем восстановленную корзину
|
||
}
|
||
});
|
||
|
||
// Смена склада
|
||
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('Произошла ошибка при смене склада');
|
||
}
|
||
});
|
||
|
||
// Вспомогательная функция для получения CSRF токена
|
||
function getCsrfToken() {
|
||
const name = 'csrftoken';
|
||
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;
|
||
}
|
||
|
||
// Обработчик поиска с 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();
|