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:
2026-05-16 12:10:54 -04:00
parent 27aa4e0ea6
commit 36680eced9
3 changed files with 313 additions and 4 deletions
@@ -498,6 +498,13 @@ public class JobsController : Controller
.OrderByDescending(t => t.TransactionDate).ToList();
ViewBag.MaterialsUsed = allJobTransactions;
// Inventory items for the manual log-material modal
var inventoryItemsForModal = (await _unitOfWork.InventoryItems.GetAllAsync())
.OrderBy(i => i.Name)
.Select(i => new { i.Id, i.Name, i.UnitOfMeasure, i.QuantityOnHand })
.ToList();
ViewBag.InventoryItemsForModal = System.Text.Json.JsonSerializer.Serialize(inventoryItemsForModal);
// Pre-logged powder grouped by InventoryItemId (for Complete Job modal pre-fill)
ViewBag.PreLoggedPowder = allJobTransactions
.GroupBy(t => t.InventoryItemId)
@@ -4080,9 +4087,87 @@ public class JobsController : Controller
_logger.LogInformation("Recorded first job creation for company {CompanyId}", companyId);
}
/// <summary>
/// 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).
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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." });
}
}
}
public class DeleteTimeEntryRequest { public int Id { 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; } public string? Notes { get; set; } }
public class UpdateWorkerAssignmentRequest