Merge dev: inventory label scanner improvements and AI lookup parity
This commit is contained in:
@@ -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: 60–70%). 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: 60–70%). 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>
|
||||
|
||||
Reference in New Issue
Block a user