diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index a5909c2..1f85b8f 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -836,9 +836,13 @@ public class InventoryController : Controller // Check if this product already exists in the tenant's inventory. // Match by ManufacturerPartNumber first (most precise); fall back to color name + manufacturer. - // Returns the first active match so the UI can warn the user before they create a duplicate. - int? existingInventoryId = null; - string? existingInventoryName = null; + // Returns the first active match so the UI can prompt to add stock inline. + int? existingInventoryId = null; + string? existingInventoryName = null; + decimal? existingQuantityOnHand = null; + string? existingUnitOfMeasure = null; + + InventoryItem? existingHit = null; if (!string.IsNullOrEmpty(sku)) { @@ -846,52 +850,119 @@ public class InventoryController : Controller var byPart = await _unitOfWork.InventoryItems.FindAsync(i => i.ManufacturerPartNumber != null && i.ManufacturerPartNumber.ToLower() == skuLower); - var hit = byPart.FirstOrDefault(); - if (hit != null) { existingInventoryId = hit.Id; existingInventoryName = hit.Name; } + existingHit = byPart.FirstOrDefault(); } - if (existingInventoryId == null && !string.IsNullOrEmpty(colorName)) + if (existingHit == null && !string.IsNullOrEmpty(colorName)) { var nameLower = colorName.ToLower(); var mfrLower = manufacturer?.ToLower() ?? ""; var byName = await _unitOfWork.InventoryItems.FindAsync(i => (i.ColorName != null && i.ColorName.ToLower() == nameLower) || i.Name.ToLower() == nameLower); - var hit = byName.FirstOrDefault(i => + existingHit = byName.FirstOrDefault(i => string.IsNullOrEmpty(mfrLower) || (i.Manufacturer ?? "").ToLower().Contains(mfrLower) || mfrLower.Contains((i.Manufacturer ?? "").ToLower().Trim())); - if (hit != null) { existingInventoryId = hit.Id; existingInventoryName = hit.Name; } + } + + if (existingHit != null) + { + existingInventoryId = existingHit.Id; + existingInventoryName = existingHit.Name; + existingQuantityOnHand = existingHit.QuantityOnHand; + existingUnitOfMeasure = existingHit.UnitOfMeasure; } return Json(new { - success = true, - manufacturer = manufacturer, - manufacturerPartNumber= sku, - colorName = colorName, - description = aiResult.Description, - finish = catalogMatch?.Finish ?? aiResult.Finish, - cureTemperatureF = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF, - cureTimeMinutes = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes, - colorFamilies = catalogMatch?.ColorFamilies ?? aiResult.ColorFamilies, - requiresClearCoat = catalogMatch?.RequiresClearCoat ?? aiResult.RequiresClearCoat, - coverageSqFtPerLb = catalogMatch?.CoverageSqFtPerLb ?? aiResult.CoverageSqFtPerLb, - transferEfficiency = catalogMatch?.TransferEfficiency ?? aiResult.TransferEfficiency, - unitPrice = catalogMatch?.UnitPrice ?? aiResult.UnitCostPerLb ?? 0m, - imageUrl = catalogMatch?.ImageUrl ?? aiResult.ImageUrl, - productUrl = catalogMatch?.ProductUrl ?? aiResult.SpecPageUrl, - sdsUrl = catalogMatch?.SdsUrl ?? aiResult.SdsUrl, - tdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl, - vendorName = manufacturer, - wasInCatalog = wasInCatalog, - addedToCatalog = addedToCatalog, - existingInventoryId = existingInventoryId, - existingInventoryName = existingInventoryName, - reasoning = aiResult.Reasoning, + success = true, + manufacturer = manufacturer, + manufacturerPartNumber = sku, + colorName = colorName, + description = aiResult.Description, + finish = catalogMatch?.Finish ?? aiResult.Finish, + cureTemperatureF = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF, + cureTimeMinutes = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes, + colorFamilies = catalogMatch?.ColorFamilies ?? aiResult.ColorFamilies, + requiresClearCoat = catalogMatch?.RequiresClearCoat ?? aiResult.RequiresClearCoat, + coverageSqFtPerLb = catalogMatch?.CoverageSqFtPerLb ?? aiResult.CoverageSqFtPerLb, + transferEfficiency = catalogMatch?.TransferEfficiency ?? aiResult.TransferEfficiency, + unitPrice = catalogMatch?.UnitPrice ?? aiResult.UnitCostPerLb ?? 0m, + imageUrl = catalogMatch?.ImageUrl ?? aiResult.ImageUrl, + productUrl = catalogMatch?.ProductUrl ?? aiResult.SpecPageUrl, + sdsUrl = catalogMatch?.SdsUrl ?? aiResult.SdsUrl, + tdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl, + vendorName = manufacturer, + wasInCatalog = wasInCatalog, + addedToCatalog = addedToCatalog, + existingInventoryId = existingInventoryId, + existingInventoryName = existingInventoryName, + existingQuantityOnHand = existingQuantityOnHand, + existingUnitOfMeasure = existingUnitOfMeasure, + reasoning = aiResult.Reasoning, }); } + /// + /// Adds stock to an existing inventory item from the label scanner inline prompt. + /// Creates a Purchase transaction and updates QuantityOnHand without navigating away. + /// + [HttpPost] + public async Task AddStock(int inventoryItemId, decimal quantity, decimal? unitCost, string? notes) + { + try + { + if (quantity <= 0) + return Json(new { success = false, errorMessage = "Quantity must be greater than zero." }); + + var item = await _unitOfWork.InventoryItems.GetByIdAsync(inventoryItemId); + if (item == null) return Json(new { success = false, errorMessage = "Item not found." }); + + var cost = (unitCost.HasValue && unitCost.Value > 0) ? unitCost.Value : item.UnitCost; + + item.QuantityOnHand += quantity; + item.LastPurchaseDate = DateTime.UtcNow; + if (unitCost.HasValue && unitCost.Value > 0) + { + item.LastPurchasePrice = unitCost.Value; + item.UnitCost = unitCost.Value; + } + item.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.InventoryItems.UpdateAsync(item); + + var txn = new InventoryTransaction + { + InventoryItemId = item.Id, + TransactionType = InventoryTransactionType.Purchase, + Quantity = quantity, + UnitCost = cost, + TotalCost = quantity * cost, + TransactionDate = DateTime.UtcNow, + BalanceAfter = item.QuantityOnHand, + Notes = !string.IsNullOrWhiteSpace(notes) ? notes.Trim() : "Added via label scan", + }; + await _unitOfWork.InventoryTransactions.AddAsync(txn); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("Label scan added {Qty} {UOM} to inventory item {Id} ({Name})", + quantity, item.UnitOfMeasure, item.Id, item.Name); + + return Json(new + { + success = true, + newQuantityOnHand = item.QuantityOnHand, + unitOfMeasure = item.UnitOfMeasure, + itemName = item.Name, + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding stock via label scan to inventory item {ItemId}", inventoryItemId); + return Json(new { success = false, errorMessage = "An error occurred. Please try again." }); + } + } + /// /// Searches the platform-level PowderCatalogItems table by SKU or color name and returns /// up to 10 matches as JSON. Called by the inventory Create/Edit form before falling back diff --git a/src/PowderCoating.Web/Views/Inventory/_LabelScanModal.cshtml b/src/PowderCoating.Web/Views/Inventory/_LabelScanModal.cshtml index 16ae126..e35338a 100644 --- a/src/PowderCoating.Web/Views/Inventory/_LabelScanModal.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/_LabelScanModal.cshtml @@ -1,3 +1,52 @@ + + +