← back to Sku Check Skill
public/index.html
651 lines
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SKU Check Skill - Designer Wallcoverings</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f1117; color: #e1e4e8; min-height: 100vh; }
.agent-avatar { width: 56px; height: 56px; border-radius: 50%; border: 2px solid #EC4899; object-fit: cover; }
.agent-avatar-fallback { width: 56px; height: 56px; border-radius: 50%; border: 2px solid #EC4899; background: #EC4899; color: #fff; font-size: 22px; font-weight: 700; display: none; align-items: center; justify-content: center; }
.header { background: linear-gradient(135deg, #1a1f2e 0%, #0d1117 100%); border-bottom: 1px solid #30363d; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
.header h1 { font-size: 20px; font-weight: 600; color: #58a6ff; }
.header .subtitle { font-size: 13px; color: #8b949e; margin-top: 2px; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
.stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; }
.stat-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px 16px; text-align: center; }
.stat-card .value { font-size: 28px; font-weight: 700; }
.stat-card .label { font-size: 12px; color: #8b949e; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-card.found .value { color: #3fb950; }
.stat-card.not-found .value { color: #f85149; }
.stat-card.error .value { color: #d29922; }
.stat-card.total .value { color: #58a6ff; }
.stat-card.tagged .value { color: #bc8cff; }
.panels { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 20px; }
@media (max-width: 900px) { .panels { grid-template-columns: 1fr; } }
.panel { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.panel h3 { font-size: 14px; color: #58a6ff; margin-bottom: 12px; font-weight: 600; }
.panel label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 4px; }
.panel input, .panel select { width: 100%; padding: 8px 10px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 14px; margin-bottom: 10px; }
.panel input:focus, .panel select:focus { border-color: #58a6ff; outline: none; }
.row { display: flex; gap: 8px; }
.row input { flex: 1; }
.btn { padding: 6px 12px; border: none; border-radius: 5px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.15s; white-space: nowrap; }
.btn-primary { background: #238636; color: #fff; }
.btn-primary:hover { background: #2ea043; }
.btn-danger { background: #da3633; color: #fff; }
.btn-danger:hover { background: #f85149; }
.btn-secondary { background: #30363d; color: #e1e4e8; }
.btn-secondary:hover { background: #484f58; }
.btn-sm { padding: 3px 8px; font-size: 11px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Action button colors */
.btn-specs { background: #0d9488; color: #fff; }
.btn-specs:hover { background: #14b8a6; }
.btn-images { background: #2563eb; color: #fff; }
.btn-images:hover { background: #3b82f6; }
.btn-ai { background: #7c3aed; color: #fff; }
.btn-ai:hover { background: #8b5cf6; }
.btn-tags { background: #d97706; color: #fff; }
.btn-tags:hover { background: #f59e0b; }
.btn-meta { background: #4f46e5; color: #fff; }
.btn-meta:hover { background: #6366f1; }
.btn-title { background: #16a34a; color: #fff; }
.btn-title:hover { background: #22c55e; }
.btn-sample { background: #db2777; color: #fff; }
.btn-sample:hover { background: #ec4899; }
.btn-all { background: #dc2626; color: #fff; font-weight: 700; }
.btn-all:hover { background: #ef4444; }
.btn-vendor { background: #0891b2; color: #fff; }
.btn-vendor:hover { background: #06b6d4; }
.job-bar { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px 16px; margin-bottom: 20px; display: none; }
.job-bar.active { display: block; }
.job-bar .job-title { font-size: 14px; font-weight: 600; color: #58a6ff; margin-bottom: 8px; }
.progress-track { height: 6px; background: #21262d; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: #238636; border-radius: 3px; transition: width 0.3s; }
.job-stats { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: #8b949e; }
.job-stats span { display: flex; align-items: center; gap: 4px; }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot.green { background: #3fb950; }
.dot.red { background: #f85149; }
.dot.yellow { background: #d29922; }
.filters { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.filters select, .filters input { padding: 6px 10px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 13px; }
/* Bulk action bar */
.bulk-bar { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 1px solid #334155; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.bulk-bar .bulk-label { font-size: 13px; font-weight: 600; color: #94a3b8; }
.table-wrap { background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #21262d; padding: 10px 12px; text-align: left; font-weight: 600; color: #8b949e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #30363d; white-space: nowrap; }
tbody td { padding: 8px 10px; border-bottom: 1px solid #21262d; vertical-align: top; }
tbody tr:hover { background: #1c2128; }
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
.status-found { background: rgba(63, 185, 80, 0.15); color: #3fb950; }
.status-not_found { background: rgba(248, 81, 73, 0.15); color: #f85149; }
.status-error { background: rgba(210, 153, 34, 0.15); color: #d29922; }
.status-pending { background: rgba(139, 148, 158, 0.15); color: #8b949e; }
a { color: #58a6ff; text-decoration: none; }
a:hover { text-decoration: underline; }
.empty-state { text-align: center; padding: 40px 20px; color: #8b949e; }
.empty-state h3 { font-size: 16px; margin-bottom: 8px; color: #e1e4e8; }
/* Action button row */
.action-btns { display: flex; gap: 3px; flex-wrap: wrap; max-width: 500px; }
/* Detail panel that slides open below a row */
.detail-row td { padding: 0 !important; border: none !important; }
.detail-panel { background: #1c2128; border: 1px solid #30363d; border-radius: 0 0 8px 8px; margin: 0 8px 8px 8px; padding: 16px; display: none; }
.detail-panel.open { display: block; animation: slideDown 0.2s ease-out; }
@keyframes slideDown { from { opacity: 0; max-height: 0; } to { opacity: 1; max-height: 600px; } }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
@media (max-width: 1200px) { .detail-grid { grid-template-columns: 1fr 1fr; } }
.detail-section { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 12px; }
.detail-section h4 { font-size: 12px; color: #58a6ff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
.detail-section .field { display: flex; justify-content: space-between; padding: 3px 0; font-size: 12px; border-bottom: 1px solid #21262d; }
.detail-section .field .fk { color: #8b949e; }
.detail-section .field .fv { color: #e1e4e8; font-weight: 500; text-align: right; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
.detail-images { display: flex; gap: 8px; flex-wrap: wrap; }
.detail-images img { width: 80px; height: 80px; object-fit: cover; border-radius: 4px; border: 1px solid #30363d; cursor: pointer; }
.detail-images img:hover { border-color: #58a6ff; }
.ai-tag { display: inline-block; padding: 2px 6px; background: rgba(124, 58, 237, 0.2); color: #a78bfa; border-radius: 4px; font-size: 11px; margin: 2px; }
.ai-color { display: inline-block; padding: 2px 6px; background: rgba(59, 130, 246, 0.2); color: #93c5fd; border-radius: 4px; font-size: 11px; margin: 2px; }
.step-status { display: inline-block; font-size: 11px; margin-left: 4px; }
.step-ok { color: #3fb950; }
.step-fail { color: #f85149; }
.step-spin { color: #d29922; }
.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.toast { position: fixed; bottom: 20px; right: 20px; background: #238636; color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 13px; display: none; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.4); max-width: 400px; }
.toast.error { background: #da3633; }
.toast.show { display: block; animation: fadeIn 0.3s; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.pagination { display: flex; justify-content: center; gap: 8px; margin-top: 12px; padding: 12px; }
</style>
</head>
<body>
<div class="header">
<img class="agent-avatar" src="http://45.61.58.125/agent-avatars/the-stager.png" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="agent-avatar-fallback">S</div>
<div>
<h1>SKU Check Skill</h1>
<div class="subtitle">Cross-reference DW SKUs with vendor websites • Fetch specs, AI analysis, update Shopify</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-sm" onclick="loadStats()">Refresh Stats</button>
</div>
</div>
<div class="container">
<div class="stats-bar" id="statsBar">
<div class="stat-card total"><div class="value" id="statTotal">0</div><div class="label">Total Checked</div></div>
<div class="stat-card found"><div class="value" id="statFound">0</div><div class="label">Found</div></div>
<div class="stat-card not-found"><div class="value" id="statNotFound">0</div><div class="label">Not Found</div></div>
<div class="stat-card error"><div class="value" id="statErrors">0</div><div class="label">Errors</div></div>
<div class="stat-card tagged"><div class="value" id="statTagged">0</div><div class="label">Tagged Review</div></div>
</div>
<div class="panels">
<div class="panel">
<h3>Check Single SKU</h3>
<label>DW SKU (e.g. GGA-82668)</label>
<input type="text" id="singleSku" placeholder="GGA-82668" />
<button class="btn btn-primary" onclick="checkSingle()" id="btnSingle">Check SKU</button>
</div>
<div class="panel">
<h3>Check Range</h3>
<label>Prefix (e.g. GGA)</label>
<input type="text" id="rangePrefix" placeholder="GGA" />
<div class="row">
<div><label>From</label><input type="number" id="rangeFrom" placeholder="82000" /></div>
<div><label>To</label><input type="number" id="rangeTo" placeholder="83000" /></div>
</div>
<button class="btn btn-primary" onclick="checkRange()" id="btnRange">Check Range</button>
</div>
<div class="panel">
<h3>Check by Vendor</h3>
<label>Vendor</label>
<select id="vendorSelect"><option value="">Loading vendors...</option></select>
<label>Limit</label>
<input type="number" id="vendorLimit" value="500" placeholder="500" />
<button class="btn btn-primary" onclick="checkVendor()" id="btnVendor">Check Vendor</button>
</div>
</div>
<div class="job-bar" id="jobBar">
<div class="job-title" id="jobTitle">Processing...</div>
<div class="progress-track"><div class="progress-fill" id="jobProgress" style="width:0%"></div></div>
<div class="job-stats">
<span><span class="dot green"></span> Found: <b id="jobFound">0</b></span>
<span><span class="dot red"></span> Not Found: <b id="jobNotFound">0</b></span>
<span><span class="dot yellow"></span> Errors: <b id="jobErrors">0</b></span>
<span>Progress: <b id="jobChecked">0</b>/<b id="jobTotal">0</b></span>
<button class="btn btn-danger btn-sm" onclick="stopJob()">Stop</button>
</div>
</div>
<div class="filters">
<select id="filterStatus" onchange="loadResults()">
<option value="">All Statuses</option>
<option value="found">Found</option>
<option value="not_found">Not Found</option>
<option value="error">Error</option>
</select>
<select id="filterVendor" onchange="loadResults()">
<option value="">All Vendors</option>
</select>
<button class="btn btn-secondary btn-sm" onclick="loadResults()">Refresh</button>
</div>
<!-- Bulk actions -->
<div class="bulk-bar">
<span class="bulk-label">Bulk:</span>
<button class="btn btn-all btn-sm" onclick="bulkRunAll()">Run ALL on Visible</button>
<button class="btn btn-specs btn-sm" onclick="bulkAction('fetch-specs')">Fetch All Specs</button>
<button class="btn btn-ai btn-sm" onclick="bulkAction('ai-analyze')">AI All</button>
<button class="btn btn-tags btn-sm" onclick="bulkAction('update-tags')">Tags All</button>
<button class="btn btn-meta btn-sm" onclick="bulkAction('update-metafields')">Meta All</button>
<button class="btn btn-sample btn-sm" onclick="bulkAction('fix-sample')">Fix All Samples</button>
<span id="bulkProgress" style="font-size:12px;color:#8b949e;margin-left:8px;"></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>DW SKU</th>
<th>MFR SKU</th>
<th>Vendor</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="resultsBody">
<tr><td colspan="5" class="empty-state"><h3>No results yet</h3>Check a SKU, range, or vendor to get started</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
<div class="toast" id="toast"></div>
<script>
function xfetch(url,opts){return new Promise(function(resolve,reject){var x=new XMLHttpRequest();x.open((opts&&opts.method)||"GET",url,true);x.setRequestHeader("Authorization","Basic "+btoa("admin:DWSecure2024!"));if(opts&&opts.body){x.setRequestHeader("Content-Type","application/json");}x.onload=function(){try{resolve({ok:x.status>=200&&x.status<300,status:x.status,json:function(){return Promise.resolve(JSON.parse(x.responseText))},text:function(){return Promise.resolve(x.responseText)}})}catch(e){reject(e)}};x.onerror=function(){reject(new Error("XHR failed"))};x.send(opts&&opts.body?JSON.stringify(opts.body):null)})};
const API = '';
let currentPage = 0;
const PAGE_SIZE = 50;
let jobPollInterval = null;
let currentResults = [];
function showToast(msg, isError) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show' + (isError ? ' error' : '');
setTimeout(() => t.className = 'toast', 4000);
}
async function api(method, path, body) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await xfetch(API + path, opts);
return r.json();
}
async function loadStats() {
try {
const data = await api('GET', '/api/stats');
if (data.overall) {
document.getElementById('statTotal').textContent = data.overall.total || 0;
document.getElementById('statFound').textContent = data.overall.found || 0;
document.getElementById('statNotFound').textContent = data.overall.not_found || 0;
document.getElementById('statErrors').textContent = data.overall.errors || 0;
document.getElementById('statTagged').textContent = data.overall.tagged || 0;
}
} catch (e) { console.error('Stats error:', e); }
}
async function loadVendors() {
try {
const data = await api('GET', '/api/vendors');
const sel = document.getElementById('vendorSelect');
const filter = document.getElementById('filterVendor');
sel.innerHTML = '<option value="">Select vendor...</option>';
data.forEach(v => {
sel.innerHTML += `<option value="${v.vendor_code}">${v.vendor_name || v.vendor_code} (${v.sku_count})</option>`;
filter.innerHTML += `<option value="${v.vendor_code}">${v.vendor_name || v.vendor_code}</option>`;
});
} catch (e) { console.error('Vendors error:', e); }
}
async function loadResults() {
const status = document.getElementById('filterStatus').value;
const vendor = document.getElementById('filterVendor').value;
let qs = `?limit=${PAGE_SIZE}&offset=${currentPage * PAGE_SIZE}`;
if (status) qs += `&status=${status}`;
if (vendor) qs += `&vendor=${vendor}`;
try {
const data = await api('GET', '/api/results' + qs);
currentResults = data.results || [];
const tbody = document.getElementById('resultsBody');
if (!data.results || data.results.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state"><h3>No results</h3>Check a SKU to get started</td></tr>';
document.getElementById('pagination').innerHTML = '';
return;
}
tbody.innerHTML = data.results.map((r, idx) => `
<tr id="row-${r.dw_sku}">
<td>
<strong style="font-size:14px">${r.dw_sku_dash || r.dw_sku}</strong>
${r.shopify_handle ? `<br><a href="https://www.designerwallcoverings.com/products/${r.shopify_handle}" target="_blank" rel="noopener noreferrer" style="font-size:11px">View on site</a>` : ''}
${r.shopify_product_id ? `<br><a href="https://admin.shopify.com/store/designer-laboratory-sandbox/products/${r.shopify_product_id}" target="_blank" rel="noopener noreferrer" style="font-size:11px;color:#d29922">Admin</a>` : ''}
${r.vendor_url ? `<br><a href="${r.vendor_url}" target="_blank" rel="noopener noreferrer" style="font-size:11px;color:#0891b2" title="View ${r.mfr_sku} on manufacturer website">MFR Site</a>` : ''}
</td>
<td>
<span style="font-weight:600">${r.mfr_sku || '-'}</span>
<br><span style="font-size:11px;color:#8b949e">${r.checked_at ? new Date(r.checked_at).toLocaleDateString() : ''}</span>
</td>
<td>
${r.vendor_name || r.vendor_code || '-'}
<br><a href="#" onclick="openVendorSite('${r.dw_sku}');return false" style="font-size:11px" class="btn-vendor-link">Vendor Site</a>
</td>
<td><span class="status-badge status-${r.check_status}">${r.check_status}</span></td>
<td>
<div class="action-btns">
<button class="btn btn-specs btn-sm" onclick="doAction('fetch-specs','${r.dw_sku}')" title="Fetch specs from vendor catalog">Specs</button>
<button class="btn btn-images btn-sm" onclick="doAction('fetch-images','${r.dw_sku}')" title="Fetch images from catalog">Imgs</button>
<button class="btn btn-ai btn-sm" onclick="doAction('ai-analyze','${r.dw_sku}')" title="Run Gemini AI analysis">AI</button>
<button class="btn btn-tags btn-sm" onclick="doAction('update-tags','${r.dw_sku}')" title="Update Shopify tags">Tags</button>
<button class="btn btn-meta btn-sm" onclick="doAction('update-metafields','${r.dw_sku}')" title="Push metafields to Shopify">Meta</button>
<button class="btn btn-title btn-sm" onclick="doAction('fix-title','${r.dw_sku}')" title="Fix title format">Title</button>
<button class="btn btn-sample btn-sm" onclick="doAction('fix-sample','${r.dw_sku}')" title="Fix sample variant $4.25">Sample</button>
<button class="btn btn-all btn-sm" onclick="doAction('run-all','${r.dw_sku}')" title="Run full pipeline">ALL</button>
${r.check_status === 'not_found' && !r.tagged_review ? `<button class="btn btn-danger btn-sm" onclick="tagReview('${r.dw_sku}')">Review</button>` : ''}
${r.tagged_review ? '<span style="color:#bc8cff;font-size:10px">Tagged</span>' : ''}
</div>
<div id="detail-${r.dw_sku}" class="detail-panel"></div>
</td>
</tr>
`).join('');
const totalPages = Math.ceil(data.total / PAGE_SIZE);
const pag = document.getElementById('pagination');
if (totalPages > 1) {
let html = '';
if (currentPage > 0) html += `<button class="btn btn-secondary btn-sm" onclick="goPage(${currentPage - 1})">Prev</button>`;
html += `<span style="font-size:13px;color:#8b949e">Page ${currentPage + 1} of ${totalPages} (${data.total} total)</span>`;
if (currentPage < totalPages - 1) html += `<button class="btn btn-secondary btn-sm" onclick="goPage(${currentPage + 1})">Next</button>`;
pag.innerHTML = html;
} else {
pag.innerHTML = data.total > 0 ? `<span style="font-size:13px;color:#8b949e">${data.total} results</span>` : '';
}
} catch (e) { console.error('Results error:', e); }
}
function goPage(p) { currentPage = p; loadResults(); }
// === ACTION HANDLER ===
async function doAction(action, dwSku) {
const panel = document.getElementById(`detail-${dwSku}`);
panel.classList.add('open');
panel.innerHTML = `<div style="text-align:center;padding:16px"><span class="spinner"></span> Running ${action}...</div>`;
try {
const data = await api('POST', `/api/action/${action}`, { dw_sku: dwSku });
if (action === 'run-all') {
panel.innerHTML = renderRunAllResult(data, dwSku);
} else if (action === 'fetch-specs') {
panel.innerHTML = renderSpecsResult(data);
} else if (action === 'fetch-images') {
panel.innerHTML = renderImagesResult(data);
} else if (action === 'ai-analyze') {
panel.innerHTML = renderAiResult(data);
} else if (action === 'update-tags') {
panel.innerHTML = renderTagsResult(data);
} else if (action === 'update-metafields') {
panel.innerHTML = renderMetaResult(data);
} else if (action === 'fix-title') {
panel.innerHTML = renderTitleResult(data);
} else if (action === 'fix-sample') {
panel.innerHTML = renderSampleResult(data);
} else {
panel.innerHTML = `<pre style="font-size:11px;color:#8b949e;white-space:pre-wrap">${JSON.stringify(data, null, 2)}</pre>`;
}
if (data.error) showToast(`${action}: ${data.error}`, true);
else showToast(`${action} done for ${dwSku}`);
} catch (e) {
panel.innerHTML = `<div style="color:#f85149;padding:8px">Error: ${e.message}</div>`;
showToast(e.message, true);
}
}
function renderSpecsResult(data) {
if (!data.specs) return `<div style="color:#f85149;padding:8px">${data.message || 'No specs found'}</div>`;
const s = data.specs;
const fields = [
['Pattern', s.patternName], ['Color', s.colorName], ['Collection', s.collection],
['Width', s.width], ['Length', s.length], ['Repeat', s.repeat],
['Composition', s.composition], ['Backing', s.backing], ['Fire Rating', s.fireRating],
['Match', s.matchType], ['Country', s.country],
['Retail', s.retailPrice], ['Trade', s.tradePrice], ['Cost', s.costPrice],
].filter(([,v]) => v);
let html = `<div class="detail-section"><h4>Specs from Catalog</h4>`;
fields.forEach(([k,v]) => { html += `<div class="field"><span class="fk">${k}</span><span class="fv">${v}</span></div>`; });
if (s.productUrl) html += `<div style="margin-top:8px"><a href="${s.productUrl}" target="_blank" rel="noopener noreferrer" style="font-size:11px">View on vendor site</a></div>`;
if (s.imageUrl) html += `<div style="margin-top:8px"><img src="${s.imageUrl}" style="max-width:120px;border-radius:4px;border:1px solid #30363d" /></div>`;
html += `</div>`;
return html;
}
function renderImagesResult(data) {
if (!data.images || data.images.length === 0) return `<div style="color:#8b949e;padding:8px">${data.message || 'No images found'}</div>`;
let html = `<div class="detail-section"><h4>Images (${data.images.length})</h4><div class="detail-images">`;
data.images.forEach(url => { html += `<a href="${url}" target="_blank" rel="noopener noreferrer"><img src="${url}" /></a>`; });
html += '</div>';
if (data.productUrl) html += `<div style="margin-top:8px"><a href="${data.productUrl}" target="_blank" rel="noopener noreferrer" style="font-size:11px">Vendor product page</a></div>`;
html += '</div>';
return html;
}
function renderAiResult(data) {
if (!data.analysis) return `<div style="color:#f85149;padding:8px">${data.message || 'AI analysis failed'}</div>`;
const a = data.analysis;
let html = `<div class="detail-section"><h4>AI Analysis (Gemini)</h4>`;
html += `<div class="field"><span class="fk">Background</span><span class="fv">${a.backgroundColor || '-'}</span></div>`;
html += `<div class="field"><span class="fk">Material</span><span class="fv">${a.material || '-'}</span></div>`;
if (a.colors) html += `<div style="margin:6px 0"><span class="fk" style="font-size:11px">Colors: </span>${a.colors.map(c => `<span class="ai-color">${c}</span>`).join('')}</div>`;
if (a.styles) html += `<div style="margin:4px 0"><span class="fk" style="font-size:11px">Styles: </span>${a.styles.map(s => `<span class="ai-tag">${s}</span>`).join('')}</div>`;
if (a.patterns) html += `<div style="margin:4px 0"><span class="fk" style="font-size:11px">Patterns: </span>${a.patterns.map(p => `<span class="ai-tag">${p}</span>`).join('')}</div>`;
if (a.tags) html += `<div style="margin:4px 0"><span class="fk" style="font-size:11px">Tags: </span>${a.tags.map(t => `<span class="ai-tag">${t}</span>`).join('')}</div>`;
if (a.description) html += `<div style="margin-top:8px;font-size:12px;color:#d1d5db;line-height:1.4">${a.description}</div>`;
html += '</div>';
return html;
}
function renderTagsResult(data) {
let html = `<div class="detail-section"><h4>Tags Updated</h4>`;
if (data.error) { html += `<div style="color:#f85149">${data.error}</div>`; }
else { html += `<div class="field"><span class="fk">Total Tags</span><span class="fv">${data.tagCount || 0}</span></div>`; }
html += `<div style="margin-top:4px;font-size:11px;color:#8b949e">${data.message || ''}</div></div>`;
return html;
}
function renderMetaResult(data) {
let html = `<div class="detail-section"><h4>Metafields</h4>`;
if (data.error) { html += `<div style="color:#f85149">${data.error}</div>`; }
else { html += `<div class="field"><span class="fk">Set</span><span class="fv" style="color:#3fb950">${data.metafieldsSet}/${data.total}</span></div>`; }
html += `<div style="margin-top:4px;font-size:11px;color:#8b949e">${data.message || ''}</div></div>`;
return html;
}
function renderTitleResult(data) {
let html = `<div class="detail-section"><h4>Title</h4>`;
if (data.error) { html += `<div style="color:#f85149">${data.error}</div>`; }
else {
html += `<div class="field"><span class="fk">Old</span><span class="fv" style="color:#f85149">${data.oldTitle || '-'}</span></div>`;
html += `<div class="field"><span class="fk">New</span><span class="fv" style="color:#3fb950">${data.newTitle || '-'}</span></div>`;
}
html += '</div>';
return html;
}
function renderSampleResult(data) {
let html = `<div class="detail-section"><h4>Sample Variant</h4>`;
if (data.error) { html += `<div style="color:#f85149">${data.error}</div>`; }
else {
if (data.oldVariant) html += `<div class="field"><span class="fk">Was</span><span class="fv">${data.oldVariant.title} @ $${data.oldVariant.price}</span></div>`;
if (data.newVariant) html += `<div class="field"><span class="fk">Now</span><span class="fv" style="color:#3fb950">${data.newVariant.title} @ $${data.newVariant.price} (${data.newVariant.sku})</span></div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-top:4px">${data.message || ''}</div>`;
}
html += '</div>';
return html;
}
function renderRunAllResult(data, dwSku) {
if (data.error) return `<div style="color:#f85149;padding:8px">${data.error}</div>`;
const steps = data.steps || {};
let html = `<div class="detail-grid">`;
// Specs
html += `<div class="detail-section"><h4>Specs ${steps.specs?.done ? '<span class="step-status step-ok">OK</span>' : '<span class="step-status step-fail">MISS</span>'}</h4>`;
if (steps.specs?.data) {
const s = steps.specs.data;
[['Pattern', s.patternName], ['Color', s.colorName], ['Collection', s.collection], ['Width', s.width], ['Repeat', s.repeat]].filter(([,v]) => v).forEach(([k,v]) => {
html += `<div class="field"><span class="fk">${k}</span><span class="fv">${v}</span></div>`;
});
} else { html += `<div style="color:#8b949e;font-size:11px">${steps.specs?.message || 'No data'}</div>`; }
html += '</div>';
// AI
html += `<div class="detail-section"><h4>AI ${steps.ai?.done ? '<span class="step-status step-ok">OK</span>' : '<span class="step-status step-fail">MISS</span>'}</h4>`;
if (steps.ai?.data) {
const a = steps.ai.data;
if (a.backgroundColor) html += `<div class="field"><span class="fk">BG</span><span class="fv">${a.backgroundColor}</span></div>`;
if (a.colors) html += `<div style="margin:4px 0">${a.colors.map(c => `<span class="ai-color">${c}</span>`).join('')}</div>`;
if (a.styles) html += `<div style="margin:4px 0">${a.styles.map(s => `<span class="ai-tag">${s}</span>`).join('')}</div>`;
if (a.description) html += `<div style="font-size:11px;color:#d1d5db;margin-top:4px">${a.description.substring(0,150)}...</div>`;
} else { html += `<div style="color:#8b949e;font-size:11px">${steps.ai?.message || 'No data'}</div>`; }
html += '</div>';
// Updates summary
html += `<div class="detail-section"><h4>Updates</h4>`;
html += `<div class="field"><span class="fk">Title</span><span class="fv">${steps.title?.done ? '<span class="step-ok">Updated</span>' : '<span class="step-fail">Failed</span>'}</span></div>`;
html += `<div class="field"><span class="fk">Tags</span><span class="fv">${steps.tags?.count || 0} tags</span></div>`;
html += `<div class="field"><span class="fk">Metafields</span><span class="fv">${steps.metafields?.set || 0}/${steps.metafields?.total || 0}</span></div>`;
html += `<div class="field"><span class="fk">Sample</span><span class="fv">${steps.sample?.done ? '<span class="step-ok">OK</span>' : '<span class="step-fail">Failed</span>'}</span></div>`;
if (steps.title?.newTitle) html += `<div style="font-size:11px;color:#3fb950;margin-top:6px">${steps.title.newTitle}</div>`;
html += '</div>';
html += '</div>';
html += `<div style="text-align:center;margin-top:8px;font-size:12px;color:#3fb950;font-weight:600">${data.summary || 'Complete'}</div>`;
return html;
}
// Open vendor website for a SKU
async function openVendorSite(dwSku) {
try {
const data = await api('POST', '/api/action/vendor-url', { dw_sku: dwSku });
if (data.searchUrl) window.open(data.searchUrl, '_blank');
else if (data.vendorWebsite) window.open(data.vendorWebsite, '_blank');
else showToast('No vendor URL found', true);
} catch (e) { showToast(e.message, true); }
}
// Bulk actions
async function bulkRunAll() {
if (currentResults.length === 0) return showToast('No results to process', true);
const prog = document.getElementById('bulkProgress');
prog.textContent = `Processing 0/${currentResults.length}...`;
for (let i = 0; i < currentResults.length; i++) {
prog.textContent = `Processing ${i + 1}/${currentResults.length}...`;
await doAction('run-all', currentResults[i].dw_sku);
await new Promise(r => setTimeout(r, 500));
}
prog.textContent = `Done! ${currentResults.length} processed`;
showToast(`Bulk Run ALL complete: ${currentResults.length} SKUs`);
}
async function bulkAction(action) {
if (currentResults.length === 0) return showToast('No results to process', true);
const prog = document.getElementById('bulkProgress');
for (let i = 0; i < currentResults.length; i++) {
prog.textContent = `${action} ${i + 1}/${currentResults.length}...`;
await doAction(action, currentResults[i].dw_sku);
await new Promise(r => setTimeout(r, 300));
}
prog.textContent = `Done! ${currentResults.length} processed`;
showToast(`Bulk ${action} complete`);
}
// Existing functions
async function checkSingle() {
const sku = document.getElementById('singleSku').value.trim();
if (!sku) return showToast('Enter a DW SKU', true);
document.getElementById('btnSingle').disabled = true;
document.getElementById('btnSingle').textContent = 'Checking...';
try {
const data = await api('POST', '/api/check/sku', { dw_sku: sku });
if (data.error) showToast(data.error, true);
else {
const st = data.result?.check_status || data.vendorCheck?.status;
showToast(`${sku}: ${st?.toUpperCase()} (MFR: ${data.crossref?.mfr_sku})`);
loadResults(); loadStats();
}
} catch (e) { showToast('Error: ' + e.message, true); }
document.getElementById('btnSingle').disabled = false;
document.getElementById('btnSingle').textContent = 'Check SKU';
}
async function checkRange() {
const prefix = document.getElementById('rangePrefix').value.trim();
const from = parseInt(document.getElementById('rangeFrom').value);
const to = parseInt(document.getElementById('rangeTo').value);
if (!prefix || !from || !to) return showToast('Fill in prefix, from, and to', true);
try {
const data = await api('POST', '/api/check/range', { prefix, from, to });
if (data.error) return showToast(data.error, true);
showToast(`Job started: ${data.total} SKUs`);
startJobPolling();
} catch (e) { showToast('Error: ' + e.message, true); }
}
async function checkVendor() {
const vendor_code = document.getElementById('vendorSelect').value;
const limit = parseInt(document.getElementById('vendorLimit').value) || 500;
if (!vendor_code) return showToast('Select a vendor', true);
try {
const data = await api('POST', '/api/check/vendor', { vendor_code, limit });
if (data.error) return showToast(data.error, true);
showToast(`Job started: ${data.total} SKUs`);
startJobPolling();
} catch (e) { showToast('Error: ' + e.message, true); }
}
function startJobPolling() {
document.getElementById('jobBar').classList.add('active');
if (jobPollInterval) clearInterval(jobPollInterval);
jobPollInterval = setInterval(pollJob, 2000);
pollJob();
}
async function pollJob() {
try {
const data = await api('GET', '/api/job');
if (data.status === 'idle') { clearInterval(jobPollInterval); document.getElementById('jobBar').classList.remove('active'); loadResults(); loadStats(); return; }
document.getElementById('jobTitle').textContent = `${data.type === 'vendor' ? 'Vendor' : 'Range'} Check: ${data.vendor_code || data.prefix || ''}`;
document.getElementById('jobFound').textContent = data.found || 0;
document.getElementById('jobNotFound').textContent = data.notFound || 0;
document.getElementById('jobErrors').textContent = data.errors || 0;
document.getElementById('jobChecked').textContent = data.checked || 0;
document.getElementById('jobTotal').textContent = data.total || 0;
document.getElementById('jobProgress').style.width = (data.total > 0 ? (data.checked / data.total * 100) : 0) + '%';
if (data.status === 'complete' || data.status === 'stopped') {
clearInterval(jobPollInterval);
setTimeout(() => { document.getElementById('jobBar').classList.remove('active'); loadResults(); loadStats(); }, 3000);
showToast(`Job ${data.status}: ${data.checked} checked, ${data.notFound} not found`);
}
} catch (e) {}
}
async function stopJob() { await api('POST', '/api/job/stop'); showToast('Job stop requested'); }
async function tagReview(dwSku) {
try {
const data = await api('POST', '/api/tag-review', { dw_sku: dwSku });
showToast(data.success ? `Tagged ${dwSku} as Review` : (data.error || data.message), !data.success);
loadResults();
} catch (e) { showToast('Error: ' + e.message, true); }
}
// Init
loadStats();
loadVendors();
loadResults();
setInterval(() => { loadStats(); }, 30000);
</script>
</body>
</html>