@@ -4,62 +4,173 @@
* /
( function ( ) {
let _items = [ ] ;
let _jobPowderIds = new Set ( ) ;
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 ( 'opti on' ) ;
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-n one ') ;
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 lmMakeRow ( it ) {
const display = ( it . manufacturer ? escLm ( it . manufacturer ) + ' – ' : '' ) +
escLm ( it . name ) +
( it . unitOfMeasure ? ' <span class="text-muted" style="font-size:.82rem;">(' + escLm ( it . unitOfMeasure ) + ')</span>' : '' ) ;
const label = ( it . manufacturer ? it . manufacturer + ' - ' : '' ) +
it . name +
( it . unitOfMeasure ? ' (' + it . unitOfMeasure + ')' : '' ) ;
return ` <div class="lm-item-opt" style="padding:.35rem .75rem;font-size:.875rem;cursor:pointer;"
data - id = "${it.id}"
data - qty = "${it.quantityOnHand}"
data - uom = "${escLm(it.unitOfMeasure || '')}"
data - label = "${escLm(label)}"
onmousedown = "event.preventDefault(); lmComboSelect(this)"
onmouseenter = "this.style.background='#f0f4ff'"
onmouseleave = "this.classList.contains('lm-active') ? null : this.style.background=''" >
$ { display }
< / d i v > ` ;
}
function lmComboRender ( query ) {
const dd = document . getElementById ( 'lmItemDropdown' ) ;
if ( ! dd ) return ;
const filtered = query
? _items . filter ( it => it . name . toLowerCase ( ) . includes ( query ) ||
( it . manufacturer && it . manufacturer . toLowerCase ( ) . includes ( query ) ) ||
( it . unitOfMeasure && it . unitOfMeasure . toLowerCase ( ) . includes ( query ) ) )
: _items ;
if ( filtered . length === 0 ) {
dd . innerHTML = '<div class="px-3 py-2 text-muted small">No items match.</div>' ;
return ;
}
const jobItems = filtered . filter ( it => _jobPowderIds . has ( it . id ) ) ;
const otherItems = filtered . filter ( it => ! _jobPowderIds . has ( it . id ) ) ;
let html = '' ;
if ( jobItems . length > 0 ) {
html += '<div class="px-3 py-1 text-muted" style="font-size:.72rem;letter-spacing:.04em;text-transform:uppercase;background:#f8f9fa;border-bottom:1px solid #dee2e6;">This Job</div>' ;
html += jobItems . map ( lmMakeRow ) . join ( '' ) ;
if ( otherItems . length > 0 ) {
html += '<div style="height:1px;background:#dee2e6;margin:.25rem 0;"></div>' ;
html += '<div class="px-3 py-1 text-muted" style="font-size:.72rem;letter-spacing:.04em;text-transform:uppercase;background:#f8f9fa;border-bottom:1px solid #dee2e6;">All Inventory</div>' ;
}
}
html += otherItems . map ( lmMakeRow ) . join ( '' ) ;
dd . innerHTML = html ;
}
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 , '>' ) . 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 +181,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 +195,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 +209,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 +239,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 +248,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 +259,27 @@
}
} ;
// ── Init ──────────────────────────────────────────────────────────────────
function init ( ) {
const cfg = window . _ _logMaterial ;
if ( ! cfg ) return ;
_items = cfg . inventoryItems || [ ] ;
_jobPowderIds = new Set ( cfg . jobPowderIds || [ ] ) ;
_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 ) ;
} ) ( ) ;