Merge duplicate powder lines on dashboard order queue

When multiple jobs need the same powder, the 'Powder in Queue to be
Ordered' panel now collapses them into a single line (summed lbs) rather
than showing one row per coat. 'Mark as Ordered' marks all contributing
coats at once and injects each into the 'Awaiting Receipt' panel
individually so per-coat receiving still works unchanged.

- Add PowderOrderJobRefDto; PowderOrderLineDto gains CoatIds + Jobs lists
  (scalar CoatId/JobId/etc. become computed accessors for backward compat)
- MapPowderOrderGroupsMerged: secondary GroupBy on (ColorName, ColorCode,
  Finish, SKU) within vendor group for the 'needed' panel
- MapPowderOrderGroups kept per-coat for the 'awaiting receipt' panel
- MarkPowderOrdered accepts comma-separated coatIds, returns coats array
- Dashboard view: Customer column loops job refs for merged rows; JS posts
  coatIds and iterates data.coats to populate awaiting-receipt panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:59:10 -04:00
parent a21c05f655
commit 8f11e00a0a
4 changed files with 282 additions and 525 deletions
@@ -528,11 +528,25 @@
<tbody>
@foreach (var line in vendorGroup.Lines)
{
<tr id="powder-line-@line.CoatId">
<tr id="powder-line-@line.CoatIds[0]">
<td>
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@line.JobId"
class="fw-medium text-decoration-none">@line.CustomerName</a>
<span class="text-muted ms-1">(@line.JobNumber)</span>
@if (line.Jobs.Count == 1)
{
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@line.Jobs[0].JobId"
class="fw-medium text-decoration-none">@line.Jobs[0].CustomerName</a>
<span class="text-muted ms-1">(@line.Jobs[0].JobNumber)</span>
}
else
{
@foreach (var jobRef in line.Jobs)
{
<div>
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@jobRef.JobId"
class="fw-medium text-decoration-none">@jobRef.CustomerName</a>
<span class="text-muted ms-1">(@jobRef.JobNumber &middot; @jobRef.LbsToOrder.ToString("N2") lbs)</span>
</div>
}
}
</td>
<td>
@if (!string.IsNullOrEmpty(line.ColorName))
@@ -551,7 +565,7 @@
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
data-coat-id="@line.CoatId"
data-coat-ids="@string.Join(",", line.CoatIds)"
title="Mark as ordered">
<i class="bi bi-check2-circle me-1"></i>Mark as Ordered
</button>
@@ -875,10 +889,11 @@
// Powder Orders - Mark as Ordered
document.querySelectorAll('.mark-ordered-btn').forEach(btn => {
btn.addEventListener('click', async function () {
const coatId = this.dataset.coatId;
const row = document.getElementById('powder-line-' + coatId);
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
const coatIds = this.dataset.coatIds;
const firstId = coatIds.split(',')[0];
const row = document.getElementById('powder-line-' + firstId);
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
@@ -887,7 +902,7 @@
const resp = await fetch('@Url.Action("MarkPowderOrdered", "Dashboard")', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': token },
body: 'coatId=' + coatId
body: 'coatIds=' + encodeURIComponent(coatIds)
});
const data = await resp.json();
if (data.success) {
@@ -905,8 +920,8 @@
badge.textContent = n;
}
}
// Inject into Awaiting Receipt widget
addToAwaitingReceipt(data.coat);
// Inject each marked coat into the Awaiting Receipt widget
(data.coats || []).forEach(c => addToAwaitingReceipt(c));
}, 400);
} else {
alert(data.message || 'Could not update. Please try again.');