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:
@@ -1511,6 +1511,9 @@ modelBuilder.Entity<Job>()
|
||||
modelBuilder.Entity<InventoryItem>()
|
||||
.HasIndex(i => new { i.CompanyId, i.SKU })
|
||||
.IsUnique()
|
||||
// Filter on IsDeleted so soft-deleted items don't reserve their SKU and block a new
|
||||
// (or re-created) item from reusing it — matching the app's soft-delete semantics.
|
||||
.HasFilter("[IsDeleted] = 0")
|
||||
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU");
|
||||
|
||||
modelBuilder.Entity<Company>()
|
||||
|
||||
+11376
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class FilterInventorySkuUniqueIndexOnSoftDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InventoryItems_CompanyId_SKU",
|
||||
table: "InventoryItems");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4251));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4258));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4259));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_CompanyId_SKU",
|
||||
table: "InventoryItems",
|
||||
columns: new[] { "CompanyId", "SKU" },
|
||||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InventoryItems_CompanyId_SKU",
|
||||
table: "InventoryItems");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(5997));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6002));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6003));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_CompanyId_SKU",
|
||||
table: "InventoryItems",
|
||||
columns: new[] { "CompanyId", "SKU" },
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4244,7 +4244,8 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("CompanyId", "SKU")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU");
|
||||
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU")
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
|
||||
b.HasIndex("CompanyId", "QuantityOnHand", "ReorderPoint")
|
||||
.HasDatabaseName("IX_InventoryItems_CompanyId_Quantity_Reorder");
|
||||
@@ -7234,7 +7235,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 1,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(5997),
|
||||
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4251),
|
||||
Description = "Standard pricing for regular customers",
|
||||
DiscountPercent = 0m,
|
||||
IsActive = true,
|
||||
@@ -7245,7 +7246,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 2,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6002),
|
||||
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4258),
|
||||
Description = "5% discount for preferred customers",
|
||||
DiscountPercent = 5m,
|
||||
IsActive = true,
|
||||
@@ -7256,7 +7257,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 3,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6003),
|
||||
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4259),
|
||||
Description = "10% discount for premium customers",
|
||||
DiscountPercent = 10m,
|
||||
IsActive = true,
|
||||
|
||||
@@ -771,59 +771,9 @@ public class DashboardController : Controller
|
||||
// coat's colorCode), preferring the same manufacturer; fall back to color name.
|
||||
await EnrichInventoryFromCatalogAsync(inventoryItem, colorCode, colorName, manufacturer);
|
||||
|
||||
await _unitOfWork.InventoryItems.AddAsync(inventoryItem);
|
||||
await _unitOfWork.CompleteAsync(); // flush to get inventoryItem.Id
|
||||
|
||||
// Opening stock transaction
|
||||
var transaction = new InventoryTransaction
|
||||
{
|
||||
CompanyId = companyId,
|
||||
InventoryItemId = inventoryItem.Id,
|
||||
TransactionType = InventoryTransactionType.Purchase,
|
||||
Quantity = lbsReceived,
|
||||
UnitCost = unitCost ?? 0,
|
||||
TotalCost = lbsReceived * (unitCost ?? 0),
|
||||
TransactionDate = DateTime.UtcNow,
|
||||
Notes = $"Initial stock — received from powder order for job {jobItem?.Job?.JobNumber}",
|
||||
BalanceAfter = lbsReceived,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
await _unitOfWork.InventoryTransactions.AddAsync(transaction);
|
||||
|
||||
// Mark coat as received and link to the new inventory item
|
||||
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
coat.PowderReceived = true;
|
||||
coat.PowderReceivedAt = DateTime.UtcNow;
|
||||
coat.PowderReceivedByUserId = userId;
|
||||
coat.PowderReceivedLbs = lbsReceived;
|
||||
coat.InventoryItemId = inventoryItem.Id;
|
||||
|
||||
// Scan for sibling coats with the same custom powder and link them to the new item
|
||||
var candidateCoats = await _unitOfWork.JobItemCoats.GetCandidateCoatsForLinkingAsync(coatId, companyId);
|
||||
|
||||
int linkedCount = 0;
|
||||
foreach (var other in candidateCoats)
|
||||
{
|
||||
bool colorMatch = !string.IsNullOrWhiteSpace(colorCode)
|
||||
? string.Equals(other.ColorCode?.Trim(), colorCode.Trim(), StringComparison.OrdinalIgnoreCase)
|
||||
: !string.IsNullOrWhiteSpace(colorName) &&
|
||||
string.Equals(other.ColorName?.Trim(), colorName.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!colorMatch) continue;
|
||||
|
||||
if (primaryVendorId.HasValue && other.VendorId.HasValue && other.VendorId != primaryVendorId)
|
||||
continue;
|
||||
|
||||
other.InventoryItemId = inventoryItem.Id;
|
||||
linkedCount++;
|
||||
}
|
||||
|
||||
if (linkedCount > 0)
|
||||
_logger.LogInformation("Linked {Count} additional coat(s) to new inventory item {ItemId} ({SKU})",
|
||||
linkedCount, inventoryItem.Id, inventoryItem.SKU);
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
var linkedCount = await FinalizeReceivedPowderAsync(
|
||||
coat, inventoryItem, lbsReceived, companyId, colorCode, colorName, primaryVendorId,
|
||||
jobItem?.Job?.JobNumber);
|
||||
|
||||
return Json(new { success = true, itemName = inventoryItem.Name, sku = inventoryItem.SKU, linkedCount });
|
||||
}
|
||||
@@ -835,40 +785,45 @@ public class DashboardController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills blank spec/document fields on a newly received custom-powder inventory item from the
|
||||
/// matching platform powder catalog row — cure schedule, coverage, specific gravity, transfer
|
||||
/// efficiency, SDS/TDS links, sample image, color families, product page — so the tenant gets a
|
||||
/// complete record instead of just the color code/name carried on the quote. Matches by catalog
|
||||
/// SKU (stored as the coat's color code), preferring the same manufacturer, then by color name.
|
||||
/// Only fills gaps (never overwrites form-entered values) and links the item to the catalog row.
|
||||
/// Finds the platform powder catalog row for an inventory/coat identity: by catalog SKU
|
||||
/// (stored as the coat's color code), preferring the same manufacturer, then by color name.
|
||||
/// Returns null when no match is found.
|
||||
/// </summary>
|
||||
private async Task EnrichInventoryFromCatalogAsync(
|
||||
InventoryItem item, string? colorCode, string? colorName, string? manufacturer)
|
||||
private async Task<PowderCatalogItem?> FindCatalogByIdentityAsync(
|
||||
string? colorCode, string? colorName, string? manufacturer)
|
||||
{
|
||||
PowderCatalogItem? catalog = null;
|
||||
|
||||
var code = colorCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
var codeLower = code.ToLower();
|
||||
var hits = (await _unitOfWork.PowderCatalog.FindAsync(p => p.Sku.ToLower() == codeLower)).ToList();
|
||||
var mfr = manufacturer?.Trim().ToLower();
|
||||
catalog = (!string.IsNullOrWhiteSpace(mfr)
|
||||
var match = (!string.IsNullOrWhiteSpace(mfr)
|
||||
? hits.FirstOrDefault(p => p.VendorName.ToLower().Contains(mfr))
|
||||
: null)
|
||||
?? hits.FirstOrDefault();
|
||||
if (match != null)
|
||||
return match;
|
||||
}
|
||||
|
||||
if (catalog == null && !string.IsNullOrWhiteSpace(colorName))
|
||||
if (!string.IsNullOrWhiteSpace(colorName))
|
||||
{
|
||||
var nameLower = colorName.Trim().ToLower();
|
||||
catalog = (await _unitOfWork.PowderCatalog.FindAsync(p => p.ColorName.ToLower() == nameLower))
|
||||
return (await _unitOfWork.PowderCatalog.FindAsync(p => p.ColorName.ToLower() == nameLower))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (catalog == null)
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies catalog spec/document fields onto an inventory item — cure schedule, coverage,
|
||||
/// specific gravity, transfer efficiency, SDS/TDS links, sample image, color families, product
|
||||
/// page — and links <see cref="InventoryItem.PowderCatalogItemId"/>. Only fills gaps, so any
|
||||
/// value already set (e.g. entered on the receive form) is preserved.
|
||||
/// </summary>
|
||||
private static void ApplyCatalogToInventory(InventoryItem item, PowderCatalogItem catalog)
|
||||
{
|
||||
item.PowderCatalogItemId = catalog.Id;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.ManufacturerPartNumber)) item.ManufacturerPartNumber = catalog.Sku;
|
||||
@@ -897,6 +852,185 @@ public class DashboardController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills blank spec/document fields on a received custom-powder inventory item from the matching
|
||||
/// platform powder catalog row, so the tenant gets a complete record instead of just the color
|
||||
/// code/name carried on the quote. No-op when the powder isn't in the catalog.
|
||||
/// </summary>
|
||||
private async Task EnrichInventoryFromCatalogAsync(
|
||||
InventoryItem item, string? colorCode, string? colorName, string? manufacturer)
|
||||
{
|
||||
var catalog = await FindCatalogByIdentityAsync(colorCode, colorName, manufacturer);
|
||||
if (catalog != null)
|
||||
ApplyCatalogToInventory(item, catalog);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared finalize for a received powder: saves the inventory item, writes the opening Purchase
|
||||
/// transaction, marks the coat received and links it, then links any sibling coats ordering the
|
||||
/// same color. Returns the number of additional coats linked. Used by both the manual modal
|
||||
/// (<see cref="AddCustomPowderToInventory"/>) and the catalog auto-receive
|
||||
/// (<see cref="ReceivePowderFromCatalog"/>).
|
||||
/// </summary>
|
||||
private async Task<int> FinalizeReceivedPowderAsync(
|
||||
JobItemCoat coat, InventoryItem inventoryItem, decimal lbsReceived, int companyId,
|
||||
string? colorCode, string? colorName, int? primaryVendorId, string? jobNumber)
|
||||
{
|
||||
await _unitOfWork.InventoryItems.AddAsync(inventoryItem);
|
||||
await _unitOfWork.CompleteAsync(); // flush to get inventoryItem.Id
|
||||
|
||||
var transaction = new InventoryTransaction
|
||||
{
|
||||
CompanyId = companyId,
|
||||
InventoryItemId = inventoryItem.Id,
|
||||
TransactionType = InventoryTransactionType.Purchase,
|
||||
Quantity = lbsReceived,
|
||||
UnitCost = inventoryItem.UnitCost,
|
||||
TotalCost = lbsReceived * inventoryItem.UnitCost,
|
||||
TransactionDate = DateTime.UtcNow,
|
||||
Notes = $"Initial stock — received from powder order for job {jobNumber}",
|
||||
BalanceAfter = lbsReceived,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
await _unitOfWork.InventoryTransactions.AddAsync(transaction);
|
||||
|
||||
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
coat.PowderReceived = true;
|
||||
coat.PowderReceivedAt = DateTime.UtcNow;
|
||||
coat.PowderReceivedByUserId = userId;
|
||||
coat.PowderReceivedLbs = lbsReceived;
|
||||
coat.InventoryItemId = inventoryItem.Id;
|
||||
|
||||
var candidateCoats = await _unitOfWork.JobItemCoats.GetCandidateCoatsForLinkingAsync(coat.Id, companyId);
|
||||
|
||||
var linkedCount = 0;
|
||||
foreach (var other in candidateCoats)
|
||||
{
|
||||
var colorMatch = !string.IsNullOrWhiteSpace(colorCode)
|
||||
? string.Equals(other.ColorCode?.Trim(), colorCode.Trim(), StringComparison.OrdinalIgnoreCase)
|
||||
: !string.IsNullOrWhiteSpace(colorName) &&
|
||||
string.Equals(other.ColorName?.Trim(), colorName.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!colorMatch) continue;
|
||||
if (primaryVendorId.HasValue && other.VendorId.HasValue && other.VendorId != primaryVendorId) continue;
|
||||
|
||||
other.InventoryItemId = inventoryItem.Id;
|
||||
linkedCount++;
|
||||
}
|
||||
|
||||
if (linkedCount > 0)
|
||||
_logger.LogInformation("Linked {Count} additional coat(s) to new inventory item {ItemId} ({SKU})",
|
||||
linkedCount, inventoryItem.Id, inventoryItem.SKU);
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
return linkedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique powder SKU for a company in the form <c>{CODE}-{YYMM}-{####}</c>, where
|
||||
/// CODE is the (padded) inventory category code. Mirrors the inventory SKU pattern used when
|
||||
/// adding catalog-sourced powders.
|
||||
/// </summary>
|
||||
private async Task<string> GeneratePowderSkuAsync(InventoryCategoryLookup category)
|
||||
{
|
||||
var code = category.CategoryCode.Length >= 4
|
||||
? category.CategoryCode[..4].ToUpperInvariant()
|
||||
: category.CategoryCode.ToUpperInvariant().PadRight(4, 'X');
|
||||
var yearMonth = DateTime.Now.ToString("yyMM");
|
||||
var prefix = $"{code}-{yearMonth}-";
|
||||
|
||||
var allItems = await _unitOfWork.InventoryItems.GetAllAsync(ignoreQueryFilters: true);
|
||||
var maxSeq = allItems
|
||||
.Where(i => i.SKU.StartsWith(prefix))
|
||||
.Select(i => int.TryParse(i.SKU[prefix.Length..], out var n) ? n : 0)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
|
||||
return $"{prefix}{(maxSeq + 1):D4}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives an ordered custom powder straight into inventory WITHOUT the manual modal when the
|
||||
/// powder is already in the master catalog — the new record is fully populated from the catalog
|
||||
/// (specs, SDS/TDS, image, pricing). Returns <c>needsDetails = true</c> (without saving) when
|
||||
/// the powder isn't in the catalog or no coating category is configured, signaling the caller to
|
||||
/// fall back to the manual entry modal.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ReceivePowderFromCatalog(int coatId, decimal lbsReceived)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (lbsReceived <= 0)
|
||||
return Json(new { success = false, message = "Quantity received must be greater than zero." });
|
||||
|
||||
var coat = await _unitOfWork.JobItemCoats.GetByIdAsync(coatId);
|
||||
if (coat == null)
|
||||
return Json(new { success = false, message = "Coat record not found." });
|
||||
|
||||
var jobItem = await _unitOfWork.JobItems.FirstOrDefaultAsync(
|
||||
i => i.Coats.Any(c => c.Id == coatId), false, i => i.Job);
|
||||
var companyId = jobItem?.Job?.CompanyId ?? _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
// Only auto-receive when the powder resolves in the master catalog; otherwise the caller
|
||||
// opens the manual modal.
|
||||
var catalog = await FindCatalogByIdentityAsync(coat.ColorCode, coat.ColorName, null);
|
||||
if (catalog == null)
|
||||
return Json(new { success = false, needsDetails = true });
|
||||
|
||||
// Resolve the company's POWDER (coating) inventory category.
|
||||
var categories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId);
|
||||
var coatingCategory = categories.FirstOrDefault(c => c.IsActive && c.IsCoating
|
||||
&& c.CategoryCode.Equals("POWDER", StringComparison.OrdinalIgnoreCase))
|
||||
?? categories.Where(c => c.IsActive && c.IsCoating)
|
||||
.OrderBy(c => c.DisplayOrder).FirstOrDefault();
|
||||
if (coatingCategory == null)
|
||||
return Json(new { success = false, needsDetails = true });
|
||||
|
||||
var sku = await GeneratePowderSkuAsync(coatingCategory);
|
||||
|
||||
var inventoryItem = new InventoryItem
|
||||
{
|
||||
CompanyId = companyId,
|
||||
SKU = sku,
|
||||
Name = catalog.ColorName,
|
||||
ColorName = catalog.ColorName,
|
||||
ColorCode = coat.ColorCode,
|
||||
InventoryCategoryId = coatingCategory.Id,
|
||||
Category = coatingCategory.DisplayName,
|
||||
QuantityOnHand = lbsReceived,
|
||||
UnitOfMeasure = "lbs",
|
||||
UnitCost = catalog.UnitPrice,
|
||||
LastPurchasePrice = catalog.UnitPrice,
|
||||
LastPurchaseDate = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
ApplyCatalogToInventory(inventoryItem, catalog);
|
||||
|
||||
var linkedCount = await FinalizeReceivedPowderAsync(
|
||||
coat, inventoryItem, lbsReceived, companyId, coat.ColorCode, coat.ColorName, null,
|
||||
jobItem?.Job?.JobNumber);
|
||||
|
||||
return Json(new
|
||||
{
|
||||
success = true,
|
||||
fromCatalog = true,
|
||||
itemName = inventoryItem.Name,
|
||||
sku = inventoryItem.SKU,
|
||||
linkedCount
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error auto-receiving powder from catalog for coat {CoatId}", coatId);
|
||||
return Json(new { success = false, message = "An error occurred while saving." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-level dashboard visible only to SuperAdmins who are not impersonating a tenant.
|
||||
/// Displays a cross-company overview: total/active/inactive company counts, user count,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user