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 += '' + (fileHeaders[i]||'Col '+(i+1)) + ' ';
}
html += ' ';
previewRows.forEach(function(row, ri) {
html += '';
html += '' + (ri+1) + ' ';
for (let i = 0; i < cols; i++) {
const rawVal = row[i];
const val = (i === 2 || i === 3) ? toMMDDYYYY(rawVal) : String(rawVal||'');
html += ''
+ (i===0 ? '' + val + ' ' : val) + ' ';
}
html += ' ';
});
html += '
';
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 += '' + (headers[i]||'Col '+(i+1)) + ' ';
}
html += ' ';
previewRows.forEach(function(row, ri) {
html += '';
html += '' + (ri+1) + ' ';
for (let i = 0; i < cols; i++) {
var val = String(row[i]||'');
html += ''
+ (i===0 ? '' + val + ' ' : val) + ' ';
}
html += ' ';
});
html += '
';
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;
}