← back to Filemaker Mcp
build-grs-map.mjs
99 lines
// Build a clean GRS dw_sku -> real mfr code map from the FM dump.
// Read-only. Produces a reviewed fill map + a junk/needs-manual list.
import fs from 'fs';
const SRC = process.argv[2];
const OUT = process.env.HOME + '/Desktop/GRS_mfr_fill_map_' +
new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.csv';
// --- tiny CSV parser (quoted) ---
function parseCSV(txt) {
const rows = []; let i = 0, f = '', row = [], q = false;
while (i < txt.length) {
const c = txt[i];
if (q) { if (c === '"') { if (txt[i + 1] === '"') { f += '"'; i++; } else q = false; } else f += c; }
else { if (c === '"') q = true; else if (c === ',') { row.push(f); f = ''; } else if (c === '\n') { row.push(f); rows.push(row); row = []; f = ''; } else if (c !== '\r') f += c; }
i++;
}
if (f.length || row.length) { row.push(f); rows.push(row); }
return rows;
}
const raw = parseCSV(fs.readFileSync(SRC, 'utf8'));
const hdr = raw.shift();
const idx = Object.fromEntries(hdr.map((h, i) => [h, i]));
// Extract a real mfr code token from a free-text Mfr Pattern.
// Returns {code, kind} where kind = REAL | PLACEHOLDER | JUNK | BLANK
function classify(mfrRaw, jsPattern) {
const s = (mfrRaw || '').trim();
if (!s) return { code: '', kind: 'BLANK' };
const up = s.toUpperCase();
if (/DO NOT SELL|CONFUSING|TEST RECORD|DELETE|DISCONTINUE|SEE |NEED SAMPLE/i.test(s)) return { code: '', kind: 'JUNK' };
// grab leading code-like token: letters then digits (e.g. SC5823, GT3940, SUA211, A4, BL72600)
const m = up.match(/\b([A-Z]{1,4}\d{1,6}[A-Z]?)\b/);
if (m) {
const code = m[1];
if (code === String(jsPattern || '').toUpperCase()) return { code: '', kind: 'PLACEHOLDER' };
return { code, kind: 'REAL' };
}
// pure-number mfr that differs from js pattern -> possible real numeric code, flag as REVIEW
const n = up.match(/^(\d{2,6})\b/);
if (n) {
if (n[1] === String(jsPattern || '')) return { code: '', kind: 'PLACEHOLDER' };
return { code: n[1], kind: 'REVIEW_NUM' };
}
return { code: '', kind: 'JUNK' };
}
const norm = s => (s || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
// group all FM records by dw base sku
const byBase = new Map();
for (const r of raw) {
if (!r.length || !r[idx.combo_sku]) continue;
const combo = norm(r[idx.combo_sku]); // GRS43023
if (!combo.startsWith('GRS')) continue;
const base = 'GRS-' + combo.slice(3); // GRS-43023
const rec = {
base, mfrRaw: r[idx.mfr_pattern], js: r[idx.js_pattern],
supplier: r[idx.supplier], net: r[idx.net_price],
jpg: r[idx.jpg_name], recId: r[idx.fm_record_id]
};
rec.cls = classify(rec.mfrRaw, rec.js);
if (!byBase.has(base)) byBase.set(base, []);
byBase.get(base).push(rec);
}
// pick best record per base: prefer REAL > REVIEW_NUM > PLACEHOLDER > JUNK > BLANK
const rank = { REAL: 5, REVIEW_NUM: 4, PLACEHOLDER: 2, JUNK: 1, BLANK: 0 };
const out = [];
const counts = {};
for (const [base, recs] of byBase) {
recs.sort((a, b) => rank[b.cls.kind] - rank[a.cls.kind]);
const best = recs[0];
const status = best.cls.kind;
counts[status] = (counts[status] || 0) + 1;
// pick a supplier from any record that has a non-DW one
const supp = (recs.find(r => r.supplier && !/designer wallcoverings/i.test(r.supplier))?.supplier)
|| best.supplier || '';
out.push({
dw_sku: base, clean_mfr: best.cls.code, status,
mfr_raw: (best.mfrRaw || '').trim(), supplier: supp,
net: best.net || '', dup_records: recs.length
});
}
out.sort((a, b) => a.dw_sku.localeCompare(b.dw_sku, undefined, { numeric: true }));
const csvq = s => '"' + String(s ?? '').replace(/"/g, '""') + '"';
const cols = ['dw_sku', 'clean_mfr', 'status', 'mfr_raw', 'supplier', 'net', 'dup_records'];
fs.writeFileSync(OUT, cols.join(',') + '\n' + out.map(o => cols.map(c => csvq(o[c])).join(',')).join('\n') + '\n');
console.log('== GRS mfr map built ==');
console.log('unique GRS base skus in FM:', out.length);
console.log('status counts:', JSON.stringify(counts, null, 0));
console.log('REAL codes recovered:', out.filter(o => o.status === 'REAL').length);
console.log('out ->', OUT);
console.log('\n-- GRS-43023 check --');
console.log(JSON.stringify(out.find(o => o.dw_sku === 'GRS-43023')));
console.log('\n-- sample REAL codes --');
for (const o of out.filter(o => o.status === 'REAL').slice(0, 12)) console.log(`${o.dw_sku} -> ${o.clean_mfr} [${o.mfr_raw}]`);