diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index 880374b..9437e25 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -1642,8 +1642,10 @@ public class InventoryController : Controller var userId = _userManager.GetUserId(User); + var recentCutoff = DateTime.UtcNow.AddDays(-7); + var myJobs = (await _unitOfWork.Jobs.FindAsync( - j => !j.JobStatus.IsTerminalStatus && j.AssignedUserId == userId, + j => (!j.JobStatus.IsTerminalStatus || j.UpdatedAt >= recentCutoff) && j.AssignedUserId == userId, false, j => j.Customer, j => j.JobStatus)) @@ -1651,7 +1653,7 @@ public class InventoryController : Controller .Select(j => new ScanJobOption { Id = j.Id, - JobNumber = j.JobNumber, + JobNumber = j.JobNumber + (j.JobStatus.IsTerminalStatus ? " (completed)" : ""), CustomerName = j.Customer != null ? (j.Customer.CompanyName ?? j.Customer.ContactFirstName + " " + j.Customer.ContactLastName) : "No Customer" @@ -1660,7 +1662,7 @@ public class InventoryController : Controller var myJobIds = myJobs.Select(j => j.Id).ToHashSet(); var otherJobs = (await _unitOfWork.Jobs.FindAsync( - j => !j.JobStatus.IsTerminalStatus && !myJobIds.Contains(j.Id), + j => (!j.JobStatus.IsTerminalStatus || j.UpdatedAt >= recentCutoff) && !myJobIds.Contains(j.Id), false, j => j.Customer, j => j.JobStatus)) @@ -1669,7 +1671,7 @@ public class InventoryController : Controller .Select(j => new ScanJobOption { Id = j.Id, - JobNumber = j.JobNumber, + JobNumber = j.JobNumber + (j.JobStatus.IsTerminalStatus ? " (completed)" : ""), CustomerName = j.Customer != null ? (j.Customer.CompanyName ?? j.Customer.ContactFirstName + " " + j.Customer.ContactLastName) : "No Customer" @@ -1686,9 +1688,64 @@ public class InventoryController : Controller } /// - /// Records powder usage logged via the mobile scan page. Creates a JobUsage - /// InventoryTransaction (and PowderUsageLog) when a job is selected, or an - /// Adjustment transaction when logging without a job. Updates QuantityOnHand. + /// Core inventory usage recording logic shared by LogUsage (scan page) and LogMaterial (modal). + /// Deducts quantityUsed from QuantityOnHand, writes an InventoryTransaction, and posts GL entries. + /// + private async Task RecordInventoryUsageAsync( + int inventoryItemId, int? jobId, decimal quantityUsed, + InventoryTransactionType transactionType, string? notes) + { + var item = await _unitOfWork.InventoryItems.GetByIdAsync(inventoryItemId); + if (item == null) + return new InventoryUsageResult(false, "Inventory item not found.", 0, "", ""); + + string? reference = null; + if (jobId.HasValue) + { + var job = await _unitOfWork.Jobs.GetByIdAsync(jobId.Value); + reference = job != null ? $"Job {job.JobNumber}" : null; + } + + item.QuantityOnHand -= quantityUsed; + item.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.InventoryItems.UpdateAsync(item); + + var txn = new InventoryTransaction + { + InventoryItemId = item.Id, + TransactionType = transactionType, + Quantity = -quantityUsed, + UnitCost = item.UnitCost, + TotalCost = quantityUsed * item.UnitCost, + TransactionDate = DateTime.UtcNow, + BalanceAfter = item.QuantityOnHand, + JobId = jobId, + Reference = reference, + Notes = notes?.Trim(), + CompanyId = item.CompanyId, + CreatedAt = DateTime.UtcNow + }; + await _unitOfWork.InventoryTransactions.AddAsync(txn); + await _unitOfWork.CompleteAsync(); + + if (item.CogsAccountId.HasValue && item.InventoryAccountId.HasValue) + { + var cost = quantityUsed * (item.AverageCost > 0 ? item.AverageCost : item.UnitCost); + await _accountBalanceService.DebitAsync(item.CogsAccountId, cost); + await _accountBalanceService.CreditAsync(item.InventoryAccountId, cost); + } + + return new InventoryUsageResult( + true, + $"Logged {quantityUsed:N2} {item.UnitOfMeasure} of {item.Name}. New balance: {item.QuantityOnHand:N2} {item.UnitOfMeasure}.", + item.QuantityOnHand, + item.UnitOfMeasure, + item.Name); + } + + /// + /// Records powder usage from the mobile scan page. Resolves the used quantity + /// (caller already converts "remaining weight" to delta before posting) and redirects to ScanSuccess. /// [HttpPost] [ValidateAntiForgeryToken] @@ -1697,55 +1754,26 @@ public class InventoryController : Controller { try { - var item = await _unitOfWork.InventoryItems.GetByIdAsync(inventoryItemId); - if (item == null) return NotFound(); - if (quantity <= 0) { TempData["ScanError"] = "Quantity must be greater than zero."; return RedirectToAction(nameof(Scan), new { id = inventoryItemId }); } - var userId = _userManager.GetUserId(User) ?? string.Empty; - // Scan-based logging always records as JobUsage; Adjustment is for manual stock corrections only - var txnType = InventoryTransactionType.JobUsage; + var result = await RecordInventoryUsageAsync( + inventoryItemId, jobId, quantity, + InventoryTransactionType.JobUsage, notes); - item.QuantityOnHand -= quantity; - item.UpdatedAt = DateTime.UtcNow; - await _unitOfWork.InventoryItems.UpdateAsync(item); - - var txn = new InventoryTransaction + if (!result.Success) { - InventoryItemId = item.Id, - TransactionType = txnType, - Quantity = -quantity, - UnitCost = item.UnitCost, - TotalCost = quantity * item.UnitCost, - TransactionDate = DateTime.UtcNow, - BalanceAfter = item.QuantityOnHand, - JobId = jobId, - Reference = jobId.HasValue ? $"Job #{jobId}" : null, - Notes = notes?.Trim() - }; - await _unitOfWork.InventoryTransactions.AddAsync(txn); - await _unitOfWork.SaveChangesAsync(); - - // GL: DR COGS, CR Inventory Asset — no-op if accounts not configured on the item - if (item.CogsAccountId.HasValue && item.InventoryAccountId.HasValue) - { - var cost = quantity * (item.AverageCost > 0 ? item.AverageCost : item.UnitCost); - await _accountBalanceService.DebitAsync(item.CogsAccountId, cost); - await _accountBalanceService.CreditAsync(item.InventoryAccountId, cost); + TempData["ScanError"] = result.Message; + return RedirectToAction(nameof(Scan), new { id = inventoryItemId }); } - // PowderUsageLog requires a specific JobItem + Coat FK — scan-based logging - // doesn't have that context, so we rely on the InventoryTransaction alone - // for the audit trail. Coat-level PowderUsageLogs are created by the job workflow. - - TempData["ScanSuccess"] = $"Logged {quantity:N2} {item.UnitOfMeasure} of {item.Name}. New balance: {item.QuantityOnHand:N2} {item.UnitOfMeasure}."; + TempData["ScanSuccess"] = result.Message; TempData["ScanItemId"] = inventoryItemId.ToString(); TempData["ScanJobId"] = jobId?.ToString(); - TempData["ScanItemName"] = item.Name; + TempData["ScanItemName"] = result.ItemName; return RedirectToAction(nameof(ScanSuccess)); } catch (Exception ex) @@ -1756,6 +1784,43 @@ public class InventoryController : Controller } } + /// + /// Records manual material usage from the job details modal. Accepts JSON, resolves + /// the amount used (caller sends the already-computed used quantity), and returns JSON + /// so the modal can close and refresh inline. + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task LogMaterial([FromBody] LogMaterialRequest req) + { + try + { + if (req.QuantityUsed <= 0) + return Json(new { success = false, message = "Quantity used must be greater than zero." }); + + var txnType = req.TransactionType == "Waste" + ? InventoryTransactionType.Waste + : InventoryTransactionType.JobUsage; + + var result = await RecordInventoryUsageAsync( + req.InventoryItemId, req.JobId, req.QuantityUsed, txnType, req.Notes); + + return Json(new + { + success = result.Success, + message = result.Message, + newBalance = result.NewBalance, + unitOfMeasure = result.UnitOfMeasure, + itemName = result.ItemName + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error logging material for job {JobId}", req.JobId); + return Json(new { success = false, message = "An error occurred. Please try again." }); + } + } + /// /// Success screen shown after a usage log is saved. Offers "Log Another Item for /// This Job" and "Done" options. @@ -2003,7 +2068,7 @@ public class InventoryController : Controller /// /// Returns the current values of a JobUsage InventoryTransaction plus a list of active - /// jobs so the edit modal can be pre-populated without a full page reload. + /// jobs (plus the currently assigned job even if terminal) for the edit modal. /// [HttpGet] public async Task GetUsageForEdit(int id) @@ -2034,10 +2099,27 @@ public class InventoryController : Controller }) .ToList(); + // If the assigned job has terminal status it won't appear in the active list; insert it at the top + // so the dropdown pre-selects correctly and the user can see the existing job assignment. + if (txn.JobId.HasValue && jobs.All(j => j.Id != txn.JobId.Value)) + { + var assignedJob = await _unitOfWork.Jobs.GetByIdAsync(txn.JobId.Value, false, j => j.Customer); + if (assignedJob != null) + jobs.Insert(0, new ScanJobOption + { + Id = assignedJob.Id, + JobNumber = assignedJob.JobNumber, + CustomerName = assignedJob.Customer != null + ? (assignedJob.Customer.CompanyName ?? $"{assignedJob.Customer.ContactFirstName} {assignedJob.Customer.ContactLastName}".Trim()) + : "No Customer" + }); + } + return Json(new { transactionId = txn.Id, jobId = txn.JobId, + quantity = Math.Abs(txn.Quantity), notes = txn.Notes, transactionDate = txn.TransactionDate.ToString("yyyy-MM-ddTHH:mm"), itemName = txn.InventoryItem?.Name, @@ -2046,14 +2128,15 @@ public class InventoryController : Controller } /// - /// Saves edits to a JobUsage InventoryTransaction's job assignment, notes, and date. - /// Quantity and balance are not changed. + /// Saves edits to a JobUsage InventoryTransaction: job assignment, quantity, notes, and date. + /// When quantity changes the InventoryItem.QuantityOnHand is adjusted by the delta so the + /// ledger balance remains consistent. /// [HttpPost] [ValidateAntiForgeryToken] - public async Task EditUsageTransaction(int id, int? jobId, string? notes, DateTime transactionDate) + public async Task EditUsageTransaction(int id, int? jobId, string? notes, DateTime transactionDate, decimal? quantity) { - var txn = await _unitOfWork.InventoryTransactions.GetByIdAsync(id); + var txn = await _unitOfWork.InventoryTransactions.GetByIdAsync(id, false, t => t.InventoryItem); if (txn == null) return NotFound(); if (txn.TransactionType != InventoryTransactionType.JobUsage && txn.TransactionType != InventoryTransactionType.Adjustment) @@ -2075,6 +2158,28 @@ public class InventoryController : Controller if (jobId.HasValue && txn.TransactionType == InventoryTransactionType.Adjustment) txn.TransactionType = InventoryTransactionType.JobUsage; + // Adjust inventory when the logged quantity is changed. + // txn.Quantity is stored as a negative number for usage (e.g. -3.5 for 3.5 lbs used). + if (quantity.HasValue && quantity.Value > 0) + { + var oldUsed = Math.Abs(txn.Quantity); + var newUsed = quantity.Value; + if (oldUsed != newUsed) + { + var item = txn.InventoryItem ?? await _unitOfWork.InventoryItems.GetByIdAsync(txn.InventoryItemId); + if (item != null) + { + // Positive delta means less was actually used → restore the difference to inventory. + item.QuantityOnHand += oldUsed - newUsed; + item.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.InventoryItems.UpdateAsync(item); + txn.BalanceAfter = item.QuantityOnHand; + } + txn.Quantity = -newUsed; + txn.TotalCost = newUsed * txn.UnitCost; + } + } + txn.Notes = notes?.Trim(); txn.TransactionDate = transactionDate.Kind == DateTimeKind.Utc ? transactionDate : DateTime.SpecifyKind(transactionDate, DateTimeKind.Utc); @@ -2094,3 +2199,21 @@ public class ScanJobOption public string JobNumber { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; } + +/// Result returned by RecordInventoryUsageAsync. +public record InventoryUsageResult( + bool Success, + string Message, + decimal NewBalance, + string UnitOfMeasure, + string ItemName); + +/// JSON body for the LogMaterial endpoint (job details modal). +public class LogMaterialRequest +{ + public int JobId { get; set; } + public int InventoryItemId { get; set; } + public decimal QuantityUsed { get; set; } + public string TransactionType { get; set; } = "JobUsage"; + public string? Notes { get; set; } +} diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs index 841fd02..11df57d 100644 --- a/src/PowderCoating.Web/Controllers/JobsController.cs +++ b/src/PowderCoating.Web/Controllers/JobsController.cs @@ -4399,75 +4399,7 @@ public class JobsController : Controller _logger.LogInformation("Recorded first job creation for company {CompanyId}", companyId); } - /// - /// Logs manual material usage from the job details page. Mirrors the QR scan LogUsage - /// flow in InventoryController but returns JSON so the modal can close and refresh inline. - /// Quantity is always the amount USED (caller converts from remaining if needed). - /// - [HttpPost] - [ValidateAntiForgeryToken] - public async Task LogMaterial([FromBody] LogMaterialRequest req) - { - try - { - if (req.QuantityUsed <= 0) - return Json(new { success = false, message = "Quantity used must be greater than zero." }); - - var item = await _unitOfWork.InventoryItems.GetByIdAsync(req.InventoryItemId); - if (item == null) return Json(new { success = false, message = "Inventory item not found." }); - - var job = await _unitOfWork.Jobs.GetByIdAsync(req.JobId); - if (job == null) return Json(new { success = false, message = "Job not found." }); - - var txnType = req.TransactionType == "Waste" - ? InventoryTransactionType.Waste - : InventoryTransactionType.JobUsage; - - item.QuantityOnHand -= req.QuantityUsed; - item.UpdatedAt = DateTime.UtcNow; - await _unitOfWork.InventoryItems.UpdateAsync(item); - - var txn = new PowderCoating.Core.Entities.InventoryTransaction - { - InventoryItemId = item.Id, - TransactionType = txnType, - Quantity = -req.QuantityUsed, - UnitCost = item.UnitCost, - TotalCost = req.QuantityUsed * item.UnitCost, - TransactionDate = DateTime.UtcNow, - BalanceAfter = item.QuantityOnHand, - JobId = req.JobId, - Reference = $"Job {job.JobNumber}", - Notes = req.Notes?.Trim(), - CompanyId = item.CompanyId, - CreatedAt = DateTime.UtcNow - }; - await _unitOfWork.InventoryTransactions.AddAsync(txn); - await _unitOfWork.CompleteAsync(); - - // GL: DR COGS, CR Inventory Asset - if (item.CogsAccountId.HasValue && item.InventoryAccountId.HasValue) - { - var cost = req.QuantityUsed * (item.AverageCost > 0 ? item.AverageCost : item.UnitCost); - await _accountBalanceService.DebitAsync(item.CogsAccountId, cost); - await _accountBalanceService.CreditAsync(item.InventoryAccountId, cost); - } - - return Json(new - { - success = true, - message = $"Logged {req.QuantityUsed:N2} {item.UnitOfMeasure} of {item.Name}.", - newBalance = item.QuantityOnHand, - unitOfMeasure = item.UnitOfMeasure, - itemName = item.Name - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error logging material for job {JobId}", req.JobId); - return Json(new { success = false, message = "An error occurred. Please try again." }); - } - } + // LogMaterial has been consolidated into InventoryController.LogMaterial. /// /// Inline-edits description, quantity, and unit price on a single job line item. @@ -4554,14 +4486,6 @@ public class PatchJobItemRequest public decimal Quantity { get; set; } public decimal UnitPrice { get; set; } } -public class LogMaterialRequest -{ - public int JobId { get; set; } - public int InventoryItemId { get; set; } - public decimal QuantityUsed { get; set; } - public string TransactionType { get; set; } = "JobUsage"; - public string? Notes { get; set; } -} public class CreateReworkJobRequest { public int ReworkRecordId { get; set; } diff --git a/src/PowderCoating.Web/Views/Inventory/Ledger.cshtml b/src/PowderCoating.Web/Views/Inventory/Ledger.cshtml index d8c6919..b7d30d2 100644 --- a/src/PowderCoating.Web/Views/Inventory/Ledger.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Ledger.cshtml @@ -353,6 +353,11 @@

+
+ + +
Adjusts the inventory balance by the difference from the original entry.
+
- -
-
- - -
+
+ + +
+
+ Enter how much powder you took out of the bag.
-
+
@@ -3311,7 +3314,7 @@ const inventoryItems = @Html.Raw(ViewBag.InventoryItemsForModal ?? "[]"); const jobPowderIds = @Html.Raw(ViewBag.JobPowderIds ?? "[]"); const jobId = @Model.Id; - const logUrl = '@Url.Action("LogMaterial", "Jobs")'; + const logUrl = '@Url.Action("LogMaterial", "Inventory")'; const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; window.__logMaterial = { inventoryItems, jobPowderIds, jobId, logUrl, token }; })(); diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-ledger.js b/src/PowderCoating.Web/wwwroot/js/inventory-ledger.js index fabe5f4..e667064 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-ledger.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-ledger.js @@ -18,6 +18,7 @@ async function openUsageEdit(transactionId) { document.getElementById('euTxnId').value = data.transactionId; document.getElementById('euItemName').textContent = data.itemName || '—'; + document.getElementById('euQuantity').value = data.quantity != null ? parseFloat(data.quantity).toFixed(4) : ''; document.getElementById('euDate').value = data.transactionDate; document.getElementById('euNotes').value = data.notes || ''; @@ -54,6 +55,7 @@ document.getElementById('euSaveBtn').addEventListener('click', async () => { const token = form.querySelector('input[name="__RequestVerificationToken"]')?.value; const params = new URLSearchParams({ id: document.getElementById('euTxnId').value, + quantity: document.getElementById('euQuantity').value, jobId: document.getElementById('euJobId').value, notes: document.getElementById('euNotes').value, transactionDate: document.getElementById('euDate').value, diff --git a/src/PowderCoating.Web/wwwroot/js/log-material.js b/src/PowderCoating.Web/wwwroot/js/log-material.js index 11afcbe..466d9ce 100644 --- a/src/PowderCoating.Web/wwwroot/js/log-material.js +++ b/src/PowderCoating.Web/wwwroot/js/log-material.js @@ -6,9 +6,64 @@ let _items = []; let _jobPowderIds = new Set(); let _modal = null; + let _selectedItemId = 0; + let _entryMethod = 'used'; // 'used' | 'remaining' + + // ── Mode toggle ─────────────────────────────────────────────────────────── + + window.lmSetMethod = function (method) { + _entryMethod = method; + const btnUsed = document.getElementById('lmBtnUsed'); + const btnRemaining = document.getElementById('lmBtnRemaining'); + const hintEl = document.getElementById('lmMethodHint'); + const qtyLabel = document.getElementById('lmQtyLabel'); + + if (method === 'remaining') { + btnUsed.className = 'btn btn-outline-primary'; + btnRemaining.className = 'btn btn-primary'; + hintEl.textContent = 'Enter how much is LEFT in the bag — the system calculates what was used.'; + qtyLabel.innerHTML = 'Weight Remaining in Bag *'; + } else { + btnUsed.className = 'btn btn-primary'; + btnRemaining.className = 'btn btn-outline-primary'; + hintEl.textContent = 'Enter how much powder you took out of the bag.'; + qtyLabel.innerHTML = 'Quantity Used *'; + } + lmUpdatePreview(); + }; + + // ── Live preview (always visible once qty + item are set) ───────────────── + + function lmUpdatePreview() { + const computedDiv = document.getElementById('lmComputedUsed'); + if (!_selectedItemId || !computedDiv) { computedDiv?.classList.add('d-none'); return; } + const item = _items.find(it => it.id === _selectedItemId); + const onHand = item ? (parseFloat(item.quantityOnHand) || 0) : 0; + const qty = parseFloat(document.getElementById('lmQuantity').value) || 0; + if (qty <= 0) { computedDiv.classList.add('d-none'); return; } + + const uom = item?.unitOfMeasure || ''; + if (_entryMethod === 'remaining') { + const used = onHand - qty; + if (used <= 0) { + computedDiv.className = 'form-text fw-semibold text-danger'; + computedDiv.textContent = 'Remaining cannot be ≥ current stock (' + onHand.toFixed(2) + ' ' + uom + ').'; + } else { + computedDiv.className = 'form-text fw-semibold text-success'; + computedDiv.textContent = + 'Will log ' + used.toFixed(2) + ' ' + uom + ' used — new balance: ' + qty.toFixed(2) + ' ' + uom; + } + } else { + const newBal = onHand - qty; + const col = newBal < 0 ? 'text-danger' : 'text-success'; + computedDiv.className = 'form-text fw-semibold ' + col; + computedDiv.textContent = + 'Will log ' + qty.toFixed(2) + ' ' + uom + ' used — new balance: ' + newBal.toFixed(2) + ' ' + uom; + } + computedDiv.classList.remove('d-none'); + } // ── Combobox state ──────────────────────────────────────────────────────── - let _selectedItemId = 0; function lmComboInput() { const q = document.getElementById('lmItemSearch')?.value?.toLowerCase() || ''; @@ -16,7 +71,7 @@ lmComboShow(); _selectedItemId = 0; document.getElementById('lmItemBalance').classList.add('d-none'); - lmOnQtyInput(); + lmUpdatePreview(); } function lmComboOpen() { @@ -111,7 +166,7 @@ const balDiv = document.getElementById('lmItemBalance'); balDiv.textContent = 'Current stock: ' + qty.toFixed(2) + (uom ? ' ' + uom : ''); balDiv.classList.remove('d-none'); - lmOnQtyInput(); + lmUpdatePreview(); }; window.lmComboInput = lmComboInput; @@ -152,39 +207,14 @@ 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; - } - 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 used = onHand - remaining; - const computedDiv = document.getElementById('lmComputedUsed'); - computedDiv.textContent = 'Usage = ' + onHand.toFixed(2) + ' − ' + remaining.toFixed(2) + ' = ' + used.toFixed(2) + (item?.unitOfMeasure ? ' ' + item.unitOfMeasure : ''); - computedDiv.classList.remove('d-none'); - } - - window.lmUpdateQuantityLabel = function () { - const method = document.querySelector('input[name="lmEntryMethod"]:checked')?.value; - document.getElementById('lmQtyLabel').innerHTML = - (method === 'remaining' ? 'Quantity Remaining' : 'Quantity Used') + - ' *'; - lmOnQtyInput(); - }; + // ── Kept for backward-compat with any inline onchange handlers that may exist ─ + window.lmUpdateQuantityLabel = function () { lmUpdatePreview(); }; // ── Modal open / save ───────────────────────────────────────────────────── window.openLogMaterialModal = function () { _selectedItemId = 0; + _entryMethod = 'used'; document.getElementById('lmItemSearch').value = ''; document.getElementById('lmItemBalance').classList.add('d-none'); document.getElementById('lmQuantity').value = ''; @@ -193,8 +223,7 @@ document.getElementById('lmNotes').value = ''; document.getElementById('lmAlert').classList.add('d-none'); document.getElementById('lmSaveBtn').disabled = false; - document.getElementById('lmMethodUsed').checked = true; - window.lmUpdateQuantityLabel(); + lmSetMethod('used'); lmComboClose(); if (_modal) _modal.show(); }; @@ -214,14 +243,14 @@ 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; + const item = _items.find(it => it.id === _selectedItemId); + const onHand = item ? (parseFloat(item.quantityOnHand) || 0) : 0; + let quantityUsed = qtyInput; - if (method === 'remaining') { - const item = _items.find(it => it.id === _selectedItemId); - const onHand = item ? (parseFloat(item.quantityOnHand) || 0) : 0; + if (_entryMethod === 'remaining') { quantityUsed = onHand - qtyInput; if (quantityUsed <= 0) { - showError('Remaining quantity cannot be equal to or greater than the current stock (' + onHand.toFixed(2) + ').'); + showError('Remaining cannot be equal to or greater than the current stock (' + onHand.toFixed(2) + ').'); return; } } @@ -269,9 +298,8 @@ _jobPowderIds = new Set(cfg.jobPowderIds || []); _modal = new bootstrap.Modal(document.getElementById('logMaterialModal')); - document.getElementById('lmQuantity').addEventListener('input', lmOnQtyInput); + document.getElementById('lmQuantity').addEventListener('input', lmUpdatePreview); - // Close dropdown when clicking outside document.addEventListener('click', function (e) { if (!e.target.closest('#lmItemSearch') && !e.target.closest('#lmItemDropdown') &&