From 6cf355071b979bdb4238e386873181ad234891af Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Sat, 16 May 2026 21:41:14 -0400 Subject: [PATCH] Replace Log Material item dropdown with searchable combobox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inventory lists grow over time; a plain - - +
+
+ + +
+ +
diff --git a/src/PowderCoating.Web/wwwroot/js/log-material.js b/src/PowderCoating.Web/wwwroot/js/log-material.js index 6169cd4..7cc506a 100644 --- a/src/PowderCoating.Web/wwwroot/js/log-material.js +++ b/src/PowderCoating.Web/wwwroot/js/log-material.js @@ -6,60 +6,148 @@ let _items = []; let _modal = null; - function init() { - const cfg = window.__logMaterial; - if (!cfg) return; + // ── Combobox state ──────────────────────────────────────────────────────── + let _selectedItemId = 0; - _items = cfg.inventoryItems || []; - _modal = new bootstrap.Modal(document.getElementById('logMaterialModal')); - - const sel = document.getElementById('lmInventoryItem'); - _items.forEach(function (item) { - const opt = document.createElement('option'); - opt.value = item.id; - opt.textContent = item.name + (item.unitOfMeasure ? ' (' + item.unitOfMeasure + ')' : ''); - opt.dataset.qty = item.quantityOnHand; - opt.dataset.uom = item.unitOfMeasure || ''; - sel.appendChild(opt); - }); - - sel.addEventListener('change', lmOnItemChange); - document.getElementById('lmQuantity').addEventListener('input', lmOnQtyInput); - } - - function lmOnItemChange() { - const sel = document.getElementById('lmInventoryItem'); - const opt = sel.options[sel.selectedIndex]; - const balDiv = document.getElementById('lmItemBalance'); - if (sel.value && opt) { - const qty = parseFloat(opt.dataset.qty) || 0; - const uom = opt.dataset.uom; - balDiv.textContent = 'Current stock: ' + qty.toFixed(2) + (uom ? ' ' + uom : ''); - balDiv.classList.remove('d-none'); - } else { - balDiv.classList.add('d-none'); - } + function lmComboInput() { + const q = document.getElementById('lmItemSearch')?.value?.toLowerCase() || ''; + lmComboRender(q); + lmComboShow(); + _selectedItemId = 0; + document.getElementById('lmItemBalance').classList.add('d-none'); lmOnQtyInput(); } + function lmComboOpen() { + const q = document.getElementById('lmItemSearch')?.value?.toLowerCase() || ''; + lmComboRender(q); + lmComboShow(); + } + + function lmComboToggle() { + const dd = document.getElementById('lmItemDropdown'); + if (!dd) return; + if (dd.style.display === 'none' || !dd.style.display) { + lmComboOpen(); + document.getElementById('lmItemSearch')?.focus(); + } else { + lmComboClose(); + } + } + + function lmComboRender(query) { + const dd = document.getElementById('lmItemDropdown'); + if (!dd) return; + const filtered = query + ? _items.filter(it => it.name.toLowerCase().includes(query) || + (it.unitOfMeasure && it.unitOfMeasure.toLowerCase().includes(query))) + : _items; + if (filtered.length === 0) { + dd.innerHTML = '
No items match.
'; + return; + } + dd.innerHTML = filtered.map(it => { + const label = it.name + (it.unitOfMeasure ? ' (' + it.unitOfMeasure + ')' : ''); + return `
+ ${escLm(label)} +
`; + }).join(''); + } + + function lmComboShow() { + const dd = document.getElementById('lmItemDropdown'); + const anchor = document.getElementById('lmItemSearch'); + if (!dd || !anchor) return; + const rect = anchor.closest('.input-group').getBoundingClientRect(); + dd.style.position = 'fixed'; + dd.style.top = (rect.bottom + 2) + 'px'; + dd.style.left = rect.left + 'px'; + dd.style.width = rect.width + 'px'; + dd.style.display = 'block'; + } + + function lmComboClose() { + const dd = document.getElementById('lmItemDropdown'); + if (dd) dd.style.display = 'none'; + } + + window.lmComboSelect = function (el) { + _selectedItemId = parseInt(el.dataset.id) || 0; + document.getElementById('lmItemSearch').value = el.dataset.label; + lmComboClose(); + + const qty = parseFloat(el.dataset.qty) || 0; + const uom = el.dataset.uom; + const balDiv = document.getElementById('lmItemBalance'); + balDiv.textContent = 'Current stock: ' + qty.toFixed(2) + (uom ? ' ' + uom : ''); + balDiv.classList.remove('d-none'); + lmOnQtyInput(); + }; + + window.lmComboInput = lmComboInput; + window.lmComboOpen = lmComboOpen; + window.lmComboToggle = lmComboToggle; + + window.lmComboKey = function (event) { + const dd = document.getElementById('lmItemDropdown'); + if (!dd || dd.style.display === 'none') { + if (event.key === 'ArrowDown' || event.key === 'Enter') { + event.preventDefault(); + lmComboOpen(); + } + return; + } + const opts = Array.from(dd.querySelectorAll('.lm-item-opt')); + let idx = opts.findIndex(o => o.classList.contains('lm-active')); + if (event.key === 'ArrowDown') { + event.preventDefault(); + idx = Math.min(idx + 1, opts.length - 1); + opts.forEach(o => { o.classList.remove('lm-active'); o.style.background = ''; }); + if (opts[idx]) { opts[idx].classList.add('lm-active'); opts[idx].style.background = '#e8eeff'; opts[idx].scrollIntoView({ block: 'nearest' }); } + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + idx = Math.max(idx - 1, 0); + opts.forEach(o => { o.classList.remove('lm-active'); o.style.background = ''; }); + if (opts[idx]) { opts[idx].classList.add('lm-active'); opts[idx].style.background = '#e8eeff'; opts[idx].scrollIntoView({ block: 'nearest' }); } + } else if (event.key === 'Enter') { + event.preventDefault(); + const active = dd.querySelector('.lm-active') || opts[0]; + if (active) active.dispatchEvent(new MouseEvent('mousedown')); + } else if (event.key === 'Escape') { + lmComboClose(); + } + }; + + function escLm(s) { + return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + // ── Quantity / label logic ──────────────────────────────────────────────── + function lmOnQtyInput() { const method = document.querySelector('input[name="lmEntryMethod"]:checked')?.value; if (method !== 'remaining') { document.getElementById('lmComputedUsed').classList.add('d-none'); return; } - const sel = document.getElementById('lmInventoryItem'); - const opt = sel.options[sel.selectedIndex]; + if (!_selectedItemId) { + document.getElementById('lmComputedUsed').classList.add('d-none'); + return; + } + const item = _items.find(it => it.id === _selectedItemId); + const onHand = item ? (parseFloat(item.quantityOnHand) || 0) : 0; const remaining = parseFloat(document.getElementById('lmQuantity').value) || 0; - const onHand = parseFloat(opt?.dataset.qty) || 0; const used = onHand - remaining; const computedDiv = document.getElementById('lmComputedUsed'); - if (sel.value) { - computedDiv.textContent = 'Usage = ' + onHand.toFixed(2) + ' − ' + remaining.toFixed(2) + ' = ' + used.toFixed(2) + ' ' + (opt?.dataset.uom || ''); - computedDiv.classList.remove('d-none'); - } else { - computedDiv.classList.add('d-none'); - } + computedDiv.textContent = 'Usage = ' + onHand.toFixed(2) + ' − ' + remaining.toFixed(2) + ' = ' + used.toFixed(2) + (item?.unitOfMeasure ? ' ' + item.unitOfMeasure : ''); + computedDiv.classList.remove('d-none'); } window.lmUpdateQuantityLabel = function () { @@ -70,9 +158,11 @@ lmOnQtyInput(); }; + // ── Modal open / save ───────────────────────────────────────────────────── + window.openLogMaterialModal = function () { - // Reset form - document.getElementById('lmInventoryItem').value = ''; + _selectedItemId = 0; + document.getElementById('lmItemSearch').value = ''; document.getElementById('lmItemBalance').classList.add('d-none'); document.getElementById('lmQuantity').value = ''; document.getElementById('lmComputedUsed').classList.add('d-none'); @@ -82,14 +172,12 @@ document.getElementById('lmSaveBtn').disabled = false; document.getElementById('lmMethodUsed').checked = true; window.lmUpdateQuantityLabel(); + lmComboClose(); if (_modal) _modal.show(); }; window.lmSave = async function () { const cfg = window.__logMaterial; - const itemId = parseInt(document.getElementById('lmInventoryItem').value) || 0; - const qtyInput = parseFloat(document.getElementById('lmQuantity').value) || 0; - const method = document.querySelector('input[name="lmEntryMethod"]:checked')?.value; const alertEl = document.getElementById('lmAlert'); function showError(msg) { @@ -98,13 +186,16 @@ alertEl.classList.remove('d-none'); } - if (!itemId) { showError('Please select an inventory item.'); return; } + if (!_selectedItemId) { showError('Please select an inventory item.'); return; } + + const qtyInput = parseFloat(document.getElementById('lmQuantity').value) || 0; if (qtyInput <= 0) { showError('Please enter a quantity greater than zero.'); return; } + const method = document.querySelector('input[name="lmEntryMethod"]:checked')?.value; let quantityUsed = qtyInput; if (method === 'remaining') { - const sel = document.getElementById('lmInventoryItem'); - const onHand = parseFloat(sel.options[sel.selectedIndex]?.dataset.qty) || 0; + const item = _items.find(it => it.id === _selectedItemId); + const onHand = item ? (parseFloat(item.quantityOnHand) || 0) : 0; quantityUsed = onHand - qtyInput; if (quantityUsed <= 0) { showError('Remaining quantity cannot be equal to or greater than the current stock (' + onHand.toFixed(2) + ').'); @@ -125,7 +216,7 @@ }, body: JSON.stringify({ jobId: cfg.jobId, - inventoryItemId: itemId, + inventoryItemId: _selectedItemId, quantityUsed: quantityUsed, transactionType: document.getElementById('lmTransactionType').value, notes: document.getElementById('lmNotes').value.trim() || null @@ -134,7 +225,6 @@ const data = await resp.json(); if (data.success) { if (_modal) _modal.hide(); - // Reload page so the materials table refreshes window.location.reload(); } else { showError(data.message || 'An error occurred.'); @@ -146,5 +236,26 @@ } }; + // ── Init ────────────────────────────────────────────────────────────────── + + function init() { + const cfg = window.__logMaterial; + if (!cfg) return; + + _items = cfg.inventoryItems || []; + _modal = new bootstrap.Modal(document.getElementById('logMaterialModal')); + + document.getElementById('lmQuantity').addEventListener('input', lmOnQtyInput); + + // Close dropdown when clicking outside + document.addEventListener('click', function (e) { + if (!e.target.closest('#lmItemSearch') && + !e.target.closest('#lmItemDropdown') && + !e.target.closest('#lmItemDropdownToggle')) { + lmComboClose(); + } + }); + } + document.addEventListener('DOMContentLoaded', init); })();