Add vendor supply categories with inventory auto-filter
Vendors can now be tagged with one or more inventory categories (Powder, Chemical, etc.) via checkboxes on the Create/Edit form. The inventory Create/Edit vendor dropdown automatically filters to matching vendors when a category is selected; falls back to all vendors if none are tagged. Includes migration AddVendorCategories (VendorInventoryCategories join table). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,8 @@ public class CreateVendorDto
|
|||||||
|
|
||||||
[Display(Name = "Default Expense Account")]
|
[Display(Name = "Default Expense Account")]
|
||||||
public int? DefaultExpenseAccountId { get; set; }
|
public int? DefaultExpenseAccountId { get; set; }
|
||||||
|
|
||||||
|
public List<int> CategoryIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -209,4 +211,6 @@ public class UpdateVendorDto
|
|||||||
|
|
||||||
[Display(Name = "Default Expense Account")]
|
[Display(Name = "Default Expense Account")]
|
||||||
public int? DefaultExpenseAccountId { get; set; }
|
public int? DefaultExpenseAccountId { get; set; }
|
||||||
|
|
||||||
|
public List<int> CategoryIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ public class InventoryCategoryLookup : BaseEntity
|
|||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
||||||
|
public virtual ICollection<Vendor> Vendors { get; set; } = new List<Vendor>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class Vendor : BaseEntity
|
|||||||
public virtual ICollection<BillPayment> BillPayments { get; set; } = new List<BillPayment>();
|
public virtual ICollection<BillPayment> BillPayments { get; set; } = new List<BillPayment>();
|
||||||
public virtual ICollection<Expense> Expenses { get; set; } = new List<Expense>();
|
public virtual ICollection<Expense> Expenses { get; set; } = new List<Expense>();
|
||||||
public virtual Account? DefaultExpenseAccount { get; set; }
|
public virtual Account? DefaultExpenseAccount { get; set; }
|
||||||
|
public virtual ICollection<InventoryCategoryLookup> Categories { get; set; } = new List<InventoryCategoryLookup>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InventoryTransaction : BaseEntity
|
public class InventoryTransaction : BaseEntity
|
||||||
|
|||||||
@@ -809,6 +809,15 @@ modelBuilder.Entity<ReworkRecord>().HasQueryFilter(e =>
|
|||||||
.HasForeignKey(s => s.DefaultExpenseAccountId)
|
.HasForeignKey(s => s.DefaultExpenseAccountId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
// Vendor ↔ InventoryCategoryLookup (many-to-many supply categories)
|
||||||
|
modelBuilder.Entity<Vendor>()
|
||||||
|
.HasMany(v => v.Categories)
|
||||||
|
.WithMany(c => c.Vendors)
|
||||||
|
.UsingEntity<Dictionary<string, object>>(
|
||||||
|
"VendorInventoryCategories",
|
||||||
|
j => j.HasOne<InventoryCategoryLookup>().WithMany().HasForeignKey("InventoryCategoryLookupId"),
|
||||||
|
j => j.HasOne<Vendor>().WithMany().HasForeignKey("VendorId"));
|
||||||
|
|
||||||
// Bill → APAccount (no cascade to avoid cycles)
|
// Bill → APAccount (no cascade to avoid cycles)
|
||||||
modelBuilder.Entity<Bill>()
|
modelBuilder.Entity<Bill>()
|
||||||
.HasOne(b => b.APAccount)
|
.HasOne(b => b.APAccount)
|
||||||
|
|||||||
Generated
+10672
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddVendorCategories : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VendorInventoryCategories",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
InventoryCategoryLookupId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
VendorId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VendorInventoryCategories", x => new { x.InventoryCategoryLookupId, x.VendorId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorInventoryCategories_InventoryCategoryLookups_InventoryCategoryLookupId",
|
||||||
|
column: x => x.InventoryCategoryLookupId,
|
||||||
|
principalTable: "InventoryCategoryLookups",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorInventoryCategories_Vendors_VendorId",
|
||||||
|
column: x => x.VendorId,
|
||||||
|
principalTable: "Vendors",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4300));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4313));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4315));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorInventoryCategories_VendorId",
|
||||||
|
table: "VendorInventoryCategories",
|
||||||
|
column: "VendorId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VendorInventoryCategories");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8533));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8542));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8543));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6711,7 +6711,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8533),
|
CreatedAt = new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4300),
|
||||||
Description = "Standard pricing for regular customers",
|
Description = "Standard pricing for regular customers",
|
||||||
DiscountPercent = 0m,
|
DiscountPercent = 0m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6722,7 +6722,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8542),
|
CreatedAt = new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4313),
|
||||||
Description = "5% discount for preferred customers",
|
Description = "5% discount for preferred customers",
|
||||||
DiscountPercent = 5m,
|
DiscountPercent = 5m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6733,7 +6733,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 21, 14, 32, 24, 337, DateTimeKind.Utc).AddTicks(8543),
|
CreatedAt = new DateTime(2026, 5, 23, 13, 51, 6, 293, DateTimeKind.Utc).AddTicks(4315),
|
||||||
Description = "10% discount for premium customers",
|
Description = "10% discount for premium customers",
|
||||||
DiscountPercent = 10m,
|
DiscountPercent = 10m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -8634,6 +8634,21 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.ToTable("YearEndCloses");
|
b.ToTable("YearEndCloses");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("VendorInventoryCategories", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("InventoryCategoryLookupId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("VendorId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("InventoryCategoryLookupId", "VendorId");
|
||||||
|
|
||||||
|
b.HasIndex("VendorId");
|
||||||
|
|
||||||
|
b.ToTable("VendorInventoryCategories");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
@@ -10372,6 +10387,21 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Navigation("JournalEntry");
|
b.Navigation("JournalEntry");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("VendorInventoryCategories", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PowderCoating.Core.Entities.InventoryCategoryLookup", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InventoryCategoryLookupId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PowderCoating.Core.Entities.Vendor", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("VendorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PowderCoating.Core.Entities.Account", b =>
|
modelBuilder.Entity("PowderCoating.Core.Entities.Account", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("BillLineItems");
|
b.Navigation("BillLineItems");
|
||||||
|
|||||||
@@ -1495,8 +1495,20 @@ public class InventoryController : Controller
|
|||||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||||
ViewBag.AiInventoryAssistEnabled = await _subscriptionService.IsAiInventoryAssistEnabledAsync(companyId);
|
ViewBag.AiInventoryAssistEnabled = await _subscriptionService.IsAiInventoryAssistEnabledAsync(companyId);
|
||||||
|
|
||||||
var vendors = await _unitOfWork.Vendors.FindAsync(v => v.CompanyId == companyId);
|
var vendors = (await _unitOfWork.Vendors.FindAsync(v => v.IsActive, false, v => v.Categories))
|
||||||
ViewBag.Vendors = new SelectList(vendors.Where(s => s.IsActive).OrderBy(s => s.CompanyName), "Id", "CompanyName");
|
.OrderBy(v => v.CompanyName).ToList();
|
||||||
|
ViewBag.Vendors = new SelectList(vendors, "Id", "CompanyName");
|
||||||
|
|
||||||
|
// Build {categoryId: [vendorId, ...]} so the inventory form can filter vendors by category
|
||||||
|
var categoryVendorMap = new Dictionary<string, List<int>>();
|
||||||
|
foreach (var v in vendors)
|
||||||
|
foreach (var cat in v.Categories)
|
||||||
|
{
|
||||||
|
var key = cat.Id.ToString();
|
||||||
|
if (!categoryVendorMap.ContainsKey(key)) categoryVendorMap[key] = new List<int>();
|
||||||
|
categoryVendorMap[key].Add(v.Id);
|
||||||
|
}
|
||||||
|
ViewBag.CategoryVendorMapJson = System.Text.Json.JsonSerializer.Serialize(categoryVendorMap);
|
||||||
|
|
||||||
// Load categories from lookup table
|
// Load categories from lookup table
|
||||||
var allCategories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId);
|
var allCategories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId);
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ public class VendorsController : Controller
|
|||||||
public async Task<IActionResult> Create(bool inline = false)
|
public async Task<IActionResult> Create(bool inline = false)
|
||||||
{
|
{
|
||||||
await PopulateExpenseAccountsAsync();
|
await PopulateExpenseAccountsAsync();
|
||||||
|
await PopulateVendorCategoriesAsync();
|
||||||
if (inline)
|
if (inline)
|
||||||
return PartialView(new CreateVendorDto());
|
return PartialView(new CreateVendorDto());
|
||||||
return View(new CreateVendorDto());
|
return View(new CreateVendorDto());
|
||||||
@@ -207,6 +208,7 @@ public class VendorsController : Controller
|
|||||||
return Json(new { success = false, errors });
|
return Json(new { success = false, errors });
|
||||||
}
|
}
|
||||||
await PopulateExpenseAccountsAsync();
|
await PopulateExpenseAccountsAsync();
|
||||||
|
await PopulateVendorCategoriesAsync(dto.CategoryIds);
|
||||||
return View(dto);
|
return View(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +218,12 @@ public class VendorsController : Controller
|
|||||||
var vendor = _mapper.Map<Vendor>(dto);
|
var vendor = _mapper.Map<Vendor>(dto);
|
||||||
vendor.CompanyId = currentUser!.CompanyId;
|
vendor.CompanyId = currentUser!.CompanyId;
|
||||||
|
|
||||||
|
if (dto.CategoryIds.Any())
|
||||||
|
{
|
||||||
|
var cats = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => dto.CategoryIds.Contains(c.Id));
|
||||||
|
vendor.Categories = cats.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
await _unitOfWork.Vendors.AddAsync(vendor);
|
await _unitOfWork.Vendors.AddAsync(vendor);
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
|
|
||||||
@@ -247,14 +255,16 @@ public class VendorsController : Controller
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vendor = await _unitOfWork.Vendors.GetByIdAsync(id.Value);
|
var vendor = await _unitOfWork.Vendors.GetByIdAsync(id.Value, false, v => v.Categories);
|
||||||
if (vendor == null)
|
if (vendor == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dto = _mapper.Map<UpdateVendorDto>(vendor);
|
var dto = _mapper.Map<UpdateVendorDto>(vendor);
|
||||||
|
dto.CategoryIds = vendor.Categories.Select(c => c.Id).ToList();
|
||||||
await PopulateExpenseAccountsAsync();
|
await PopulateExpenseAccountsAsync();
|
||||||
|
await PopulateVendorCategoriesAsync(dto.CategoryIds);
|
||||||
return View(dto);
|
return View(dto);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -282,18 +292,27 @@ public class VendorsController : Controller
|
|||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
await PopulateExpenseAccountsAsync();
|
await PopulateExpenseAccountsAsync();
|
||||||
|
await PopulateVendorCategoriesAsync(dto.CategoryIds);
|
||||||
return View(dto);
|
return View(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vendor = await _unitOfWork.Vendors.GetByIdAsync(id);
|
var vendor = await _unitOfWork.Vendors.GetByIdAsync(id, false, v => v.Categories);
|
||||||
if (vendor == null)
|
if (vendor == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
_mapper.Map(dto, vendor);
|
_mapper.Map(dto, vendor);
|
||||||
|
|
||||||
|
vendor.Categories.Clear();
|
||||||
|
if (dto.CategoryIds.Any())
|
||||||
|
{
|
||||||
|
var cats = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => dto.CategoryIds.Contains(c.Id));
|
||||||
|
foreach (var cat in cats) vendor.Categories.Add(cat);
|
||||||
|
}
|
||||||
|
|
||||||
await _unitOfWork.Vendors.UpdateAsync(vendor);
|
await _unitOfWork.Vendors.UpdateAsync(vendor);
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
|
|
||||||
@@ -413,6 +432,20 @@ public class VendorsController : Controller
|
|||||||
/// purchases) in addition to regular operating expenses. A "— None —" placeholder is prepended so
|
/// purchases) in addition to regular operating expenses. A "— None —" placeholder is prepended so
|
||||||
/// the field is optional — not every vendor needs a default account pre-set.
|
/// the field is optional — not every vendor needs a default account pre-set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Populates ViewBag.VendorCategories with active inventory categories for the checkbox list,
|
||||||
|
/// and ViewBag.SelectedCategoryIds with the IDs already assigned to the vendor being edited.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PopulateVendorCategoriesAsync(IEnumerable<int>? selectedIds = null)
|
||||||
|
{
|
||||||
|
var companyId = (await _userManager.GetUserAsync(User))!.CompanyId;
|
||||||
|
var cats = (await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId && c.IsActive))
|
||||||
|
.OrderBy(c => c.DisplayOrder)
|
||||||
|
.ToList();
|
||||||
|
ViewBag.VendorCategories = cats;
|
||||||
|
ViewBag.SelectedCategoryIds = (selectedIds ?? Enumerable.Empty<int>()).ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PopulateExpenseAccountsAsync()
|
private async Task PopulateExpenseAccountsAsync()
|
||||||
{
|
{
|
||||||
var accounts = (await _unitOfWork.Accounts.FindAsync(
|
var accounts = (await _unitOfWork.Accounts.FindAsync(
|
||||||
|
|||||||
@@ -345,6 +345,10 @@
|
|||||||
<option value="">Select vendor</option>
|
<option value="">Select vendor</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="vendor-filter-note" class="form-text d-none">
|
||||||
|
<i class="bi bi-funnel me-1 text-info"></i><span class="text-info">Showing vendors for this category.</span>
|
||||||
|
<a href="#" id="vendor-filter-clear" class="ms-1">Show all</a>
|
||||||
|
</div>
|
||||||
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,4 +442,38 @@
|
|||||||
{
|
{
|
||||||
<script src="~/js/inventory-label-scan.js"></script>
|
<script src="~/js/inventory-label-scan.js"></script>
|
||||||
}
|
}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const categoryVendorMap = @Html.Raw(ViewBag.CategoryVendorMapJson ?? "{}");
|
||||||
|
const vendorSelect = document.getElementById('field-vendor');
|
||||||
|
const allVendorOptions = Array.from(vendorSelect.options).map(o => ({ v: o.value, t: o.text }));
|
||||||
|
|
||||||
|
function filterVendors(catId, forceAll) {
|
||||||
|
const vendorIds = (!forceAll && catId) ? (categoryVendorMap[catId] || []) : [];
|
||||||
|
const isFiltered = vendorIds.length > 0;
|
||||||
|
const currentVal = vendorSelect.value;
|
||||||
|
|
||||||
|
vendorSelect.innerHTML = '';
|
||||||
|
allVendorOptions.forEach(function (opt) {
|
||||||
|
if (!isFiltered || !opt.v || opt.v === '__new__' || vendorIds.includes(Number(opt.v)))
|
||||||
|
vendorSelect.add(new Option(opt.t, opt.v));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.from(vendorSelect.options).some(o => o.value === currentVal))
|
||||||
|
vendorSelect.value = currentVal;
|
||||||
|
|
||||||
|
document.getElementById('vendor-filter-note').classList.toggle('d-none', !isFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('field-category').addEventListener('change', function () {
|
||||||
|
filterVendors(this.value, false);
|
||||||
|
});
|
||||||
|
document.getElementById('vendor-filter-clear')?.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
filterVendors(document.getElementById('field-category').value, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
filterVendors(document.getElementById('field-category').value, false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
<option value="">Select vendor</option>
|
<option value="">Select vendor</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="vendor-filter-note" class="form-text d-none">
|
||||||
|
<i class="bi bi-funnel me-1 text-info"></i><span class="text-info">Showing vendors for this category.</span>
|
||||||
|
<a href="#" id="vendor-filter-clear" class="ms-1">Show all</a>
|
||||||
|
</div>
|
||||||
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -457,4 +461,39 @@
|
|||||||
{
|
{
|
||||||
<script src="~/js/inventory-label-scan.js"></script>
|
<script src="~/js/inventory-label-scan.js"></script>
|
||||||
}
|
}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const categoryVendorMap = @Html.Raw(ViewBag.CategoryVendorMapJson ?? "{}");
|
||||||
|
const vendorSelect = document.getElementById('field-vendor');
|
||||||
|
const allVendorOptions = Array.from(vendorSelect.options).map(o => ({ v: o.value, t: o.text }));
|
||||||
|
|
||||||
|
function filterVendors(catId, forceAll) {
|
||||||
|
const vendorIds = (!forceAll && catId) ? (categoryVendorMap[catId] || []) : [];
|
||||||
|
const isFiltered = vendorIds.length > 0;
|
||||||
|
const currentVal = vendorSelect.value;
|
||||||
|
|
||||||
|
vendorSelect.innerHTML = '';
|
||||||
|
allVendorOptions.forEach(function (opt) {
|
||||||
|
if (!isFiltered || !opt.v || opt.v === '__new__' || vendorIds.includes(Number(opt.v)))
|
||||||
|
vendorSelect.add(new Option(opt.t, opt.v));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.from(vendorSelect.options).some(o => o.value === currentVal))
|
||||||
|
vendorSelect.value = currentVal;
|
||||||
|
|
||||||
|
document.getElementById('vendor-filter-note').classList.toggle('d-none', !isFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('field-category').addEventListener('change', function () {
|
||||||
|
filterVendors(this.value, false);
|
||||||
|
});
|
||||||
|
document.getElementById('vendor-filter-clear')?.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
filterVendors(document.getElementById('field-category').value, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply on load — Edit already has a category selected
|
||||||
|
filterVendors(document.getElementById('field-category').value, false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Supply Categories -->
|
||||||
|
@if (ViewBag.VendorCategories is IEnumerable<PowderCoating.Core.Entities.InventoryCategoryLookup> cats && cats.Any())
|
||||||
|
{
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<i class="bi bi-tags me-2 text-primary"></i>Supply Categories
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
@foreach (var cat in cats)
|
||||||
|
{
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
name="CategoryIds" value="@cat.Id" id="cat_@cat.Id" />
|
||||||
|
<label class="form-check-label" for="cat_@cat.Id">@cat.DisplayName</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Tag this vendor with the types of supplies they provide. Used to filter the vendor list when adding inventory items.</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5 class="border-bottom pb-2 mb-3">
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
|||||||
@@ -198,6 +198,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Supply Categories -->
|
||||||
|
@if (ViewBag.VendorCategories is IEnumerable<PowderCoating.Core.Entities.InventoryCategoryLookup> cats && cats.Any())
|
||||||
|
{
|
||||||
|
var selectedCatIds = (HashSet<int>)ViewBag.SelectedCategoryIds;
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<i class="bi bi-tags me-2 text-primary"></i>Supply Categories
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
@foreach (var cat in cats)
|
||||||
|
{
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
name="CategoryIds" value="@cat.Id" id="cat_@cat.Id"
|
||||||
|
@(selectedCatIds.Contains(cat.Id) ? "checked" : "") />
|
||||||
|
<label class="form-check-label" for="cat_@cat.Id">@cat.DisplayName</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Tag this vendor with the types of supplies they provide. Used to filter the vendor list when adding inventory items.</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5 class="border-bottom pb-2 mb-3">
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user