feat: упростить создание заказов и рефакторинг единиц измерения
- Добавить inline-редактирование цен в списке товаров - Оптимизировать карточки товаров в POS-терминале - Рефакторинг моделей единиц измерения - Миграция unit -> base_unit в SalesUnit - Улучшить UI форм создания/редактирования товаров Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -108,21 +108,23 @@ function formatMoney(v) {
|
||||
* - Баланс кошелька в модальном окне продажи (если оно открыто)
|
||||
*/
|
||||
function updateCustomerDisplay() {
|
||||
// Определяем, системный ли это клиент
|
||||
const isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id);
|
||||
const displayName = isSystemCustomer ? 'Выбрать клиента' : selectedCustomer.name;
|
||||
|
||||
// Обновляем текст кнопки в корзине
|
||||
const btnText = document.getElementById('customerSelectBtnText');
|
||||
if (btnText) {
|
||||
btnText.textContent = selectedCustomer.name;
|
||||
btnText.textContent = displayName;
|
||||
}
|
||||
|
||||
// Обновляем текст кнопки в модалке продажи
|
||||
const checkoutBtnText = document.getElementById('checkoutCustomerSelectBtnText');
|
||||
if (checkoutBtnText) {
|
||||
checkoutBtnText.textContent = selectedCustomer.name;
|
||||
checkoutBtnText.textContent = displayName;
|
||||
}
|
||||
|
||||
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
|
||||
// Приводим к числу для надёжного сравнения (JSON может вернуть разные типы)
|
||||
const isSystemCustomer = Number(selectedCustomer.id) === Number(SYSTEM_CUSTOMER.id);
|
||||
|
||||
[document.getElementById('resetCustomerBtn'),
|
||||
document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
|
||||
@@ -450,35 +452,34 @@ async function openProductUnitModal(product) {
|
||||
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} доступно`;
|
||||
stockText = `${availableQty} шт доступно`;
|
||||
} else if (availableQty > 0) {
|
||||
stockBadgeClass = 'stock-badge-low';
|
||||
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
|
||||
stockText = `${availableQty} шт доступно`;
|
||||
}
|
||||
|
||||
|
||||
// Бейдж "По умолчанию"
|
||||
const defaultBadge = unit.is_default ?
|
||||
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>
|
||||
@@ -486,7 +487,7 @@ function renderUnitSelectionList() {
|
||||
<span class="badge ${stockBadgeClass}">${stockText}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
listContainer.appendChild(card);
|
||||
});
|
||||
}
|
||||
@@ -508,9 +509,9 @@ function selectUnit(unit) {
|
||||
});
|
||||
|
||||
// Обновляем отображение выбранной единицы
|
||||
document.getElementById('selectedUnitDisplay').textContent =
|
||||
`${unit.name} (${unit.unit_short_name})`;
|
||||
|
||||
document.getElementById('selectedUnitDisplay').textContent =
|
||||
unit.name;
|
||||
|
||||
// Устанавливаем минимальное количество и шаг
|
||||
const qtyInput = document.getElementById('unitModalQuantity');
|
||||
qtyInput.value = roundQuantity(unit.min_quantity, 3);
|
||||
@@ -626,7 +627,6 @@ function addToCartFromModal() {
|
||||
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
|
||||
});
|
||||
@@ -645,6 +645,54 @@ function addToCartFromModal() {
|
||||
unitModalInstance.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет товар с единицей продажи напрямую в корзину (без модального окна)
|
||||
* Используется для быстрого добавления когда у товара только одна единица продажи
|
||||
* @param {object} product - Товар из ITEMS
|
||||
* @param {object} salesUnit - Единица продажи (default_sales_unit)
|
||||
* @param {number} qty - Количество для добавления
|
||||
*/
|
||||
async function addProductWithUnitToCart(product, salesUnit, qty = 1) {
|
||||
const cartKey = `product-${product.id}-${salesUnit.id}`;
|
||||
|
||||
if (cart.has(cartKey)) {
|
||||
const existing = cart.get(cartKey);
|
||||
existing.qty = roundQuantity(existing.qty + qty, 3);
|
||||
} else {
|
||||
cart.set(cartKey, {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: Number(salesUnit.price),
|
||||
qty: qty,
|
||||
type: 'product',
|
||||
sales_unit_id: salesUnit.id,
|
||||
unit_name: salesUnit.name,
|
||||
quantity_step: parseFloat(salesUnit.quantity_step) || 1
|
||||
});
|
||||
}
|
||||
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
|
||||
// Перерисовываем товары для обновления визуального остатка
|
||||
if (!isShowcaseView) {
|
||||
renderProducts();
|
||||
}
|
||||
|
||||
// Фокус на поле количества
|
||||
setTimeout(() => {
|
||||
const qtyInputs = document.querySelectorAll('.qty-input');
|
||||
const itemIndex = Array.from(cart.keys()).indexOf(cartKey);
|
||||
|
||||
if (itemIndex !== -1 && qtyInputs[itemIndex]) {
|
||||
if (!isMobileDevice()) {
|
||||
qtyInputs[itemIndex].focus();
|
||||
qtyInputs[itemIndex].select();
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function renderCategories() {
|
||||
const grid = document.getElementById('categoryGrid');
|
||||
grid.innerHTML = '';
|
||||
@@ -699,7 +747,7 @@ function renderCategories() {
|
||||
// Категории
|
||||
CATEGORIES.forEach(cat => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-custom-5';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
|
||||
@@ -752,7 +800,7 @@ function renderProducts() {
|
||||
|
||||
filtered.forEach(item => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-custom-5';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card product-card';
|
||||
@@ -852,54 +900,102 @@ function renderProducts() {
|
||||
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;
|
||||
// Используем единицу продажи если есть
|
||||
if (item.default_sales_unit) {
|
||||
const unit = item.default_sales_unit;
|
||||
const conversionFactor = parseFloat(unit.conversion_factor) || 1;
|
||||
|
||||
const free = available - reserved - inCart;
|
||||
const freeRounded = roundQuantity(free, 3); // Округляем для отображения
|
||||
// Вычисляем количество в корзине в единицах продажи
|
||||
const cartKey = `product-${item.id}-${unit.id}`;
|
||||
const inCartBaseQty = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
|
||||
|
||||
// Создаём элементы для стилизации разных размеров
|
||||
const freeSpan = document.createElement('span');
|
||||
freeSpan.textContent = freeRounded; // Используем округлённое значение
|
||||
freeSpan.style.fontSize = '1.1em';
|
||||
freeSpan.style.fontWeight = 'bold';
|
||||
freeSpan.style.fontStyle = 'normal';
|
||||
// Свободное количество в единицах продажи
|
||||
const availableInUnit = parseFloat(item.available_qty_in_unit) || 0;
|
||||
const reservedInUnit = reserved * conversionFactor;
|
||||
const freeInUnit = availableInUnit - reservedInUnit - inCartBaseQty;
|
||||
const freeRounded = roundQuantity(freeInUnit, 1); // Округляем для отображения
|
||||
|
||||
// Отображаем резерв и корзину если они есть
|
||||
const suffixParts = [];
|
||||
if (reserved > 0) {
|
||||
suffixParts.push(`−${roundQuantity(reserved, 3)}`);
|
||||
}
|
||||
if (inCart > 0) {
|
||||
suffixParts.push(`−${roundQuantity(inCart, 3)}🛒`);
|
||||
}
|
||||
// Создаём элементы для стилизации
|
||||
const freeSpan = document.createElement('span');
|
||||
freeSpan.style.fontSize = '1.1em';
|
||||
freeSpan.style.fontWeight = 'bold';
|
||||
|
||||
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';
|
||||
const qtyText = document.createElement('span');
|
||||
qtyText.textContent = freeRounded;
|
||||
freeSpan.appendChild(qtyText);
|
||||
|
||||
stock.appendChild(freeSpan);
|
||||
stock.appendChild(suffixSpan);
|
||||
const unitBadge = document.createElement('span');
|
||||
unitBadge.className = 'badge bg-secondary ms-1';
|
||||
unitBadge.style.fontSize = '0.7rem';
|
||||
unitBadge.textContent = unit.name;
|
||||
freeSpan.appendChild(unitBadge);
|
||||
|
||||
// Отображаем корзину если есть
|
||||
if (inCartBaseQty > 0) {
|
||||
const suffixSpan = document.createElement('span');
|
||||
suffixSpan.textContent = ` (−${roundQuantity(inCartBaseQty, 1)}🛒)`;
|
||||
suffixSpan.style.fontSize = '0.85em';
|
||||
suffixSpan.style.marginLeft = '3px';
|
||||
|
||||
stock.appendChild(freeSpan);
|
||||
stock.appendChild(suffixSpan);
|
||||
} else {
|
||||
stock.appendChild(freeSpan);
|
||||
}
|
||||
|
||||
// Цветовая индикация
|
||||
if (freeInUnit <= 0) {
|
||||
stock.style.color = '#dc3545'; // Красный
|
||||
} else if (freeInUnit < 5) {
|
||||
stock.style.color = '#ffc107'; // Жёлтый
|
||||
} else {
|
||||
stock.style.color = '#28a745'; // Зелёный
|
||||
}
|
||||
} else {
|
||||
stock.appendChild(freeSpan);
|
||||
}
|
||||
// Отображение в базовых единицах (старая логика)
|
||||
const cartKey = `product-${item.id}`;
|
||||
const inCart = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
|
||||
|
||||
// Цветовая индикация: красный если свободных остатков нет или отрицательные
|
||||
if (free <= 0) {
|
||||
stock.style.color = '#dc3545'; // Красный
|
||||
} else if (free < 5) {
|
||||
stock.style.color = '#ffc107'; // Жёлтый (мало остатков)
|
||||
} else {
|
||||
stock.style.color = '#28a745'; // Зелёный (достаточно)
|
||||
const free = available - reserved - inCart;
|
||||
const freeRounded = roundQuantity(free, 3);
|
||||
|
||||
const freeSpan = document.createElement('span');
|
||||
freeSpan.textContent = freeRounded;
|
||||
freeSpan.style.fontSize = '1.1em';
|
||||
freeSpan.style.fontWeight = 'bold';
|
||||
|
||||
const suffixParts = [];
|
||||
if (reserved > 0) {
|
||||
suffixParts.push(`−${roundQuantity(reserved, 3)}`);
|
||||
}
|
||||
if (inCart > 0) {
|
||||
suffixParts.push(`−${roundQuantity(inCart, 3)}🛒`);
|
||||
}
|
||||
|
||||
if (suffixParts.length > 0) {
|
||||
const suffixSpan = document.createElement('span');
|
||||
suffixSpan.textContent = `(${suffixParts.join(' ')})`;
|
||||
suffixSpan.style.fontSize = '0.85em';
|
||||
suffixSpan.style.marginLeft = '3px';
|
||||
|
||||
stock.appendChild(freeSpan);
|
||||
stock.appendChild(suffixSpan);
|
||||
} else {
|
||||
stock.appendChild(freeSpan);
|
||||
}
|
||||
|
||||
if (free <= 0) {
|
||||
stock.style.color = '#dc3545';
|
||||
} else if (free < 5) {
|
||||
stock.style.color = '#ffc107';
|
||||
} else {
|
||||
stock.style.color = '#28a745';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Комплекты: показываем доступное количество
|
||||
@@ -929,7 +1025,9 @@ function renderProducts() {
|
||||
|
||||
const priceSpan = document.createElement('span');
|
||||
priceSpan.className = 'product-price';
|
||||
priceSpan.textContent = `${formatMoney(item.price)}`;
|
||||
// Используем цену из единицы продажи если есть, иначе базовую цену
|
||||
const itemPrice = item.default_sales_unit ? item.price_in_unit : item.price;
|
||||
priceSpan.textContent = `${formatMoney(itemPrice)}`;
|
||||
|
||||
sku.appendChild(skuText);
|
||||
sku.appendChild(priceSpan);
|
||||
@@ -1022,9 +1120,18 @@ function setupInfiniteScroll() {
|
||||
}
|
||||
|
||||
async function addToCart(item) {
|
||||
// ПРОВЕРКА НА НАЛИЧИЕ НЕСКОЛЬКИХ ЕДИНИЦ ПРОДАЖИ
|
||||
if (item.type === 'product' && item.sales_units_count > 1) {
|
||||
// Открываем модальное окно выбора единицы
|
||||
// ПРОВЕРКА НА НАЛИЧИЕ ЕДИНИЦ ПРОДАЖИ
|
||||
// Если у товара одна единица продажи - добавляем сразу
|
||||
// Если несколько - показываем модальное окно выбора
|
||||
console.log('addToCart:', item.name, 'has_sales_units:', item.has_sales_units, 'sales_units_count:', item.sales_units_count);
|
||||
|
||||
if (item.type === 'product' && item.has_sales_units) {
|
||||
// Если одна единица продажи - добавляем сразу
|
||||
if (item.sales_units_count === 1 && item.default_sales_unit) {
|
||||
await addProductWithUnitToCart(item, item.default_sales_unit, 1);
|
||||
return;
|
||||
}
|
||||
// Иначе открываем модальное окно выбора единицы
|
||||
await openProductUnitModal(item);
|
||||
return;
|
||||
}
|
||||
@@ -1187,8 +1294,10 @@ function renderCart() {
|
||||
}
|
||||
|
||||
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>
|
||||
<div class="fw-semibold small">${typeIcon}${item.name}</div>
|
||||
<div class="price-unit-row">
|
||||
<span class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)}</span>${unitInfo}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Знак умножения
|
||||
|
||||
Reference in New Issue
Block a user