Merge dev: inventory label scanner improvements and AI lookup parity

This commit is contained in:
2026-05-03 20:30:44 -04:00
25 changed files with 21971 additions and 27 deletions
@@ -72,17 +72,20 @@
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0 d-flex align-items-center">
<i class="bi bi-palette me-2 text-primary"></i>Product Details
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? true))
<button type="button" class="btn btn-sm btn-primary ms-2" id="smart-lookup-btn">
<i class="bi bi-search me-1"></i>Lookup
</button>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="ai-lookup-btn">
<i class="bi bi-stars me-1"></i>AI Lookup
<button type="button" class="btn btn-sm btn-outline-secondary ms-1" id="scan-label-btn" title="Scan a powder bag label with your camera">
<i class="bi bi-qr-code-scan me-1"></i>Scan Label
</button>
}
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Product Details"
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. AI Lookup can auto-fill these fields from a manufacturer name or part number. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 6070%). Both values are used to calculate Powder Needed on quotes and jobs.">
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields — it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 6070%). Both values are used to calculate Powder Needed on quotes and jobs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -123,6 +126,28 @@
</div>
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
<i class="bi bi-file-earmark-pdf"></i>
</a>
</div>
<span asp-validation-for="SdsUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
<i class="bi bi-file-earmark-text"></i>
</a>
</div>
<span asp-validation-for="TdsUrl" class="text-danger"></span>
</div>
<input asp-for="ImageUrl" type="hidden" id="field-imageurl" />
<div class="col-12" id="wrap-imagepreview" style="display:@(Model.ImageUrl != null ? "" : "none");">
<label class="form-label text-muted small">Product Image (from AI Lookup)</label>
@@ -374,8 +399,18 @@
</div>
</div>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<partial name="_LabelScanModal" />
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>const inventoryFormIsCreate = true;</script>
<partial name="_InventoryColorFamilyScripts" />
<script src="~/js/inventory-catalog-lookup.js"></script>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<script src="~/js/inventory-label-scan.js"></script>
}
}
@@ -190,6 +190,26 @@
</p>
</div>
}
@if (!string.IsNullOrEmpty(Model.SdsUrl) || !string.IsNullOrEmpty(Model.TdsUrl))
{
<div class="col-12">
<label class="text-muted small mb-1">Data Sheets</label>
<div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrEmpty(Model.SdsUrl))
{
<a href="@Model.SdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-pdf me-1"></i>Safety Data Sheet
</a>
}
@if (!string.IsNullOrEmpty(Model.TdsUrl))
{
<a href="@Model.TdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-text me-1"></i>Technical Data Sheet
</a>
}
</div>
</div>
}
}
@if (!string.IsNullOrEmpty(Model.Notes))
{
@@ -74,10 +74,13 @@
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0 d-flex align-items-center">
<i class="bi bi-palette me-2 text-primary"></i>Product Details
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? true))
<button type="button" class="btn btn-sm btn-primary ms-2" id="smart-lookup-btn">
<i class="bi bi-search me-1"></i>Lookup
</button>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="ai-lookup-btn">
<i class="bi bi-stars me-1"></i>AI Lookup
<button type="button" class="btn btn-sm btn-outline-secondary ms-1" id="scan-label-btn" title="Scan a powder bag label with your camera">
<i class="bi bi-qr-code-scan me-1"></i>Scan Label
</button>
}
</h5>
@@ -125,6 +128,28 @@
</div>
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
<i class="bi bi-file-earmark-pdf"></i>
</a>
</div>
<span asp-validation-for="SdsUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
<i class="bi bi-file-earmark-text"></i>
</a>
</div>
<span asp-validation-for="TdsUrl" class="text-danger"></span>
</div>
<input asp-for="ImageUrl" type="hidden" id="field-imageurl" />
<div class="col-12" id="wrap-imagepreview" style="display:@(Model.ImageUrl != null ? "" : "none");">
<label class="form-label text-muted small">Product Image (from AI Lookup)</label>
@@ -394,7 +419,17 @@
</div>
</div>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<partial name="_LabelScanModal" />
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<partial name="_InventoryColorFamilyScripts" />
<script src="~/js/inventory-catalog-lookup.js"></script>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<script src="~/js/inventory-label-scan.js"></script>
}
}
@@ -36,7 +36,7 @@
// ── Category → IsCoating map + show/hide coating section ─────────────
const categorySelect = document.getElementById('field-category');
const coatingSection = document.getElementById('coating-specs-section');
const aiBtn = document.getElementById('ai-lookup-btn');
const smartLookupBtn = document.getElementById('smart-lookup-btn');
let coatingMap = {};
if (categorySelect && categorySelect.dataset.coatingMap) {
@@ -53,7 +53,7 @@
function updateCoatingVisibility(catId) {
const show = isCoatingCategory(catId);
if (coatingSection) coatingSection.style.display = show ? '' : 'none';
if (aiBtn) aiBtn.style.display = show ? '' : 'none';
if (smartLookupBtn) smartLookupBtn.style.display = show ? '' : 'none';
const samplePanelSection = document.getElementById('sample-panel-section');
if (samplePanelSection) samplePanelSection.style.display = show ? '' : 'none';
coatingOnlyFields.forEach(id => {
@@ -253,11 +253,8 @@
});
// ── AI Lookup ─────────────────────────────────────────────────────────
const btn = document.getElementById('ai-lookup-btn');
const statusEl = document.getElementById('ai-lookup-status');
if (!btn) return;
function showBadMatchBtn() {
if (document.getElementById('ai-bad-match-btn')) return; // already shown
const b = document.createElement('button');
@@ -297,14 +294,15 @@
showStatus('info', '<i class="bi bi-check-circle me-1"></i>Fields cleared. Update any details above and click <em>AI Lookup</em> again.');
}
});
btn.insertAdjacentElement('afterend', b);
const lookupBtn = document.getElementById('smart-lookup-btn');
if (lookupBtn) lookupBtn.insertAdjacentElement('afterend', b);
}
function hideBadMatchBtn() {
document.getElementById('ai-bad-match-btn')?.remove();
}
btn.addEventListener('click', async () => {
async function performAiLookup() {
const manufacturer = document.getElementById('field-manufacturer')?.value?.trim() || '';
const colorName = document.getElementById('field-colorname')?.value?.trim() || '';
const colorCode = document.getElementById('field-colorcode')?.value?.trim() || '';
@@ -325,8 +323,6 @@
const effectiveColorName = colorName || itemName;
hideBadMatchBtn();
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
showInfo('Searching for product specifications…', 'AI Lookup');
try {
@@ -454,6 +450,21 @@
aiFilledImage = true;
}
// SDS / TDS document URLs — fill inputs and show open-link buttons
const fillDocUrl = (fieldId, linkId, url, label) => {
if (!url) return;
const el = document.getElementById(fieldId);
const link = document.getElementById(linkId);
if (el && (forceRefill || !el.value.trim())) {
el.value = url;
filled.push(label);
if (!aiFilledFields.includes(fieldId)) aiFilledFields.push(fieldId);
}
if (link) { link.href = url; link.classList.remove('d-none'); }
};
fillDocUrl('field-sdsurl', 'field-sdsurl-link', data.sdsUrl, 'SDS');
fillDocUrl('field-tdsurl', 'field-tdsurl-link', data.tdsUrl, 'TDS');
// Build a persistent "needs more info" tip if key identity fields are still unknown
const missingHints = [];
if (!data.manufacturer && !document.getElementById('field-manufacturer')?.value?.trim())
@@ -487,11 +498,12 @@
showError('Request failed: ' + err.message, 'AI Lookup Error');
showStatus('danger', 'Request failed: ' + err.message);
} finally {
forceRefill = false; // reset after each run
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-stars me-1"></i>AI Lookup';
forceRefill = false; // reset after each run
}
});
}
// Expose so inventory-catalog-lookup.js can fall back to AI when catalog misses
window._runInventoryAiLookup = performAiLookup;
function debugPanel(data) {
const json = JSON.stringify(data, null, 2);
@@ -0,0 +1,114 @@
<!-- Add-stock modal: shown when label scan matches an existing inventory item -->
<div class="modal fade" id="addStockModal" tabindex="-1" aria-labelledby="addStockModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:420px;">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title" id="addStockModalLabel">
<i class="bi bi-box-seam me-2 text-success"></i>Already in Inventory
</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pb-2">
<p class="mb-1">
<strong id="add-stock-item-name" class="text-body"></strong> is already in your inventory.
</p>
<p class="text-muted small mb-3">
Current stock: <strong id="add-stock-current-qty"></strong>
</p>
<div class="mb-3">
<label class="form-label fw-semibold small">Quantity to Add <span class="text-danger">*</span></label>
<div class="input-group input-group-sm">
<input type="number" id="add-stock-qty" class="form-control" min="0.01" step="0.01" placeholder="e.g. 5">
<span class="input-group-text" id="add-stock-uom-label">lbs</span>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold small">Unit Cost <span class="text-muted fw-normal">(optional — updates item cost)</span></label>
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="number" id="add-stock-cost" class="form-control" min="0" step="0.01" placeholder="Leave blank to keep current">
</div>
</div>
<div class="mb-2">
<label class="form-label fw-semibold small">Notes <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" id="add-stock-notes" class="form-control form-control-sm" placeholder="e.g. New bag received">
</div>
<div id="add-stock-status" class="d-none small mt-2"></div>
</div>
<div class="modal-footer flex-column align-items-stretch gap-2 py-2">
<button id="add-stock-confirm-btn" type="button" class="btn btn-success">
<i class="bi bi-plus-circle me-1"></i>Add Stock
</button>
<button id="add-stock-new-btn" type="button" class="btn btn-link btn-sm text-muted">
Add as a new entry instead (e.g. different lot)
</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="labelScanModal" tabindex="-1" aria-labelledby="labelScanModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:480px;">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title" id="labelScanModalLabel">
<i class="bi bi-qr-code-scan me-2 text-primary"></i>Scan Powder Label
</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0 position-relative bg-black" style="min-height:300px;">
<!-- Live camera feed -->
<video id="scan-video" autoplay playsinline muted
style="width:100%;display:block;max-height:400px;object-fit:cover;"></video>
<!-- Hidden canvas used for QR analysis and frame capture -->
<canvas id="scan-canvas" style="display:none;"></canvas>
<!-- Targeting overlay: darkened edges with a bright center window -->
<div style="position:absolute;inset:0;pointer-events:none;">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="scan-mask">
<rect width="100%" height="100%" fill="white"/>
<rect x="15%" y="20%" width="70%" height="60%" rx="8" fill="black"/>
</mask>
</defs>
<rect width="100%" height="100%" fill="rgba(0,0,0,0.45)" mask="url(#scan-mask)"/>
<!-- Corner brackets -->
<g stroke="#fff" stroke-width="3" fill="none" opacity="0.9">
<path d="M 15% 28% L 15% 20% L 23% 20%"/>
<path d="M 77% 20% L 85% 20% L 85% 28%"/>
<path d="M 85% 72% L 85% 80% L 77% 80%"/>
<path d="M 23% 80% L 15% 80% L 15% 72%"/>
</g>
</svg>
</div>
<!-- Processing overlay: shown while the server lookup is running -->
<div id="scan-processing" style="display:none;position:absolute;inset:0;z-index:10;background:rgba(0,0,0,0.88);align-items:center;justify-content:center;flex-direction:column;color:#fff;text-align:center;padding:1.5rem;">
<div class="spinner-border text-light mb-3" style="width:2.5rem;height:2.5rem;"></div>
<div id="scan-processing-msg" class="fw-medium fs-6">Looking up product…</div>
<div class="text-white-50 small mt-1">This may take a few seconds</div>
</div>
<!-- Status inside the modal -->
<div id="scan-modal-status" class="alert alert-info py-2 small mb-0 mt-2 d-none mx-2 mb-2"
style="position:absolute;bottom:0;left:0;right:0;margin:8px !important;"></div>
</div>
<div class="modal-footer flex-column align-items-stretch py-2 gap-2">
<div class="text-muted small text-center">
<i class="bi bi-magic me-1"></i>QR codes are detected automatically.
</div>
<div id="scan-shutter-wrap" class="d-none">
<div class="text-muted small text-center mb-2">No QR code? Tap to read the label text with AI.</div>
<button id="scan-shutter-btn" type="button" class="btn btn-secondary w-100">
<i class="bi bi-camera me-1"></i>Scan Label Text
</button>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,158 @@
@model PowderCoating.Application.DTOs.Inventory.PowderCatalogStatsDto
@{
ViewData["Title"] = "Powder Catalog";
ViewData["PageIcon"] = "bi-palette2";
Layout = "_Layout";
}
<div class="container-fluid">
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-permanent alert-dismissible fade show mb-3" role="alert">
<i class="bi bi-check-circle me-2"></i>@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-permanent alert-dismissible fade show mb-3" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- Stats cards -->
<div class="row g-3 mb-4">
<div class="col-sm-6 col-lg-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-3 p-3 bg-primary bg-opacity-10">
<i class="bi bi-collection fs-4 text-primary"></i>
</div>
<div>
<div class="fs-3 fw-bold">@Model.TotalProducts.ToString("N0")</div>
<div class="text-muted small">Total Products</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-3 p-3 bg-success bg-opacity-10">
<i class="bi bi-check-circle fs-4 text-success"></i>
</div>
<div>
<div class="fs-3 fw-bold">@Model.ActiveProducts.ToString("N0")</div>
<div class="text-muted small">Active</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-3 p-3 bg-warning bg-opacity-10">
<i class="bi bi-slash-circle fs-4 text-warning"></i>
</div>
<div>
<div class="fs-3 fw-bold">@Model.DiscontinuedProducts.ToString("N0")</div>
<div class="text-muted small">Discontinued</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-3 p-3 bg-info bg-opacity-10">
<i class="bi bi-building fs-4 text-info"></i>
</div>
<div>
<div class="fs-3 fw-bold">@Model.VendorCount</div>
<div class="text-muted small">
@(Model.VendorCount == 1 ? "Vendor" : "Vendors")
@if (Model.LastImportedAt.HasValue)
{
<br /><span class="text-muted" style="font-size:.75rem;">Last sync @Model.LastImportedAt.Value.ToString("MMM d, yyyy")</span>
}
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="rounded-3 p-3 bg-purple bg-opacity-10" style="background:rgba(111,66,193,.1)">
<i class="bi bi-qr-code-scan fs-4" style="color:#6f42c1;"></i>
</div>
<div>
<div class="fs-3 fw-bold">@Model.UserContributedProducts.ToString("N0")</div>
<div class="text-muted small">Tenant Contributed</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- Import card -->
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-transparent border-bottom">
<h5 class="mb-0"><i class="bi bi-cloud-upload me-2 text-primary"></i>Import Catalog Data</h5>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Upload a Prismatic Powders scrape JSON file (the <code>prismatic_powders.json</code> format with
a top-level <code>results</code> array). Existing SKUs are updated in-place; new ones are inserted.
Discontinued products remain in the catalog flagged as <code>IsDiscontinued</code>.
</p>
<form asp-action="Import" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label fw-medium">Vendor Name</label>
<input type="text" name="vendorName" value="Prismatic Powders" class="form-control" required />
<div class="form-text">Must match exactly — used as the upsert key alongside SKU.</div>
</div>
<div class="mb-3">
<label class="form-label fw-medium">JSON File <span class="text-danger">*</span></label>
<input type="file" name="file" accept=".json" class="form-control" required />
<div class="form-text">Max 50 MB. Must be the scraped format with <code>results[]</code> array.</div>
</div>
<button type="submit" class="btn btn-primary" id="btn-import">
<i class="bi bi-upload me-2"></i>Import
</button>
</form>
</div>
</div>
</div>
<!-- Info / how it works card -->
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-transparent border-bottom">
<h5 class="mb-0"><i class="bi bi-info-circle me-2 text-primary"></i>How It Works</h5>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0" style="line-height:2;">
<li><i class="bi bi-check2 text-success me-2"></i><strong>Platform-level:</strong> One shared catalog, no per-tenant copies.</li>
<li><i class="bi bi-check2 text-success me-2"></i><strong>Catalog-first lookup:</strong> When a tenant adds inventory, the form searches here before calling the AI API.</li>
<li><i class="bi bi-check2 text-success me-2"></i><strong>Auto-fill:</strong> Selecting a result fills color name, manufacturer, part number, unit cost, SDS/TDS links, and product image.</li>
<li><i class="bi bi-check2 text-success me-2"></i><strong>Discontinued:</strong> Flagged <code>IsDiscontinued = true</code> — never hidden, always available for historical lookups.</li>
<li><i class="bi bi-clock text-muted me-2"></i><strong>Phase 2:</strong> Monthly price sync + push to tenant inventory items.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
document.querySelector('form').addEventListener('submit', function () {
var btn = document.getElementById('btn-import');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Importing…';
});
</script>
@@ -1308,6 +1308,10 @@
<i class="bi bi-database-fill-gear"></i>
<span>Seed Data</span>
</a>
<a asp-controller="PowderCatalog" asp-action="Index" class="nav-link">
<i class="bi bi-palette2"></i>
<span>Powder Catalog</span>
</a>
<a asp-controller="ManufacturerLookupPatterns" asp-action="Index" class="nav-link">
<i class="bi bi-link-45deg"></i>
<span>Manufacturer Lookup Patterns</span>