Add manual Log Material modal to job details page
PC users were blocked to QR scan only for logging material usage. Now a "Log Material" button opens an inline modal with: - Inventory item dropdown (name + unit of measure, current stock shown on select) - Entry method toggle: "Amount Used" or "Amount Remaining" (computes used = onHand - remaining) - Reason: Job Usage or Waste/Spillage - Notes field Submits via AJAX to Jobs/LogMaterial (new POST action) which mirrors the InventoryController.LogUsage flow — updates QuantityOnHand, creates InventoryTransaction, posts GL entries (DR COGS / CR Inventory). QR scan button retained as icon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Log Material Usage modal — job details page.
|
||||
* Reads config from window.__logMaterial injected inline by the view.
|
||||
*/
|
||||
(function () {
|
||||
let _items = [];
|
||||
let _modal = null;
|
||||
|
||||
function init() {
|
||||
const cfg = window.__logMaterial;
|
||||
if (!cfg) return;
|
||||
|
||||
_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');
|
||||
}
|
||||
lmOnQtyInput();
|
||||
}
|
||||
|
||||
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];
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
window.lmUpdateQuantityLabel = function () {
|
||||
const method = document.querySelector('input[name="lmEntryMethod"]:checked')?.value;
|
||||
document.getElementById('lmQtyLabel').innerHTML =
|
||||
(method === 'remaining' ? 'Quantity Remaining' : 'Quantity Used') +
|
||||
' <span class="text-danger">*</span>';
|
||||
lmOnQtyInput();
|
||||
};
|
||||
|
||||
window.openLogMaterialModal = function () {
|
||||
// Reset form
|
||||
document.getElementById('lmInventoryItem').value = '';
|
||||
document.getElementById('lmItemBalance').classList.add('d-none');
|
||||
document.getElementById('lmQuantity').value = '';
|
||||
document.getElementById('lmComputedUsed').classList.add('d-none');
|
||||
document.getElementById('lmTransactionType').value = 'JobUsage';
|
||||
document.getElementById('lmNotes').value = '';
|
||||
document.getElementById('lmAlert').classList.add('d-none');
|
||||
document.getElementById('lmSaveBtn').disabled = false;
|
||||
document.getElementById('lmMethodUsed').checked = true;
|
||||
window.lmUpdateQuantityLabel();
|
||||
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) {
|
||||
alertEl.className = 'alert alert-danger alert-permanent';
|
||||
alertEl.textContent = msg;
|
||||
alertEl.classList.remove('d-none');
|
||||
}
|
||||
|
||||
if (!itemId) { showError('Please select an inventory item.'); return; }
|
||||
if (qtyInput <= 0) { showError('Please enter a quantity greater than zero.'); return; }
|
||||
|
||||
let quantityUsed = qtyInput;
|
||||
if (method === 'remaining') {
|
||||
const sel = document.getElementById('lmInventoryItem');
|
||||
const onHand = parseFloat(sel.options[sel.selectedIndex]?.dataset.qty) || 0;
|
||||
quantityUsed = onHand - qtyInput;
|
||||
if (quantityUsed <= 0) {
|
||||
showError('Remaining quantity cannot be equal to or greater than the current stock (' + onHand.toFixed(2) + ').');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const btn = document.getElementById('lmSaveBtn');
|
||||
btn.disabled = true;
|
||||
alertEl.classList.add('d-none');
|
||||
|
||||
try {
|
||||
const resp = await fetch(cfg.logUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': cfg.token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jobId: cfg.jobId,
|
||||
inventoryItemId: itemId,
|
||||
quantityUsed: quantityUsed,
|
||||
transactionType: document.getElementById('lmTransactionType').value,
|
||||
notes: document.getElementById('lmNotes').value.trim() || null
|
||||
})
|
||||
});
|
||||
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.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch {
|
||||
showError('Network error. Please try again.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
Reference in New Issue
Block a user