Complete Job modal: ask powder usage once per color, not per item/coat

The modal was showing one row per coat per item, so a job with 5 items
each with 2 coats of the same powder produced 10 identical input rows.

Now groups by unique InventoryItemId and shows one row per powder color
for the whole job. The controller distributes the entered total across
coats proportionally by their estimated PowderToOrder so per-coat
reporting data is preserved. A single inventory transaction is created
per powder (net of any pre-logged scan credit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 12:30:30 -04:00
parent be0a5b26e2
commit 0df2353d4f
3 changed files with 116 additions and 144 deletions
@@ -2,8 +2,21 @@
@{
var emailDefault = ViewBag.EmailDefaultOnComplete == true;
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
// Track remaining credit per InventoryItemId as we allocate it across coat rows
var remainingCredit = preLoggedPowder.ToDictionary(kv => kv.Key, kv => kv.Value);
// Group all coats by inventory item so we ask once per powder color, not once per item/coat
var powderGroups = (Model.Items ?? new List<PowderCoating.Application.DTOs.Job.JobItemDto>())
.SelectMany(i => i.Coats ?? new List<PowderCoating.Application.DTOs.Job.JobItemCoatDto>())
.Where(c => c.InventoryItemId.HasValue)
.GroupBy(c => c.InventoryItemId!.Value)
.Select(g => new {
InventoryItemId = g.Key,
ColorName = g.First().ColorName,
ColorCode = g.First().ColorCode,
TotalEstimatedLbs = g.Sum(c => c.PowderToOrder ?? 0m),
PreLogged = preLoggedPowder.GetValueOrDefault(g.Key, 0m)
})
.OrderBy(g => g.ColorName)
.ToList();
}
<div class="modal fade" id="completeJobModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
@@ -27,102 +40,59 @@
<div class="form-text">Enter the total time in hours (e.g., 2.5 for 2 hours 30 minutes)</div>
</div>
@if (Model.Items != null && Model.Items.Any())
@if (powderGroups.Any())
{
<div class="mb-3">
<h6 class="fw-semibold mb-3">
<h6 class="fw-semibold mb-1">
<i class="bi bi-palette me-1 text-primary"></i>Actual Powder Usage
</h6>
<p class="text-muted small mb-3">Enter total lbs used per powder color for the entire job.</p>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="table-light">
<tr>
<th>Item</th>
<th>Coat</th>
<th>Color</th>
<th>Color / Powder</th>
<th class="text-end">Estimated (lbs)</th>
<th>Actual (lbs)</th>
<th style="width:150px">Actual Used (lbs)</th>
</tr>
</thead>
<tbody>
@{
var coatIndex = 0;
}
@foreach (var item in Model.Items)
@for (int i = 0; i < powderGroups.Count; i++)
{
if (item.Coats != null && item.Coats.Any())
{
foreach (var coat in item.Coats.OrderBy(c => c.Sequence))
{
<tr>
<td>
<small>@item.Description</small>
@if (item.Quantity > 1)
{
<span class="badge bg-secondary ms-1">&times;@item.Quantity</span>
}
</td>
<td><span class="badge bg-secondary">@coat.CoatName</span></td>
<td>
@if (!string.IsNullOrEmpty(coat.ColorName))
{
<small>
@coat.ColorName
@if (!string.IsNullOrEmpty(coat.ColorCode))
{
<span class="text-muted">(@coat.ColorCode)</span>
}
</small>
}
</td>
<td class="text-end">
<small class="text-muted">@((coat.PowderToOrder ?? 0).ToString("0.##"))</small>
</td>
<td>
@{
decimal preFilledLbs = 0m;
if (coat.InventoryItemId.HasValue && remainingCredit.TryGetValue(coat.InventoryItemId.Value, out var availCredit) && availCredit > 0)
{
preFilledLbs = availCredit;
remainingCredit[coat.InventoryItemId.Value] = 0m;
}
}
<input type="hidden" name="CoatUsages[@coatIndex].JobItemCoatId" value="@coat.Id" />
<input type="number"
class="form-control form-control-sm"
name="CoatUsages[@coatIndex].ActualPowderUsedLbs"
step="0.01" min="0" placeholder="0.00"
value="@(preFilledLbs > 0 ? preFilledLbs.ToString("0.##") : "")"
style="max-width: 120px;">
@if (preFilledLbs > 0)
{
<small class="text-success d-block mt-1">
<i class="bi bi-check-circle me-1"></i>Already logged — inventory adjusted
</small>
}
</td>
</tr>
coatIndex++;
}
}
else
{
<tr class="table-secondary">
<td colspan="5">
<small class="text-muted fst-italic">
<i class="bi bi-info-circle me-1"></i>
@item.Description — No coat information available (legacy job item)
var pg = powderGroups[i];
<tr>
<td>
<span class="fw-semibold">@pg.ColorName</span>
@if (!string.IsNullOrEmpty(pg.ColorCode))
{
<small class="text-muted ms-1">(@pg.ColorCode)</small>
}
</td>
<td class="text-end text-muted small align-middle">
@pg.TotalEstimatedLbs.ToString("0.##")
</td>
<td>
<input type="hidden" name="PowderUsages[@i].InventoryItemId" value="@pg.InventoryItemId" />
<input type="number"
class="form-control form-control-sm"
name="PowderUsages[@i].ActualPowderUsedLbs"
step="0.01" min="0" placeholder="0.00"
value="@(pg.PreLogged > 0 ? pg.PreLogged.ToString("0.##") : "")">
@if (pg.PreLogged > 0)
{
<small class="text-success d-block mt-1">
<i class="bi bi-check-circle me-1"></i>@pg.PreLogged.ToString("0.##") lbs already logged
</small>
</td>
</tr>
}
}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="alert alert-info alert-permanent mb-0">
<i class="bi bi-info-circle me-2"></i>
<small>Pre-filled values were already logged via scan inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
<small>Pre-filled values were already logged via scan &mdash; inventory is already adjusted for those. You can edit the amount; only the difference will be applied.</small>
</div>
</div>
}