SKU Error Filter Tool
Paste error log · Notify team · Upload file · Review SKUs · Download
Made by & for
Inventory Management Operations Team
Complete the one-time setup to enable Slack auto-send and Google Sheet lookup.
1Paste Error
2Upload File
3Generate Files
4Download
5Notify Team
Error Message
Paste the full error log. SKUs are detected and deduplicated automatically.
SKU File
Upload your Excel or CSV. Column A = SKU, Column B = Qty, Columns C & D formatted as mm/dd/yyyy.
Click to browse or drag & drop file here
Supports .xlsx, .xls, .csv
Confirm SKUs to Remove
All detected SKUs are pre-checked. Uncheck any you want to keep.
✅ SKUs to Remove
❌ SKUs to Keep
0 selected
Output Files Ready
0
Clean Rows
0
Removed Rows
0
SKUs Found & Removed
clean_skus.xlsx
Original file with selected error SKU rows deleted · Dates as mm/dd/yyyy
error_skus.xlsx
Col A = SKU · Col B = Qty · Col C = Error Reason
Notify Team via Slack
Enter the brand name — SEM and IM will be looked up from your Google Sheet automatically.
Ready. Click Setup (top right) to configure Slac // ── State ────────────────────────────────────────────────────────── let fileData = [], fileHeaders = []; let skuReasonMap = {}, detectedSKUs = []; let outputClean = [], outputErrored = []; let foundTeam = {}; // { sem: {id, name}, im: {id, name} } const today = new Date().toISOString().split('T')[0]; let lastChannelId = null; // store channel ID for edit & resend let allBrands = []; // store all brands for autocomplete // ── Smart brand autocomplete ────────────────────────────────────── function onBrandInput() { const val = document.getElementById('brand-name').value.trim(); document.getElementById('slack-lookup').style.display = 'none'; document.getElementById('slack-not-found').style.display = 'none'; document.getElementById('btn-send').disabled = true; document.getElementById('slack-receipt').style.display = 'none'; document.getElementById('btn-resend').style.display = 'none'; updateSlackPreview(); showBrandDropdown(val); } function showBrandDropdown(query) { const dd = document.getElementById('brand-dropdown'); if (!query) { dd.style.display = 'none'; return; } if (allBrands.length === 0) { dd.innerHTML = '
Loading brands...
'; dd.style.display = 'block'; loadBrandDropdown().then(function() { showBrandDropdown(document.getElementById('brand-name').value); }); return; } var q2 = query.trim().toLowerCase(); var matches = q2 ? allBrands.filter(function(b) { return b.toLowerCase().includes(q2); }).slice(0, 8) : allBrands.slice(0, 8); if (!matches.length) { dd.innerHTML = '
No brands matching "' + query + '"
'; dd.style.display = 'block'; return; } dd.innerHTML = ''; matches.forEach(function(b) { var div = document.createElement('div'); div.style.cssText = 'padding:9px 14px;cursor:pointer;font-size:13px;color:var(--text);border-bottom:1px solid var(--border);transition:background 0.1s;'; if (q2) { var re = new RegExp('(' + q2.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi'); div.innerHTML = b.replace(re, '$1'); } else { div.textContent = b; } div.addEventListener('mouseover', function() { this.style.background = 'var(--bg3)'; }); div.addEventListener('mouseout', function() { this.style.background = ''; }); div.addEventListener('mousedown', function(e) { e.preventDefault(); selectBrand(b); }); dd.appendChild(div); }); dd.style.display = 'block'; } function onBrandFocus() { const val = document.getElementById('brand-name').value.trim(); if (allBrands.length === 0) { loadBrandDropdown().then(() => showBrandDropdown(val || ' ')); return; } // Show top 8 brands if empty, or filtered if has value showBrandDropdown(val || ' '); } function selectBrand(brand) { document.getElementById('brand-name').value = brand; document.getElementById('brand-dropdown').style.display = 'none'; updateSlackPreview(); } // Close dropdown on outside click document.addEventListener('click', e => { if (!e.target.closest('#brand-name') && !e.target.closest('#brand-dropdown')) { const dd = document.getElementById('brand-dropdown'); if (dd) dd.style.display = 'none'; } }); // ── Scroll to section ───────────────────────────────────────────── function scrollTo(id) { const el = document.getElementById(id); if (el) { // Make sure element is visible first el.style.display = el.style.display === 'none' ? 'block' : el.style.display; setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50); } } function stepClick(stepNum) { const targets = { 1: 'error-section', 2: 'upload-section', 3: 'results', 4: 'results', 5: 'slack-card' }; const id = targets[stepNum]; let el = document.getElementById(id); // Fallback selectors if id not found if (!el) { if (stepNum === 1) el = document.querySelector('.card'); if (stepNum === 2) el = document.querySelector('.slack-card'); if (stepNum === 3) el = document.querySelector('#drop-zone')?.closest('.card'); if (stepNum === 4) el = document.querySelector('.checklist-card'); if (stepNum === 5) el = document.querySelector('.results'); } if (!el) { setStatus('Complete earlier steps to unlock this section.'); return; } // Force show if hidden if (el.style.display === 'none') { setStatus('Complete earlier steps to unlock this section.'); return; } el.scrollIntoView({ behavior: 'smooth', block: 'start' }); setStatus('Jumped to step ' + stepNum); } // ── Dark mode ───────────────────────────────────────────────────── function toggleDark() { const isDark = document.body.classList.toggle('dark'); localStorage.setItem('sku_dark', isDark ? '1' : '0'); document.getElementById('dark-btn').textContent = isDark ? '☀ Light' : '🌙 Dark'; } (function initDark() { if (localStorage.getItem('sku_dark') === '1') { document.body.classList.add('dark'); window.addEventListener('load', () => { const btn = document.getElementById('dark-btn'); if (btn) btn.textContent = '☀ Light'; }); } })(); // ── Tabs ────────────────────────────────────────────────────────── // ── Session History ─────────────────────────────────────────────── function saveSession(data) { const sessions = getHistory(); sessions.unshift({ id: Date.now(), date: new Date().toLocaleString(), // Step 1 errorMessage: data.errorMessage || '', detectedSKUs: data.detectedSKUs || [], // Step 2 brand: data.brand || 'Unknown', semName: data.semName || '', imName: data.imName || '', slackSent: data.slackSent || false, slackMessage: data.slackMessage || '', // Step 3 fileName: data.fileName || '', totalRows: data.totalRows || 0, // Step 4 skusRemoved: data.skusRemoved || [], skusKept: data.skusKept || [], // Step 5 cleanCount: data.cleanCount || 0, errorCount: data.errorCount || 0, filesGenerated: data.filesGenerated || false, }); localStorage.setItem('sku_history', JSON.stringify(sessions.slice(0, 50))); } function getHistory() { try { return JSON.parse(localStorage.getItem('sku_history') || '[]'); } catch { return []; } } // ── Reset functions ─────────────────────────────────────────────── function resetStep1() { document.getElementById('error-input').value = ''; document.getElementById('sku-preview').style.display = 'none'; document.getElementById('slack-card').style.display = 'none'; document.getElementById('step1').className = 'step active'; document.getElementById('step5').className = 'step'; document.getElementById('process-btn').disabled = true; setStatus('Step 1 reset.'); } function resetStep2() { document.getElementById('brand-name').value = ''; document.getElementById('slack-lookup').style.display = 'none'; document.getElementById('slack-not-found').style.display = 'none'; document.getElementById('slack-status').style.display = 'none'; document.getElementById('btn-send').disabled = true; foundTeam = {}; updateSlackPreview(); setStatus('Step 2 reset.'); } function resetStep3() { fileData = []; fileHeaders = []; document.getElementById('uz-title').textContent = 'Click to browse or drag & drop file here'; document.getElementById('uz-sub').textContent = 'Supports .xlsx, .xls, .csv'; document.getElementById('drop-zone').classList.remove('loaded'); document.getElementById('file-input').value = ''; document.getElementById('step3').className = 'step'; document.getElementById('process-btn').disabled = true; setStatus('Step 3 reset.'); } function resetStep4() { document.getElementById('checklist-card').style.display = 'none'; document.getElementById('results').style.display = 'none'; document.getElementById('step4').className = 'step'; document.getElementById('step5').className = 'step'; setStatus('Step 4 reset — re-click Analyse to rebuild checklist.'); } // ── Brand dropdown ──────────────────────────────────────────────── async function loadBrandDropdown() { const cfg = getCfg(); const input = document.getElementById('brand-name'); if (!cfg.worker) { if (input) input.placeholder = 'Type brand name manually (Setup required for autocomplete)...'; return; } if (input) input.placeholder = 'Loading brands...'; try { const res = await fetch(cfg.worker + '/brands'); const data = await res.json(); if (!data.ok || !data.brands) { if (input) input.placeholder = 'Type brand name manually...'; return; } allBrands = data.brands; if (input) input.placeholder = 'Type to search ' + allBrands.length + ' brands...'; } catch(e) { if (input) input.placeholder = 'Type brand name manually (worker error)...'; console.log('Brand list load failed:', e.message); } } const SHEET_ID = "1kuj52Kx3cDjQVwovcY36NP9SSsGJLZi4TttO2RVtqLM"; // ── Config (localStorage) ───────────────────────────────────────── function getCfg() { return { worker: localStorage.getItem('sku_worker') || '', myId: localStorage.getItem('sku_myid') || '', sheet: localStorage.getItem('sku_sheet') || 'https://docs.google.com/spreadsheets/d/' + SHEET_ID + '/export?format=csv&gid=0', }; } function openModal() { const c = getCfg(); document.getElementById('cfg-worker').value = c.worker; document.getElementById('cfg-myid').value = c.myId; document.getElementById('cfg-sheet').value = c.sheet; document.getElementById('modal-wrap').classList.add('open'); } function closeModal() { document.getElementById('modal-wrap').classList.remove('open'); } function saveConfig() { localStorage.setItem('sku_worker', document.getElementById('cfg-worker').value.trim()); localStorage.setItem('sku_myid', document.getElementById('cfg-myid').value.trim()); localStorage.setItem('sku_sheet', document.getElementById('cfg-sheet').value.trim()); closeModal(); checkConfigBanner(); loadBrandDropdown(); setStatus('Config saved.'); } function checkConfigBanner() { const c = getCfg(); document.getElementById('config-banner').style.display = (c.worker && c.myId) ? 'none' : 'flex'; } checkConfigBanner(); // Load brands on startup if worker configured window.addEventListener('load', () => { loadBrandDropdown(); // Also retry after 2s in case worker takes time setTimeout(loadBrandDropdown, 2000); }); // ── Date formatter ──────────────────────────────────────────────── function toMMDDYYYY(val) { if (!val && val !== 0) return ''; if (typeof val === 'number' && val > 1000) { const d = new Date(Math.round((val - 25569) * 86400 * 1000)); if (!isNaN(d)) return pad(d.getUTCMonth()+1)+'/'+pad(d.getUTCDate())+'/'+d.getUTCFullYear(); } const s = String(val).trim(); if (!s) return ''; if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(s)) return s; const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/); if (iso) return iso[2]+'/'+iso[3]+'/'+iso[1]; const dmy = s.match(/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})$/); if (dmy) return pad(dmy[2])+'/'+pad(dmy[1])+'/'+dmy[3]; const d = new Date(s); if (!isNaN(d)) return pad(d.getMonth()+1)+'/'+pad(d.getDate())+'/'+d.getFullYear(); return s; } function pad(n) { return String(n).padStart(2,'0'); } function formatRowDates(row) { const r = [...row]; if (r.length > 2) r[2] = toMMDDYYYY(r[2]); if (r.length > 3) r[3] = toMMDDYYYY(r[3]); return r; } // ── SKU helpers ─────────────────────────────────────────────────── function extractAllSKUs(text) { const found = new Set(); // Pre-process: insert spaces before SKU-like patterns that are concatenated with other text // e.g. "LaunchSD-FRAG-153103-FBM" -> "Launch SD-FRAG-153103-FBM" const cleaned = text .replace(/([a-z])([A-Z][A-Z]+-[A-Z0-9]+-\d)/g, '$1 $2') // camelCase boundary before SKU .replace(/([A-Z]{2,})([A-Z][A-Z]+-[A-Z0-9]+-\d)/g, '$1 $2'); // ALLCAPS boundary before SKU cleaned.split(/[\s,;|]+/).forEach(token => { const t = token.replace(/^[^A-Z0-9]+/i,'').replace(/[^A-Z0-9-]+$/i,'').toUpperCase(); // SKU must: have hyphen, start with letter, length>4, contain at least one digit if (t.includes('-') && /^[A-Z]/.test(t) && t.length > 4 && /\d/.test(t)) found.add(t); }); return found; } function parseErrorSKUs(text) { const map = {}; const lines = text.split('\n').map(l => l.trim()).filter(Boolean); // ── Strategy 1: Tab-separated rows (with fallback for lone SKU lines) ── const tsvLines = lines.filter(l => l.split('\t').length >= 2); if (tsvLines.length > 0 && tsvLines.length >= lines.length * 0.5) { let lastReason = 'See error message'; lines.forEach(line => { const cleanLine = line .replace(/([a-z])([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2') .replace(/([A-Z]{2,})([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2'); const cols = cleanLine.split('\t').map(c => c.trim()); if (cols.length >= 2) { // Full TSV row — extract SKU and reason let skuCol = -1, sku = ''; for (let i = 0; i < cols.length; i++) { const t = cols[i].replace(/[^A-Z0-9-]/gi,'').toUpperCase(); if (t.includes('-') && t.length > 4 && /^[A-Z]/.test(t) && /\d/.test(t)) { skuCol = i; sku = t; break; } } if (sku) { const reason = cols.filter((c,i) => i !== skuCol && c.length > 3 && !/^\d+$/.test(c)) .sort((a,b) => b.length - a.length)[0] || ''; lastReason = classifyReason(reason || line); map[sku] = lastReason; } } else { // Lone SKU line (no tabs) — inherit last known reason const skus = extractAllSKUs(cleanLine); skus.forEach(sku => { if (!map[sku]) map[sku] = lastReason; }); } }); return map; } // ── Strategy 2: Line by line with pre-split for concatenated SKUs ── const reasonGroups = []; lines.forEach(line => { const preLine = line .replace(/([a-z])([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2') .replace(/([A-Z]{2,})([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2'); const skus = extractAllSKUs(preLine); if (!skus.size) { reasonGroups.push({ reason: line, skus: new Set() }); return; } let reasonText = preLine; skus.forEach(s => { reasonText = reasonText.replace(new RegExp(s.replace(/[-]/g,'\\-'), 'gi'), ''); }); reasonText = reasonText.replace(/[,;:\t]+/g, ' ').replace(/\s+/g, ' ').trim(); reasonText = reasonText.replace(/^(SKU\/s?|ERROR|for|the|and|or|is|are|:)\s*/gi, '').trim(); reasonGroups.push({ reason: reasonText, skus }); }); // ── Strategy 3: "reason...SKU/s: A, B, C" ── const p1 = /([^\n]{3,150}?)(?:SKU[s\/]*)[:\s]+([A-Z][A-Z0-9,\s-]*[A-Z0-9])(?=[\s,.]|$)/gi; let m; while ((m = p1.exec(text)) !== null) { const reason = classifyReason(m[1].trim()); extractAllSKUs(m[2]).forEach(sku => { if (!map[sku]) map[sku] = reason; }); } // ── Strategy 4: "SKU X marked as DNO. DNO reason: Y" ── const p2 = /([A-Z][A-Z0-9-]+)\s+marked as DNO[.\s]*DNO reason[:\s]+([^\n.]+)/gi; while ((m = p2.exec(text)) !== null) { const sk = m[1].trim().toUpperCase(); if (/\d/.test(sk)) map[sk] = m[2].trim(); } // ── Apply reason groups ── reasonGroups.forEach(group => { group.skus.forEach(sku => { if (!map[sku]) map[sku] = classifyReason(group.reason); }); }); // ── Final fallback ── const preText = text .replace(/([a-z])([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2') .replace(/([A-Z]{2,})([A-Z]{2,}-[A-Z0-9]+-\d)/g, '$1 $2'); extractAllSKUs(preText).forEach(sku => { if (!map[sku]) map[sku] = classifyReason(text.substring(0, 100)); }); return map; } function classifyReason(raw) { if (!raw || raw.length < 3) return 'See error message'; const l = raw.toLowerCase(); // Exact known patterns first if (l.includes('production discontinued')) return 'Production Discontinued'; if (l.includes('waiting - launch') || (l.includes('waiting') && l.includes('launch'))) return 'Waiting - Launch'; if (l.includes('waiting')) { const m = raw.match(/waiting[\s\-]+([\w\s]+)/i); return 'Waiting - ' + (m ? m[1].trim().substring(0,30) : 'Pending'); } if (l.includes('dno') && l.includes('reason')) { const m = raw.match(/reason[:\s]+([^\n.]+)/i); if (m) return m[1].trim(); } if (l.includes('dno')) return 'DNO - Do Not Order'; if (l.includes('fulfillment type') && l.includes('fbm')) return 'Fulfillment Type Not Set to FBM'; if (l.includes('fulfillment channel')) return 'Fulfillment Channel Mismatch'; if (l.includes('not marked as shippable')) return 'Not Marked as Shippable'; if (l.includes('shippable')) return 'Not Shippable'; if (l.includes('wholesale price') && l.includes('not present')) return 'Wholesale Price Not Present (USD)'; if (l.includes('finance-approved') || l.includes('wholesale price')) return 'Finance-approved Price Missing'; if (l.includes('usd') && l.includes('currency')) return 'USD Currency Price Missing'; if (l.includes('out of stock')) return 'Out of Stock'; if (l.includes('inactive') || l.includes('not active')) return 'Inactive SKU'; if (l.includes('not found') || l.includes('does not exist')) return 'SKU Not Found'; if (l.includes('component')) return 'Component SKU Error'; if (l.includes('price') && l.includes('missing')) return 'Price Missing'; if (l.includes('catalog')) return 'Catalog Configuration Error'; if (l.includes('fulfillment')) return 'Fulfillment Error'; if (l.includes('not present')) return 'Required Field Not Present'; if (l.includes('currency')) return 'Currency Configuration Error'; // Clean up and return the raw text as reason (stripped of SKU patterns) const cleaned = raw .replace(/\b[A-Z]{2,}-[A-Z0-9-]+\b/g, '') .replace(/[\t,;:]+/g, ' ') .replace(/\s+/g, ' ') .trim() .substring(0, 80); return cleaned || 'See error message'; } function extractReason(line) { return classifyReason(line); } // ── Missing functions restored ──────────────────────────────────── function extractSKUsFromText(text) { return extractAllSKUs(text); } function setStatus(msg) { document.getElementById('statusbar').textContent = msg; } function scrollToErrorFile() { const el = document.getElementById('fn-error'); if (el) el.closest('.file-row').scrollIntoView({ behavior: 'smooth', block: 'center' }); } function checkReady() { const map = parseErrorSKUs(document.getElementById('error-input').value); document.getElementById('process-btn').disabled = !(fileData.length && Object.keys(map).length); } function buildMessage(forSlack) { const brand = document.getElementById('brand-name').value.trim(); const semName = foundTeam.sem?.name || ''; const semId = foundTeam.sem?.id || ''; const imName = foundTeam.im?.name || ''; const imId = foundTeam.im?.id || ''; if (!brand) return null; const semTag = forSlack && semId ? '<@' + semId + '>' : (semName || 'SEM'); const imTag = forSlack && imId ? '<@' + imId + '>' : (imName || 'IM'); const skuCount = outputErrored ? outputErrored.length : 0; const skuLine = skuCount > 0 ? ' There are *' + skuCount + ' SKU(s)* with errors.' : ''; return 'Hello ' + semTag + ',\n\n' + 'I encountered a few errors while drafting a PO for *' + brand + '*.' + skuLine + ' ' + 'The PO has been drafted with the remaining SKUs and sent to ' + imTag + '.\n\n' + 'Could you please have a look at the error file attached and help us resolve these so the remaining SKUs can be included?\n\n' + 'Thank you!'; } function resetMessageToDefault() { updateSlackPreview(); } function onBrandInput() { const val = document.getElementById('brand-name').value.trim(); document.getElementById('slack-lookup').style.display = 'none'; document.getElementById('slack-not-found').style.display = 'none'; document.getElementById('btn-send').disabled = true; document.getElementById('slack-receipt').style.display = 'none'; document.getElementById('btn-resend').style.display = 'none'; updateSlackPreview(); showBrandDropdown(val); } function updateSlackPreview() { const msg = buildMessage(false); const el = document.getElementById('slack-preview'); if (!el) return; if (msg) { el.value = msg; el.style.fontStyle = 'normal'; el.style.color = 'var(--text)'; } else { el.value = ''; } } function resetMessageToDefault() { updateSlackPreview(); } async function lookupTeam() { const brand = document.getElementById('brand-name').value.trim(); if (!brand) { alert('Enter a brand name first.'); return; } const cfg = getCfg(); if (!cfg.worker) { alert('Please complete Setup first.'); return; } const btn = document.getElementById('lookup-btn'); btn.innerHTML = ' Looking up...'; btn.disabled = true; try { const res = await fetch(cfg.worker + '/lookup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ brand }) }); const data = await res.json(); if (!data.ok) { document.getElementById('slack-not-found').style.display = 'block'; document.getElementById('slack-not-found').textContent = data.error || 'Brand not found.'; } else { foundTeam = { sem: data.sem, im: data.im }; renderLookupResults(true); updateSlackPreview(); document.getElementById('btn-send').disabled = !(foundTeam.sem?.id || foundTeam.im?.id); } } catch(e) { document.getElementById('slack-not-found').style.display = 'block'; document.getElementById('slack-not-found').textContent = 'Error: ' + e.message; } btn.innerHTML = '🔍 Look Up'; btn.disabled = false; } function initials(name) { return (name||'?').split(' ').map(p=>p[0]||'').join('').toUpperCase().slice(0,2); } const COLORS = ['#4A154B','#2B6CB0','#276749','#C53030','#744210']; function avatarColor(name) { let h=0; for(let c of (name||'')) h=c.charCodeAt(0)+((h<<5)-h); return COLORS[Math.abs(h)%COLORS.length]; } function renderLookupResults(hasIds) { const box = document.getElementById('slack-lookup'); box.style.display = 'block'; document.getElementById('slack-not-found').style.display = 'none'; let html = ''; for (const [role, person] of [['SEM', foundTeam.sem], ['IM', foundTeam.im]]) { if (!person?.name || person.name.toLowerCase() === 'n/a') continue; const color = avatarColor(person.name); html += '
' + '
' + initials(person.name) + '
' + '
' + person.name + '
' + role + '
' + (hasIds ? ('
' + (person.id ? person.id : 'Not found in Slack') + '
') : '') + '
'; } box.innerHTML = html || '
No team members found.
'; } function handleFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = e => { const wb = XLSX.read(e.target.result, { type: 'array', cellDates: false }); const ws = wb.Sheets[wb.SheetNames[0]]; const raw = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '', raw: true }); if (!raw.length) { setStatus('File appears empty.'); return; } fileHeaders = raw[0].map(h => String(h)); fileData = raw.slice(1); document.getElementById('uz-title').textContent = file.name + ' (' + fileData.length + ' rows loaded)'; document.getElementById('uz-sub').textContent = 'Col A="' + fileHeaders[0] + '" (SKU) · Col B="' + (fileHeaders[1]||'') + '" (Qty) · Cols C&D as mm/dd/yyyy'; document.getElementById('drop-zone').classList.add('loaded'); document.getElementById('step3').className = 'step done'; setStatus('Loaded: ' + file.name + ' — ' + fileData.length + ' rows'); renderFilePreview(); checkReady(); }; reader.readAsArrayBuffer(file); } document.getElementById('file-input').addEventListener('change', e => handleFile(e.target.files[0])); const dz = document.getElementById('drop-zone'); dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); }); dz.addEventListener('dragleave', () => dz.classList.remove('drag')); dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('drag'); handleFile(e.dataTransfer.files[0]); }); document.getElementById('error-input').addEventListener('input', function() { const map = parseErrorSKUs(this.value); const keys = Object.keys(map); const prev = document.getElementById('sku-preview'); if (keys.length) { prev.style.display = 'block'; prev.className = 'tag-box tag-green'; prev.textContent = 'Detected ' + keys.length + ' unique error SKU(s): ' + keys.join(', '); document.getElementById('step1').className = 'step done'; if (allBrands.length === 0) loadBrandDropdown(); } else { prev.style.display = 'none'; document.getElementById('step1').className = 'step active'; document.getElementById('slack-card').style.display = 'none'; } checkReady(); }); function analyseAndShowChecklist() { // Parse SKUs and directly generate files — no review step needed skuReasonMap = parseErrorSKUs(document.getElementById('error-input').value); detectedSKUs = Object.keys(skuReasonMap); if (!detectedSKUs.length) { setStatus('No error SKUs detected. Check your error message.'); return; } setStatus('Found ' + detectedSKUs.length + ' error SKU(s) — generating files...'); // Auto-select all SKUs and generate generateFiles(); } function renderList(autoSet) { const list = document.getElementById('sku-list'); list.innerHTML = ''; detectedSKUs.forEach((sku, idx) => { const isAuto = autoSet.has(sku); const item = document.createElement('div'); item.className = 'sku-item' + (isAuto ? ' auto-ticked' : ''); item.id = 'item-' + CSS.escape(sku); item.innerHTML = '' + '' + (idx+1) + '' + '
' + sku + 'auto
' + '
' + (skuReasonMap[sku]||'') + '
'; item.addEventListener('click', e => { if (e.target.tagName !== 'INPUT') { const cb = document.getElementById('cb-' + CSS.escape(sku)); cb.checked = !cb.checked; onToggle(sku, cb.checked); } }); list.appendChild(item); }); } document.getElementById('quicktick-remove').addEventListener('input', function() { const found = extractAllSKUs(this.value); const res = document.getElementById('quicktick-remove-result'); if (!found.size) { res.style.display = 'none'; return; } let checked = 0, notFound = []; detectedSKUs.forEach(sku => { const cb = document.getElementById('cb-' + CSS.escape(sku)); if (!cb) return; if (found.has(sku)) { cb.checked = true; onToggle(sku, true); checked++; } else { cb.checked = false; onToggle(sku, false); } }); found.forEach(sku => { if (!detectedSKUs.includes(sku)) notFound.push(sku); }); res.style.display = 'block'; res.textContent = checked ? 'Checked ' + checked + ' SKU(s) to remove' + (notFound.length ? ' · not in list: ' + notFound.join(', ') : '') : 'None matched.'; updateCount(); syncSelectAll(); }); document.getElementById('quicktick-keep').addEventListener('input', function() { const found = extractAllSKUs(this.value); const res = document.getElementById('quicktick-keep-result'); if (!found.size) { res.style.display = 'none'; return; } let kept = 0, notFound = []; detectedSKUs.forEach(sku => { const cb = document.getElementById('cb-' + CSS.escape(sku)); if (!cb) return; if (found.has(sku)) { cb.checked = false; onToggle(sku, false); kept++; } else { cb.checked = true; onToggle(sku, true); } }); found.forEach(sku => { if (!detectedSKUs.includes(sku)) notFound.push(sku); }); res.style.display = 'block'; res.textContent = kept ? 'Kept ' + kept + ' SKU(s) unchecked, rest checked' + (notFound.length ? ' · not in list: ' + notFound.join(', ') : '') : 'None matched.'; updateCount(); syncSelectAll(); }); function onToggle(sku, checked) { const item = document.getElementById('item-' + CSS.escape(sku)); if (item) item.className = 'sku-item' + (!checked ? ' unchecked' : (item.classList.contains('auto-ticked') ? ' auto-ticked' : '')); updateCount(); syncSelectAll(); } function updateCount() { const n = detectedSKUs.filter(s => document.getElementById('cb-' + CSS.escape(s))?.checked).length; document.getElementById('checked-count').textContent = n + ' of ' + detectedSKUs.length + ' selected'; document.getElementById('generate-btn').disabled = n === 0; } function toggleSelectAll(val) { detectedSKUs.forEach(sku => { const cb = document.getElementById('cb-' + CSS.escape(sku)); if(cb){cb.checked=val;onToggle(sku,val);} }); } function syncSelectAll() { document.getElementById('select-all-cb').checked = detectedSKUs.every(s => document.getElementById('cb-' + CSS.escape(s))?.checked); } function renderFilePreview() { const preview = document.getElementById('file-preview'); const table = document.getElementById('file-preview-table'); const note = document.getElementById('file-preview-note'); if (!fileHeaders.length) { preview.style.display = 'none'; return; } const previewRows = fileData; const cols = Math.min(fileHeaders.length, 6); var html = ''; html += ''; html += ''; for (let i = 0; i < cols; i++) { html += ''; } html += ''; previewRows.forEach(function(row, ri) { html += ''; html += ''; for (let i = 0; i < cols; i++) { const rawVal = row[i]; const val = (i === 2 || i === 3) ? toMMDDYYYY(rawVal) : String(rawVal||''); html += ''; } html += ''; }); html += '
#' + (fileHeaders[i]||'Col '+(i+1)) + '
' + (ri+1) + '' + (i===0 ? '' + val + '' : val) + '
'; table.innerHTML = html; note.textContent = 'Showing all ' + fileData.length + ' rows · ' + fileHeaders.length + ' columns · Col A = SKU'; preview.style.display = 'block'; } function renderOutputPreview(type, headers, rows, wrapId, tableId, noteId, color) { const wrap = document.getElementById(wrapId); const table = document.getElementById(tableId); const note = document.getElementById(noteId); if (!rows.length) { wrap.style.display = 'none'; return; } const cols = Math.min(headers.length, 6); const previewRows = rows; let html = ''; html += ''; html += ''; for (let i = 0; i < cols; i++) { html += ''; } html += ''; previewRows.forEach(function(row, ri) { html += ''; html += ''; for (let i = 0; i < cols; i++) { var val = String(row[i]||''); html += ''; } html += ''; }); html += '
#' + (headers[i]||'Col '+(i+1)) + '
' + (ri+1) + '' + (i===0 ? '' + val + '' : val) + '
'; table.innerHTML = html; note.textContent = 'Showing all ' + rows.length + ' rows'; wrap.style.display = 'block'; } function generateFiles() { // Use all detected SKUs (checklist removed - all are selected by default) const selected = new Set(detectedSKUs); outputClean = []; outputErrored = []; fileData.forEach(row => { const v = String(row[0]||'').trim(); if (selected.has(v)) outputErrored.push([v, row[1]||'', skuReasonMap[v]||'See error message']); else outputClean.push(formatRowDates([...row])); }); const actuallyRemoved = new Set(outputErrored.map(r => String(r[0]||'').trim())); document.getElementById('stat-clean').textContent = outputClean.length; document.getElementById('stat-error').textContent = outputErrored.length; document.getElementById('stat-skus').textContent = actuallyRemoved.size; document.getElementById('fn-clean').textContent = 'clean_skus_' + today + '.xlsx'; document.getElementById('fn-error').textContent = 'error_skus_' + today + '.xlsx'; renderOutputPreview('clean', fileHeaders, outputClean, 'preview-clean', 'preview-clean-table', 'preview-clean-note', '#276749'); renderOutputPreview('error', ['SKU','Qty','Error Reason'], outputErrored, 'preview-error', 'preview-error-table', 'preview-error-note', '#9B2C2C'); document.getElementById('results').style.display = 'block'; document.getElementById('step3').className = 'step done'; document.getElementById('step4').className = 'step done'; document.getElementById('step5').className = 'step active'; const notInFile = [...selected].filter(s => !actuallyRemoved.has(s)); if (notInFile.length) { setStatus('Done! ' + outputClean.length + ' clean · ' + outputErrored.length + ' removed · ⚠ ' + notInFile.length + ' SKU(s) not found in file: ' + notInFile.join(', ')); } else { setStatus('Done! ' + outputClean.length + ' clean rows · ' + outputErrored.length + ' removed · ' + actuallyRemoved.size + ' SKUs.'); } const histBrand = document.getElementById('brand-name')?.value?.trim() || 'Unknown'; const histError = document.getElementById('error-input')?.value?.trim() || ''; const histMsg = document.getElementById('slack-preview')?.value?.trim() || ''; const histSent = document.getElementById('slack-status')?.classList?.contains('ok') || false; saveSession({ errorMessage: histError, detectedSKUs: detectedSKUs, brand: histBrand, semName: foundTeam.sem?.name||'', imName: foundTeam.im?.name||'', slackSent: histSent, slackMessage: histMsg, fileName: document.getElementById('uz-title')?.textContent||'', totalRows: fileData.length, skusRemoved: Array.from(selected), skusKept: detectedSKUs.filter(s=>!selected.has(s)), cleanCount: outputClean.length, errorCount: outputErrored.length, filesGenerated: true, }); document.getElementById('results').scrollIntoView({ behavior: 'smooth' }); document.getElementById('slack-card').style.display = 'block'; document.getElementById('step5').className = 'step active'; if (allBrands.length === 0) loadBrandDropdown(); updateSlackPreview(); } function makeWorkbook(headers, rows, color) { const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet([headers,...rows]); const range = XLSX.utils.decode_range(ws['!ref']||'A1'); for (let C=range.s.c;C<=range.e.c;C++) { const a=XLSX.utils.encode_cell({r:0,c:C}); if(ws[a]) ws[a].s={font:{bold:true,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:color}},alignment:{horizontal:'center'}}; } rows.forEach((row,ri)=>{[2,3].forEach(ci=>{const a=XLSX.utils.encode_cell({r:ri+1,c:ci});if(ws[a])ws[a].t='s';});}); ws['!cols']=headers.map((h,i)=>({wch:Math.min(Math.max(String(h).length,...rows.map(r=>String(r[i]||'').length))+4,45)})); XLSX.utils.book_append_sheet(wb,ws,'Sheet1'); return wb; } function downloadFile(type) { let wb, filename; if (type==='clean') { wb=makeWorkbook(fileHeaders,outputClean,'2D7D46'); filename='clean_skus_'+today+'.xlsx'; } else { wb=makeWorkbook(['SKU','Qty','Error Reason'],outputErrored,'C53030'); filename='error_skus_'+today+'.xlsx'; } XLSX.writeFile(wb, filename); setStatus('Downloaded: ' + filename); } async function sendToSlack() { const editedMsg = document.getElementById('slack-preview').value.trim(); const semId = foundTeam.sem?.id || ''; const semName = foundTeam.sem?.name || ''; // Replace SEM and IM names with @mentions for actual Slack send let msg = editedMsg; if (semId && semName) msg = msg.replace(semName, '<@' + semId + '>'); const imId2 = foundTeam.im?.id || ''; const imName2 = foundTeam.im?.name || ''; if (imId2 && imName2) msg = msg.replace(imName2, '<@' + imId2 + '>'); if (!msg) return; const cfg = getCfg(); if (!cfg.worker) { showSlackStatus('Worker URL not set. Go to Setup.', false); return; } const myId = (cfg.myId || '').trim(); if (!myId) { showSlackStatus('\u26a0 Add your Slack Member ID in \u2699 Setup to be included in the group chat.', false); return; } const btn = document.getElementById('btn-send'); btn.innerHTML = ' Sending...'; btn.disabled = true; const ids = [...new Set([foundTeam.sem?.id, foundTeam.im?.id, myId].filter(Boolean))]; // Generate error Excel file as base64 let fileBase64 = null; let fileName = null; if (outputErrored && outputErrored.length > 0) { try { const wb = makeWorkbook(['SKU', 'Qty', 'Error Reason'], outputErrored, 'C53030'); fileBase64 = XLSX.write(wb, { bookType: 'xlsx', type: 'base64' }); fileName = 'error_skus_' + today + '.xlsx'; console.log('File ready:', fileName, 'size:', fileBase64.length, 'rows:', outputErrored.length); } catch(e) { console.log('File gen error:', e); } } else { console.log('outputErrored is empty:', outputErrored); } try { const res = await fetch(cfg.worker + '/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userIds: ids, message: msg, fileBase64, fileName }) }); const data = await res.json(); if (data.ok) { lastChannelId = data.channelId || null; document.getElementById('slack-status').style.display = 'none'; const receipt = document.getElementById('slack-receipt'); const recipients = [foundTeam.sem?.name, foundTeam.im?.name].filter(Boolean); const fileAttached = data.fileAttached || false; document.getElementById('receipt-recipients').textContent = 'Sent to: ' + recipients.join(', ') + ' and you' + (fileAttached ? ' \u2022 Error file attached \u2713' : ' \u2022 Message only'); document.getElementById('receipt-time').textContent = new Date().toLocaleTimeString(); receipt.style.display = 'block'; document.getElementById('btn-resend').style.display = 'block'; document.getElementById('btn-send').style.display = 'none'; document.getElementById('step5').className = 'step done'; // Update reminder banner const reminder = document.getElementById('file-download-reminder'); if (reminder) { if (fileAttached) { reminder.style.cssText = 'background:#F0FFF4;border:1px solid #9AE6B4;border-radius:8px;padding:10px 14px;margin-bottom:14px;display:flex;align-items:center;gap:10px;'; reminder.innerHTML = '\u2705
Error file attached automatically in Slack!
'; } else { reminder.style.cssText = 'background:#FFFBEB;border:1px solid #F6AD55;border-radius:8px;padding:10px 14px;margin-bottom:14px;display:flex;align-items:center;gap:10px;'; const errMsg = data.error ? ' (' + data.error + ')' : ''; reminder.innerHTML = '\u26a0\uFE0F
Message sent but file not attached' + errMsg + '. Please attach manually in Slack.
'; } } } else { showSlackStatus('Slack error: ' + (data.error || 'unknown'), false); } } catch(e) { showSlackStatus('Network error: ' + e.message, false); } btn.innerHTML = '\u25b6 Send via Slack'; btn.disabled = false; } async function resendToSlack() { const msg = document.getElementById('slack-preview').value.trim(); if (!msg) return; const cfg = getCfg(); if (!cfg.worker) { showSlackStatus('Worker URL not set.', false); return; } const btn = document.getElementById('btn-resend'); btn.innerHTML = ' Sending...'; btn.disabled = true; const myId2 = (cfg.myId || '').trim(); const ids2 = [...new Set([foundTeam.sem?.id, foundTeam.im?.id, myId2].filter(Boolean))]; try { const res = await fetch(cfg.worker + '/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userIds: ids2, message: msg, channelId: lastChannelId }) }); const data = await res.json(); if (data.ok) { document.getElementById('receipt-time').textContent = new Date().toLocaleTimeString() + ' (updated)'; } else { showSlackStatus('Resend error: ' + (data.error||'unknown'), false); } } catch(e) { showSlackStatus('Network error: ' + e.message, false); } btn.innerHTML = '✎ Edit & Resend'; btn.disabled = false; } function copySlackMsg() { const msg = document.getElementById('slack-preview').value.trim() || buildMessage(); if (!msg) { alert('Enter brand name first.'); return; } navigator.clipboard.writeText(msg).then(() => { showSlackStatus('Message copied to clipboard!', true); document.getElementById('step2').className = 'step done'; }).catch(() => alert('Copy failed.')); } function showSlackStatus(msg, ok) { const el = document.getElementById('slack-status'); el.style.display = 'block'; el.className = 'slack-status ' + (ok ? 'ok' : 'err'); el.textContent = msg; } ); el.textContent = msg; }