Compare commits
3 Commits
5e3b0b9ddf
...
7de65910e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7de65910e3 | |||
| 145da7b5c4 | |||
| 4182286a31 |
@@ -54,4 +54,11 @@ public interface IInventoryAiLookupService
|
|||||||
/// using Claude vision. Used by the in-browser label scanner.
|
/// using Claude vision. Used by the in-browser label scanner.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<InventoryAiLookupResult> ScanLabelAsync(string base64Image, string mediaType);
|
Task<InventoryAiLookupResult> ScanLabelAsync(string base64Image, string mediaType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches a Technical Data Sheet URL and extracts cure temperature and cure time.
|
||||||
|
/// Called when the main lookup found a TDS URL but cure specs are still missing.
|
||||||
|
/// Returns Success=false silently (no UI error) when the TDS is a PDF or unreachable.
|
||||||
|
/// </summary>
|
||||||
|
Task<InventoryAiLookupResult> FetchTdsCureSpecsAsync(string tdsUrl, string? colorName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,6 +478,108 @@ Rules:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches a TDS URL (if it is an HTML page — PDFs are silently skipped) and asks Claude
|
||||||
|
/// to extract cure temperature and cure time only. Uses the same <see cref="FetchPageAsync"/>
|
||||||
|
/// pipeline so JSON-LD and document links are still extracted before stripping HTML.
|
||||||
|
/// Returns <see cref="InventoryAiLookupResult.Success"/> = <c>false</c> without an error
|
||||||
|
/// message when the page could not be fetched (PDF, redirect loop, etc.) so the caller
|
||||||
|
/// can merge results without surfacing a user-visible error.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<InventoryAiLookupResult> FetchTdsCureSpecsAsync(string tdsUrl, string? colorName)
|
||||||
|
{
|
||||||
|
var apiKey = _config["AI:Anthropic:ApiKey"];
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey) || apiKey.StartsWith("your-"))
|
||||||
|
return new InventoryAiLookupResult { Success = false };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (pageContent, _) = await FetchPageAsync(tdsUrl);
|
||||||
|
if (string.IsNullOrWhiteSpace(pageContent))
|
||||||
|
{
|
||||||
|
// PDF or unreachable — nothing to send to Claude
|
||||||
|
_logger.LogInformation("TDS cure lookup skipped (PDF or unreachable): {Url}", tdsUrl);
|
||||||
|
return new InventoryAiLookupResult { Success = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targeted prompt: we only need cure specs from this document
|
||||||
|
const string curePrompt = @"You are reading a Technical Data Sheet (TDS) for a powder coating product.
|
||||||
|
Extract ONLY the cure schedule. Respond with a valid JSON object — no markdown, no explanation:
|
||||||
|
|
||||||
|
{
|
||||||
|
""cureTemperatureF"": number or null,
|
||||||
|
""cureTimeMinutes"": number or null,
|
||||||
|
""reasoning"": ""one sentence: what cure schedule you found""
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- cureTemperatureF: the recommended cure temperature in °F. Convert from °C if needed (multiply by 1.8 + 32). If a range is given use the midpoint. Most powders cure 325–400 °F.
|
||||||
|
- cureTimeMinutes: the hold time at cure temperature in minutes (NOT total oven time). Typically 10–20 min.
|
||||||
|
- If neither value can be found in the document, return null for both.";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Technical Data Sheet content:");
|
||||||
|
if (!string.IsNullOrWhiteSpace(colorName)) sb.AppendLine($"Product: {colorName}");
|
||||||
|
sb.AppendLine($"Source: {tdsUrl}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(pageContent);
|
||||||
|
|
||||||
|
var client = new AnthropicClient(apiKey);
|
||||||
|
var messageRequest = new MessageParameters
|
||||||
|
{
|
||||||
|
Model = "claude-sonnet-4-6",
|
||||||
|
MaxTokens = 256,
|
||||||
|
SystemMessage = curePrompt,
|
||||||
|
Messages = new List<Message>
|
||||||
|
{
|
||||||
|
new Message
|
||||||
|
{
|
||||||
|
Role = RoleType.User,
|
||||||
|
Content = new List<ContentBase> { new TextContent { Text = sb.ToString() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await client.Messages.GetClaudeMessageAsync(messageRequest);
|
||||||
|
var rawText = (response.FirstMessage?.Text
|
||||||
|
?? response.Content.OfType<TextContent>().FirstOrDefault()?.Text
|
||||||
|
?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (rawText.StartsWith("```"))
|
||||||
|
{
|
||||||
|
var start = rawText.IndexOf('\n') + 1;
|
||||||
|
var end = rawText.LastIndexOf("```");
|
||||||
|
rawText = rawText[start..end].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawText.StartsWith("{"))
|
||||||
|
{
|
||||||
|
var j = rawText.IndexOf('{');
|
||||||
|
var k = rawText.LastIndexOf('}');
|
||||||
|
if (j >= 0 && k > j) rawText = rawText[j..(k + 1)];
|
||||||
|
else return new InventoryAiLookupResult { Success = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = JsonSerializer.Deserialize<JsonElement>(rawText);
|
||||||
|
var result = new InventoryAiLookupResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
CureTemperatureF = GetDecimal(parsed, "cureTemperatureF"),
|
||||||
|
CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"),
|
||||||
|
Reasoning = GetString(parsed, "reasoning"),
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("TDS cure lookup for {Url}: temp={Temp}°F, time={Time}min ({Reasoning})",
|
||||||
|
tdsUrl, result.CureTemperatureF, result.CureTimeMinutes, result.Reasoning);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "TDS cure spec fetch failed for {Url}", tdsUrl);
|
||||||
|
return new InventoryAiLookupResult { Success = false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Manufacturer URL pattern: build direct product page URL ───────────────
|
// ── Manufacturer URL pattern: build direct product page URL ───────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -679,6 +679,11 @@ public class InventoryController : Controller
|
|||||||
return Json(new { success = false, errorMessage = "AI Inventory Assist is not enabled for your account. Contact your administrator." });
|
return Json(new { success = false, errorMessage = "AI Inventory Assist is not enabled for your account. Contact your administrator." });
|
||||||
|
|
||||||
var result = await _aiLookupService.LookupAsync(manufacturer, colorName, colorCode, partNumber);
|
var result = await _aiLookupService.LookupAsync(manufacturer, colorName, colorCode, partNumber);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
await EnrichFromCatalogAsync(result, autoContribute: true);
|
||||||
|
await ApplyTdsCureFallbackAsync(result, colorName);
|
||||||
|
}
|
||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,9 +706,117 @@ public class InventoryController : Controller
|
|||||||
return Json(new { success = false, errorMessage = "No product URL provided." });
|
return Json(new { success = false, errorMessage = "No product URL provided." });
|
||||||
|
|
||||||
var result = await _aiLookupService.LookupByUrlAsync(productUrl, colorName);
|
var result = await _aiLookupService.LookupByUrlAsync(productUrl, colorName);
|
||||||
|
if (result.Success) await ApplyTdsCureFallbackAsync(result, colorName);
|
||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up <paramref name="result"/> in the platform powder catalog by SKU + manufacturer.
|
||||||
|
/// If a match is found, catalog values overwrite Claude-inferred ones for spec fields
|
||||||
|
/// (catalog is the authoritative source) and fill gaps for URL/price fields.
|
||||||
|
/// If no match and <paramref name="autoContribute"/> is true, inserts a new catalog entry
|
||||||
|
/// so future lookups resolve instantly without an API call.
|
||||||
|
/// Returns (wasInCatalog, addedToCatalog) so callers can surface UI badges.
|
||||||
|
/// Mutates <paramref name="result"/> in place.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(bool wasInCatalog, bool addedToCatalog)> EnrichFromCatalogAsync(
|
||||||
|
InventoryAiLookupResult result, bool autoContribute)
|
||||||
|
{
|
||||||
|
var sku = result.ManufacturerPartNumber?.Trim();
|
||||||
|
var manufacturer = (result.Manufacturer ?? result.VendorName)?.Trim();
|
||||||
|
var colorName = result.ColorName?.Trim();
|
||||||
|
|
||||||
|
PowderCatalogItem? match = null;
|
||||||
|
if (!string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(manufacturer))
|
||||||
|
{
|
||||||
|
var skuLower = sku.ToLower();
|
||||||
|
var mfrLower = manufacturer.ToLower();
|
||||||
|
var hits = await _unitOfWork.PowderCatalog.FindAsync(p =>
|
||||||
|
p.Sku.ToLower() == skuLower && p.VendorName.ToLower().Contains(mfrLower));
|
||||||
|
match = hits.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match != null)
|
||||||
|
{
|
||||||
|
// Catalog is authoritative for spec fields — overwrite AI inference
|
||||||
|
if (match.Finish != null) result.Finish = match.Finish;
|
||||||
|
if (match.CureTemperatureF != null) result.CureTemperatureF = match.CureTemperatureF;
|
||||||
|
if (match.CureTimeMinutes != null) result.CureTimeMinutes = match.CureTimeMinutes;
|
||||||
|
if (match.ColorFamilies != null) result.ColorFamilies = match.ColorFamilies;
|
||||||
|
if (match.RequiresClearCoat != null) result.RequiresClearCoat = match.RequiresClearCoat;
|
||||||
|
if (match.CoverageSqFtPerLb != null) result.CoverageSqFtPerLb = match.CoverageSqFtPerLb;
|
||||||
|
if (match.TransferEfficiency != null) result.TransferEfficiency = match.TransferEfficiency;
|
||||||
|
// URL / price fields: fill gaps only — AI may have found something better
|
||||||
|
result.ImageUrl ??= match.ImageUrl;
|
||||||
|
result.SpecPageUrl ??= match.ProductUrl;
|
||||||
|
result.SdsUrl ??= match.SdsUrl;
|
||||||
|
result.TdsUrl ??= match.TdsUrl;
|
||||||
|
if (match.UnitPrice > 0) result.UnitCostPerLb ??= match.UnitPrice;
|
||||||
|
return (true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoContribute
|
||||||
|
|| string.IsNullOrEmpty(sku)
|
||||||
|
|| string.IsNullOrEmpty(manufacturer)
|
||||||
|
|| string.IsNullOrEmpty(colorName))
|
||||||
|
return (false, false);
|
||||||
|
|
||||||
|
// Auto-contribute: insert into platform catalog so future lookups/scans resolve instantly
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newItem = new PowderCatalogItem
|
||||||
|
{
|
||||||
|
VendorName = manufacturer,
|
||||||
|
Sku = sku,
|
||||||
|
ColorName = colorName,
|
||||||
|
CureTemperatureF = result.CureTemperatureF,
|
||||||
|
CureTimeMinutes = result.CureTimeMinutes,
|
||||||
|
Finish = result.Finish,
|
||||||
|
ColorFamilies = result.ColorFamilies,
|
||||||
|
RequiresClearCoat = result.RequiresClearCoat,
|
||||||
|
CoverageSqFtPerLb = result.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = result.TransferEfficiency,
|
||||||
|
ImageUrl = result.ImageUrl,
|
||||||
|
ProductUrl = result.SpecPageUrl,
|
||||||
|
SdsUrl = result.SdsUrl,
|
||||||
|
TdsUrl = result.TdsUrl,
|
||||||
|
IsUserContributed = true,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
await _unitOfWork.PowderCatalog.AddAsync(newItem);
|
||||||
|
await _unitOfWork.CompleteAsync();
|
||||||
|
_logger.LogInformation("Auto-contributed new catalog entry: {Manufacturer} {Sku}", manufacturer, sku);
|
||||||
|
return (false, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Unique constraint violation means another request beat us — not an error
|
||||||
|
_logger.LogInformation("Catalog auto-insert skipped (likely duplicate): {Message}", ex.Message);
|
||||||
|
return (false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If cure temperature or cure time is still missing after the primary lookup but a TDS URL
|
||||||
|
/// was returned, fetches that page and asks Claude to extract only the cure schedule.
|
||||||
|
/// Mutates <paramref name="result"/> in place; silently no-ops on failure so callers
|
||||||
|
/// can always return the result even if the TDS fetch does not help.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ApplyTdsCureFallbackAsync(InventoryAiLookupResult result, string? colorName)
|
||||||
|
{
|
||||||
|
if ((result.CureTemperatureF == null || result.CureTimeMinutes == null)
|
||||||
|
&& !string.IsNullOrEmpty(result.TdsUrl))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Cure specs missing after lookup; trying TDS at {Url}", result.TdsUrl);
|
||||||
|
var tds = await _aiLookupService.FetchTdsCureSpecsAsync(result.TdsUrl, colorName);
|
||||||
|
if (tds.Success)
|
||||||
|
{
|
||||||
|
if (result.CureTemperatureF == null) result.CureTemperatureF = tds.CureTemperatureF;
|
||||||
|
if (result.CureTimeMinutes == null) result.CureTimeMinutes = tds.CureTimeMinutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Accepts a base64 label photo or a decoded QR URL from the in-browser label scanner,
|
/// Accepts a base64 label photo or a decoded QR URL from the in-browser label scanner,
|
||||||
/// runs it through Claude (vision for photos, URL-fetch for QR), searches the platform
|
/// runs it through Claude (vision for photos, URL-fetch for QR), searches the platform
|
||||||
@@ -776,63 +889,15 @@ public class InventoryController : Controller
|
|||||||
if (!aiResult.Success)
|
if (!aiResult.Success)
|
||||||
return Json(new { success = false, errorMessage = aiResult.ErrorMessage });
|
return Json(new { success = false, errorMessage = aiResult.ErrorMessage });
|
||||||
|
|
||||||
// Search catalog by SKU first (most precise), then fall back to color name
|
|
||||||
var sku = aiResult.ManufacturerPartNumber?.Trim();
|
var sku = aiResult.ManufacturerPartNumber?.Trim();
|
||||||
var manufacturer = (aiResult.Manufacturer ?? aiResult.VendorName)?.Trim();
|
var manufacturer = (aiResult.Manufacturer ?? aiResult.VendorName)?.Trim();
|
||||||
var colorName = aiResult.ColorName?.Trim();
|
var colorName = aiResult.ColorName?.Trim();
|
||||||
|
|
||||||
PowderCatalogItem? catalogMatch = null;
|
// Catalog lookup, merge, and auto-contribute — same logic as AiLookup button
|
||||||
if (!string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(manufacturer))
|
var (wasInCatalog, addedToCatalog) = await EnrichFromCatalogAsync(aiResult, autoContribute: true);
|
||||||
{
|
|
||||||
var skuLower = sku.ToLower();
|
|
||||||
var mfrLower = manufacturer.ToLower();
|
|
||||||
var skuMatches = await _unitOfWork.PowderCatalog.FindAsync(p =>
|
|
||||||
p.Sku.ToLower() == skuLower && p.VendorName.ToLower().Contains(mfrLower));
|
|
||||||
catalogMatch = skuMatches.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
var wasInCatalog = catalogMatch != null;
|
// TDS cure fallback — same logic as AiLookup button
|
||||||
var addedToCatalog = false;
|
await ApplyTdsCureFallbackAsync(aiResult, colorName);
|
||||||
|
|
||||||
// Auto-contribute: insert into platform catalog if we have the minimum viable fields
|
|
||||||
// and this SKU isn't already there
|
|
||||||
if (!wasInCatalog
|
|
||||||
&& !string.IsNullOrEmpty(sku)
|
|
||||||
&& !string.IsNullOrEmpty(manufacturer)
|
|
||||||
&& !string.IsNullOrEmpty(colorName))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newItem = new PowderCatalogItem
|
|
||||||
{
|
|
||||||
VendorName = manufacturer,
|
|
||||||
Sku = sku,
|
|
||||||
ColorName = colorName,
|
|
||||||
CureTemperatureF = aiResult.CureTemperatureF,
|
|
||||||
CureTimeMinutes = aiResult.CureTimeMinutes,
|
|
||||||
Finish = aiResult.Finish,
|
|
||||||
ColorFamilies = aiResult.ColorFamilies,
|
|
||||||
RequiresClearCoat = aiResult.RequiresClearCoat,
|
|
||||||
CoverageSqFtPerLb = aiResult.CoverageSqFtPerLb,
|
|
||||||
TransferEfficiency= aiResult.TransferEfficiency,
|
|
||||||
ImageUrl = aiResult.ImageUrl,
|
|
||||||
ProductUrl = aiResult.SpecPageUrl,
|
|
||||||
SdsUrl = aiResult.SdsUrl,
|
|
||||||
TdsUrl = aiResult.TdsUrl,
|
|
||||||
IsUserContributed = true,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
await _unitOfWork.PowderCatalog.AddAsync(newItem);
|
|
||||||
await _unitOfWork.CompleteAsync();
|
|
||||||
addedToCatalog = true;
|
|
||||||
_logger.LogInformation("Label scan contributed new catalog entry: {Manufacturer} {Sku}", manufacturer, sku);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Unique constraint violation means another request beat us — not an error
|
|
||||||
_logger.LogInformation("Catalog auto-insert skipped (likely duplicate): {Message}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this product already exists in the tenant's inventory.
|
// Check if this product already exists in the tenant's inventory.
|
||||||
// Match by ManufacturerPartNumber first (most precise); fall back to color name + manufacturer.
|
// Match by ManufacturerPartNumber first (most precise); fall back to color name + manufacturer.
|
||||||
@@ -881,18 +946,18 @@ public class InventoryController : Controller
|
|||||||
manufacturerPartNumber = sku,
|
manufacturerPartNumber = sku,
|
||||||
colorName = colorName,
|
colorName = colorName,
|
||||||
description = aiResult.Description,
|
description = aiResult.Description,
|
||||||
finish = catalogMatch?.Finish ?? aiResult.Finish,
|
finish = aiResult.Finish,
|
||||||
cureTemperatureF = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF,
|
cureTemperatureF = aiResult.CureTemperatureF,
|
||||||
cureTimeMinutes = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes,
|
cureTimeMinutes = aiResult.CureTimeMinutes,
|
||||||
colorFamilies = catalogMatch?.ColorFamilies ?? aiResult.ColorFamilies,
|
colorFamilies = aiResult.ColorFamilies,
|
||||||
requiresClearCoat = catalogMatch?.RequiresClearCoat ?? aiResult.RequiresClearCoat,
|
requiresClearCoat = aiResult.RequiresClearCoat,
|
||||||
coverageSqFtPerLb = catalogMatch?.CoverageSqFtPerLb ?? aiResult.CoverageSqFtPerLb,
|
coverageSqFtPerLb = aiResult.CoverageSqFtPerLb,
|
||||||
transferEfficiency = catalogMatch?.TransferEfficiency ?? aiResult.TransferEfficiency,
|
transferEfficiency = aiResult.TransferEfficiency,
|
||||||
unitPrice = catalogMatch?.UnitPrice ?? aiResult.UnitCostPerLb ?? 0m,
|
unitPrice = aiResult.UnitCostPerLb ?? 0m,
|
||||||
imageUrl = catalogMatch?.ImageUrl ?? aiResult.ImageUrl,
|
imageUrl = aiResult.ImageUrl,
|
||||||
productUrl = catalogMatch?.ProductUrl ?? aiResult.SpecPageUrl,
|
productUrl = aiResult.SpecPageUrl,
|
||||||
sdsUrl = catalogMatch?.SdsUrl ?? aiResult.SdsUrl,
|
sdsUrl = aiResult.SdsUrl,
|
||||||
tdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl,
|
tdsUrl = aiResult.TdsUrl,
|
||||||
vendorName = manufacturer,
|
vendorName = manufacturer,
|
||||||
wasInCatalog = wasInCatalog,
|
wasInCatalog = wasInCatalog,
|
||||||
addedToCatalog = addedToCatalog,
|
addedToCatalog = addedToCatalog,
|
||||||
|
|||||||
@@ -450,6 +450,21 @@
|
|||||||
aiFilledImage = true;
|
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
|
// Build a persistent "needs more info" tip if key identity fields are still unknown
|
||||||
const missingHints = [];
|
const missingHints = [];
|
||||||
if (!data.manufacturer && !document.getElementById('field-manufacturer')?.value?.trim())
|
if (!data.manufacturer && !document.getElementById('field-manufacturer')?.value?.trim())
|
||||||
|
|||||||
Reference in New Issue
Block a user