diff --git a/public/app.js b/public/app.js index 256d506..75338f4 100644 --- a/public/app.js +++ b/public/app.js @@ -121,7 +121,7 @@ class OrderCard { idCell.textContent = item.product_id ?? item.id; const productCell = document.createElement('td'); - const productSelect = this.buildProductSelect(item); + const productSelect = this.buildProductSelect(item.product_id); productSelect.classList.add('form-select-sm'); productSelect.dataset.action = 'item-product'; productSelect.dataset.itemId = item.id; @@ -220,7 +220,12 @@ class OrderCard { const productLabel = document.createElement('label'); productLabel.className = 'form-label'; productLabel.textContent = 'Товар'; - const productSelect = this.buildProductSelect(); + const usedProductIds = new Set( + (this.order.items || []) + .map((item) => item.product_id) + .filter((value) => value !== null && value !== undefined) + ); + const productSelect = this.buildProductSelect(undefined, usedProductIds); const productSelectId = `add-item-product-${this.order.id}`; productSelect.name = 'productId'; productSelect.id = productSelectId; @@ -253,6 +258,16 @@ class OrderCard { addButton.textContent = 'Добавить позицию'; actionCol.appendChild(addButton); + if (productSelect.options.length === 0) { + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = 'Нет доступных позиций'; + productSelect.appendChild(placeholder); + productSelect.disabled = true; + quantityInput.disabled = true; + addButton.disabled = true; + } + form.appendChild(productCol); form.appendChild(quantityCol); form.appendChild(actionCol); @@ -260,10 +275,13 @@ class OrderCard { return form; } - buildProductSelect(selectedId) { + buildProductSelect(selectedId, excludedIds) { const select = document.createElement('select'); select.className = 'form-select'; this.products.forEach((product) => { + if (excludedIds && excludedIds.has(product.id)) { + return; + } const option = document.createElement('option'); option.value = product.id; option.textContent = product.name; @@ -313,7 +331,7 @@ async function fetchJson(url, options) { const payload = await response.json().catch(() => ({ message: 'Ошибка запроса.' })); - throw new Error(payload.message || 'Ошибка запроса.'); + throw new Error(payload.error || payload.message || 'Error'); } if (response.status === 204) { return null; @@ -549,18 +567,24 @@ function initializeApp() { `[data-action="item-product"][data-item-id="${itemId}"]` ); - await fetchJson(`/api/orders/${orderId}/items/${itemId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - productId: productSelect.value, - quantity: Number.parseInt(quantityInput.value, 10) - }) - }); - await refreshData(); - return; + try { + await fetchJson(`/api/orders/${orderId}/items/${itemId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + productId: productSelect.value, + quantity: Number.parseInt(quantityInput.value, 10) + }) + }); + await refreshData(); + return; + } catch (error) { + showNotification(error.message, 'warning'); + await refreshData(); + return; + } } case 'delete-item': { await fetchJson(`/api/orders/${orderId}/items/${itemId}`, { diff --git a/server/db/database.js b/server/db/database.js index 5d11592..066b0cb 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -384,10 +384,17 @@ export default class DBAdapter { } } - async updateOrderItem({ itemId, quantity }) { + async updateOrderItem({ itemId, quantity, productId }) { let connection; try { + if (!itemId || !Number.isFinite(quantity) || quantity <= 0) { + return Promise.reject({ + type: DB_USER_ERROR, + error: new Error("Invalid item or quantity") + }); + } + connection = await this.#pool.getConnection(); await connection.beginTransaction(); @@ -406,6 +413,51 @@ export default class DBAdapter { }); } + const targetProductId = productId ?? itemRow.product_id; + + if (targetProductId !== itemRow.product_id) { + const newProduct = await connection.query( + 'SELECT quantity FROM products WHERE id = ? FOR UPDATE', + [targetProductId] + ); + const newProductRow = newProduct?.[0]; + if (!newProductRow) { + await connection.rollback(); + connection.release(); + return Promise.reject({ + type: DB_USER_ERROR, + error: new Error("Product not found") + }); + } + if (newProductRow.quantity < quantity) { + await connection.rollback(); + connection.release(); + return Promise.reject({ + type: DB_USER_ERROR, + error: new Error("Insufficient product quantity") + }); + } + + await connection.query( + 'UPDATE products SET quantity = quantity + ? WHERE id = ?', + [itemRow.quantity, itemRow.product_id] + ); + + await connection.query( + 'UPDATE products SET quantity = quantity - ? WHERE id = ?', + [quantity, targetProductId] + ); + + await connection.query( + 'UPDATE order_items SET product_id = ?, quantity = ? WHERE id = ?', + [targetProductId, quantity, itemId] + ); + + await connection.commit(); + connection.release(); + return; + } + const diff = quantity - itemRow.quantity; if (diff > 0) { diff --git a/server/server.js b/server/server.js index bdf7ee8..4309347 100644 --- a/server/server.js +++ b/server/server.js @@ -312,24 +312,28 @@ app.post('/api/orders/:orderId/items', async (req, res) => { app.put('/api/orders/:orderId/items/:itemId', async (req, res) => { let itemId = req.params.itemId; - let quantity = req.body.quantity; + let { quantity, productId } = req.body; + quantity = Number.parseInt(quantity, 10); try { await adapter.updateOrderItem({ itemId, + productId, quantity }); res.status(201).json({ itemId, + productId, quantity }); } catch (err) { - res.json({ + const statusCode = err.type === DB_USER_ERROR ? 400 : 500; + res.status(statusCode).json({ timeStamp: new Date().toISOString(), - statusCode: 500, - error: err.message + statusCode, + error: err.error?.message || err.message }); } });