Auto-receive catalog powders and fix soft-deleted SKU collision

Two fixes to the "Got It" powder receive flow:

1. Skip the modal when the powder is in the master catalog. Clicking "Got It"
   now first calls ReceivePowderFromCatalog, which — if the powder resolves in
   the catalog — creates a fully populated inventory record (specs, cure, SDS/
   TDS, image, pricing) and marks the coat received, no modal. Only when the
   powder isn't in the catalog does it fall back to the manual entry modal.
   The catalog match/apply and the receive finalize (opening txn, mark received,
   sibling-coat linking) are extracted into shared helpers used by both the
   modal save and the auto-receive path.

2. Fix a crash re-receiving a previously-deleted powder. The unique index
   IX_InventoryItems_CompanyId_SKU had no filter, so a soft-deleted item still
   reserved its SKU; re-creating it generated the same SKU and violated the
   constraint. The index is now filtered on IsDeleted = 0, matching the app's
   soft-delete semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:42:29 -04:00
parent 99b22d2ad2
commit 115ccf7d5e
6 changed files with 11702 additions and 75 deletions
@@ -1041,6 +1041,37 @@
// Custom powder (no inventory item) â†' open modal to add to inventory
if (!hasInv) {
// If the powder is already in the master catalog, receive it straight to inventory
// with all its specs/docs — no modal. Only fall back to the modal when it isn't.
const tokenAuto = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
this.disabled = true; qtyInput.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
const autoResp = await fetch('@Url.Action("ReceivePowderFromCatalog", "Dashboard")', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': tokenAuto },
body: `coatId=${coatId}&lbsReceived=${lbs}`
});
const autoData = await autoResp.json();
if (autoData.success) {
fadePlacedRow(row);
showInventoryToast('Added "' + (autoData.itemName || 'powder') + '" to inventory from the catalog.');
return;
}
if (!autoData.needsDetails) {
alert(autoData.message || 'Could not record receipt. Please try again.');
this.disabled = false; qtyInput.disabled = false;
this.innerHTML = '<i class="bi bi-box-arrow-in-down"></i> Got It';
return;
}
// Not in catalog — fall through to the manual entry modal.
} catch {
// Network error — fall back to the manual entry modal.
}
this.disabled = false; qtyInput.disabled = false;
this.innerHTML = '<i class="bi bi-box-arrow-in-down"></i> Got It';
const modal = document.getElementById('addPowderModal');
// Pre-fill hidden + text fields
modal.querySelector('#apm-coatId').value = coatId;