Реализована возможность редактирования состава витринного комплекта с поддержкой отрицательных резервов

- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager
- Методы работают с резервами для всех активных экземпляров витринного комплекта
- НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft)
- Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава
- Добавлено визуальное предупреждение на фронте о нехватке товара на складе
- В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление)
- Автоматический пересчёт цен при изменении состава
- Очистка корзины POS после успешного создания витринного комплекта
This commit is contained in:
2025-12-14 13:49:13 +03:00
parent 835d6020e2
commit aff25d0317
3 changed files with 292 additions and 21 deletions

View File

@@ -1440,22 +1440,92 @@ function renderTempKitItems() {
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>
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);
}
@@ -1657,7 +1727,8 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
// Успех!
const createdCount = data.available_count || 1;
const qtyInfo = createdCount > 1 ? `\nСоздано экземпляров: ${createdCount}` : '';
const successMessage = isEditMode
let successMessage = isEditMode
? `${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
: `${data.message}
@@ -1665,6 +1736,15 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
Цена: ${data.kit_price} руб.${qtyInfo}
Зарезервировано компонентов: ${data.reservations_count}`;
// Если есть предупреждение о нехватке товара - добавляем его
if (data.stock_warning && data.stock_warnings && data.stock_warnings.length > 0) {
successMessage += '\n\n⚠ ВНИМАНИЕ: Нехватка товара на складе!\n';
data.stock_warnings.forEach(warning => {
successMessage += `\n${warning.product_name}: не хватает ${warning.overdraft} ед.`;
});
successMessage += '\n\nПроверьте остатки и пополните склад.';
}
alert(successMessage);
// Очищаем tempCart (изолированное состояние модалки)
@@ -1682,6 +1762,9 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
document.getElementById('salePriceBlock').style.display = 'none';
document.getElementById('showcaseKitQuantity').value = '1'; // Сброс количества
// Запоминаем, был ли режим редактирования до сброса
const wasEditMode = isEditMode;
// Сбрасываем режим редактирования
isEditMode = false;
editingKitId = null;
@@ -1690,6 +1773,12 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
modal.hide();
// Если это было СОЗДАНИЕ витринного комплекта из корзины,
// очищаем основную корзину POS
if (!wasEditMode) {
await clearCart();
}
// Обновляем витринные комплекты и переключаемся на вид витрины
isShowcaseView = true;
currentCategoryId = null;