feat(pos): улучшена работа с витринными комплектами

- добавлена кнопка редактирования комплектов с индикатором устаревшей цены
- реализовано редактирование цен товаров в комплекте через inline-поля ввода
- добавлена автовыбор витрины по умолчанию при создании комплекта
- улучшена генерация названия комплекта по умолчанию
- исправлены поля API для совместимости с сервером (kit_name, qty, name)
- добавлен глобальный доступ к showcaseManager из terminal.js
This commit is contained in:
2026-01-27 20:29:35 +03:00
parent 9dccc1f4b5
commit c0e9b92e4a
3 changed files with 132 additions and 22 deletions

View File

@@ -517,6 +517,47 @@ export class ProductManager {
* @private * @private
*/ */
_renderShowcaseKitBadges(card, item, cart) { _renderShowcaseKitBadges(card, item, cart) {
// Кнопка редактирования (только если не заблокирован другим)
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.title = 'Редактировать комплект';
editBtn.onclick = (e) => {
e.stopPropagation();
if (window.showcaseManager) {
window.showcaseManager.openEditModal(item.id);
}
};
card.appendChild(editBtn);
// Индикатор неактуальной цены (красный кружок)
if (item.price_outdated) {
const outdatedBadge = document.createElement('div');
outdatedBadge.className = 'badge bg-danger';
outdatedBadge.style.position = 'absolute';
outdatedBadge.style.top = '5px';
outdatedBadge.style.right = '45px';
outdatedBadge.style.zIndex = '10';
outdatedBadge.style.width = '18px';
outdatedBadge.style.height = '18px';
outdatedBadge.style.padding = '0';
outdatedBadge.style.borderRadius = '50%';
outdatedBadge.style.display = 'flex';
outdatedBadge.style.alignItems = 'center';
outdatedBadge.style.justifyContent = 'center';
outdatedBadge.style.fontSize = '10px';
outdatedBadge.style.minWidth = '18px';
outdatedBadge.title = 'Цена неактуальна';
outdatedBadge.innerHTML = '!';
card.appendChild(outdatedBadge);
}
}
// Индикация блокировки // Индикация блокировки
if (item.is_locked) { if (item.is_locked) {
const lockBadge = document.createElement('div'); const lockBadge = document.createElement('div');

View File

@@ -162,9 +162,9 @@ export class ShowcaseManager {
const key = `product-${item.product_id}-${item.sales_unit_id || 'base'}`; const key = `product-${item.product_id}-${item.sales_unit_id || 'base'}`;
this.tempCart.set(key, { this.tempCart.set(key, {
id: item.product_id, id: item.product_id,
name: item.product_name, name: item.name || item.product_name, // Сервер отдаёт name
price: item.price, price: parseFloat(item.price) || 0,
qty: item.quantity, qty: parseFloat(item.qty || item.quantity) || 1, // Сервер отдаёт qty
type: 'product', type: 'product',
sales_unit_id: item.sales_unit_id, sales_unit_id: item.sales_unit_id,
unit_name: item.unit_name unit_name: item.unit_name
@@ -254,11 +254,26 @@ export class ShowcaseManager {
if (!select) return; if (!select) return;
let html = '<option value="">Выберите витрину...</option>'; let html = '<option value="">Выберите витрину...</option>';
let defaultShowcaseId = null;
this.showcases.forEach(showcase => { this.showcases.forEach(showcase => {
html += `<option value="${showcase.id}">${escapeHtml(showcase.name)}</option>`; const displayName = showcase.warehouse_name
? `${showcase.name} (${showcase.warehouse_name})`
: showcase.name;
html += `<option value="${showcase.id}">${escapeHtml(displayName)}</option>`;
// Запоминаем витрину по умолчанию
if (showcase.is_default) {
defaultShowcaseId = showcase.id;
}
}); });
select.innerHTML = html; select.innerHTML = html;
// Автовыбор витрины по умолчанию (только в режиме создания)
if (!this.isEditMode && defaultShowcaseId) {
select.value = defaultShowcaseId;
}
} }
/** /**
@@ -268,33 +283,83 @@ export class ShowcaseManager {
const container = document.getElementById('tempKitItemsList'); const container = document.getElementById('tempKitItemsList');
if (!container) return; if (!container) return;
container.innerHTML = '';
if (this.tempCart.size === 0) { if (this.tempCart.size === 0) {
container.innerHTML = '<p class="text-muted text-center mb-0">Нет товаров</p>'; container.innerHTML = '<p class="text-muted text-center mb-0">Нет товаров</p>';
return; return;
} }
let html = '';
let totalBasePrice = 0; let totalBasePrice = 0;
this.tempCart.forEach((item, key) => { this.tempCart.forEach((item, key) => {
const itemTotal = item.price * item.qty; const itemTotal = item.price * item.qty;
totalBasePrice += itemTotal; totalBasePrice += itemTotal;
html += ` const itemDiv = document.createElement('div');
<div class="d-flex justify-content-between align-items-center py-1 border-bottom"> itemDiv.className = 'd-flex justify-content-between align-items-center py-1 border-bottom';
<div class="flex-grow-1">
<div class="small">${escapeHtml(item.name)}</div>
<div class="text-muted" style="font-size: 0.75rem;">
${formatMoney(item.price)} × ${roundQuantity(item.qty)}
${item.unit_name ? ' ' + item.unit_name : ''}
</div>
</div>
<div class="fw-semibold small">${formatMoney(itemTotal)}</div>
</div>
`;
});
container.innerHTML = html; // Левая часть: название и цена
const leftDiv = document.createElement('div');
leftDiv.className = 'flex-grow-1';
// Название товара
const nameDiv = document.createElement('div');
nameDiv.className = 'small';
nameDiv.textContent = item.name;
leftDiv.appendChild(nameDiv);
// Контейнер цены с полем ввода
const priceContainer = document.createElement('div');
priceContainer.className = 'text-muted d-flex align-items-center';
priceContainer.style.fontSize = '0.75rem';
priceContainer.style.gap = '4px';
// Поле ввода цены (всегда видимое, без стрелочек)
const priceInput = document.createElement('input');
priceInput.type = 'text';
priceInput.inputMode = 'decimal';
priceInput.className = 'form-control form-control-sm';
priceInput.style.width = '70px';
priceInput.style.padding = '2px 4px';
priceInput.style.textAlign = 'right';
priceInput.value = formatMoney(item.price);
// Сохранение цены при изменении
const savePrice = () => {
const newPrice = parseFloat(priceInput.value.replace(',', '.')) || 0;
if (item.price !== newPrice) {
item.price = newPrice;
this.renderTempKitItems();
this.updatePriceCalculation();
}
};
priceInput.onblur = savePrice;
priceInput.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
priceInput.blur();
}
};
priceInput.onfocus = () => priceInput.select();
priceContainer.appendChild(priceInput);
priceContainer.appendChild(document.createTextNode(`× ${roundQuantity(item.qty)}`));
if (item.unit_name) {
priceContainer.appendChild(document.createTextNode(` ${item.unit_name}`));
}
leftDiv.appendChild(priceContainer);
// Правая часть: итоговая сумма
const rightDiv = document.createElement('div');
rightDiv.className = 'fw-semibold small';
rightDiv.textContent = formatMoney(itemTotal);
itemDiv.appendChild(leftDiv);
itemDiv.appendChild(rightDiv);
container.appendChild(itemDiv);
});
// Обновляем базовую цену // Обновляем базовую цену
document.getElementById('tempKitBasePrice').textContent = formatMoney(totalBasePrice) + ' руб.'; document.getElementById('tempKitBasePrice').textContent = formatMoney(totalBasePrice) + ' руб.';
@@ -384,8 +449,11 @@ export class ShowcaseManager {
* Сбрасывает форму * Сбрасывает форму
*/ */
resetForm() { resetForm() {
document.getElementById('tempKitName').value = ''; // Генерируем название по умолчанию
document.getElementById('showcaseSelect').value = ''; const randomSuffix = Math.floor(Math.random() * 900) + 100;
const defaultName = `Витринный букет ${randomSuffix}`;
document.getElementById('tempKitName').value = defaultName;
// Не сбрасываем showcaseSelect - витрина по умолчанию выбирается в renderShowcaseSelect()
document.getElementById('showcaseKitQuantity').value = '1'; document.getElementById('showcaseKitQuantity').value = '1';
document.getElementById('tempKitDescription').value = ''; document.getElementById('tempKitDescription').value = '';
document.getElementById('showcaseCreatedAt').value = ''; document.getElementById('showcaseCreatedAt').value = '';
@@ -463,7 +531,7 @@ export class ShowcaseManager {
// Формируем данные для отправки // Формируем данные для отправки
const formData = new FormData(); const formData = new FormData();
formData.append('name', data.name); formData.append('kit_name', data.name); // Сервер ожидает kit_name
formData.append('showcase_id', data.showcaseId); formData.append('showcase_id', data.showcaseId);
formData.append('description', data.description || ''); formData.append('description', data.description || '');
formData.append('showcase_created_at', data.createdAt || ''); formData.append('showcase_created_at', data.createdAt || '');

View File

@@ -157,6 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Глобальные ссылки для обратной совместимости с cart-item-editor.js // Глобальные ссылки для обратной совместимости с cart-item-editor.js
window.cart = cart; window.cart = cart;
window.renderCart = renderCart; window.renderCart = renderCart;
window.showcaseManager = showcaseManager;
}); });
// ===== РЕНДЕРИНГ КАТЕГОРИЙ ===== // ===== РЕНДЕРИНГ КАТЕГОРИЙ =====