diff --git a/src/PowderCoating.Web/Views/Jobs/Details.cshtml b/src/PowderCoating.Web/Views/Jobs/Details.cshtml
index 49b0b17..58da562 100644
--- a/src/PowderCoating.Web/Views/Jobs/Details.cshtml
+++ b/src/PowderCoating.Web/Views/Jobs/Details.cshtml
@@ -1105,9 +1105,22 @@
-
+
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);
})();