← back to Filemaker Mcp
pull-grs-client.mjs
58 lines
// Pull GRS via Full View, capturing CLIENT-ACTIVITY signals, then intersect with the dedup
// plan to produce a CLIENT-SAFE deletable list: duplicate masters that NO client ever touched.
// Rule (Steve): do NOT delete if it has a client account number OR any sample-request/sent date.
// Read-only.
import fs from 'fs';
const env = JSON.parse(fs.readFileSync('/tmp/fmenv.json', 'utf8'));
for (const [k, v] of Object.entries(env)) process.env[k] = v;
const fm = await import('./src/fm-client.js');
const DB = 'WALLPAPER', LAYOUT = '*List Wallpapers - Full View', PAGE = 400;
const Q = String.fromCharCode(34);
// Any of these populated => a client touched this record => NEVER delete.
const ACCOUNT_FIELDS = ['account', 'Clients::Company', 'Sidemark', 'project name', 'Client Notes'];
const DATE_FIELDS = ['today for client', 'Date WP Sample Sent', 'Date Sample Request printed for vendor', 'Date Email Sent to Vendor after 10 Days'];
const touched = new Map(); // recordId -> reason string ('' if clean)
let offset = 1, total = null;
while (true) {
let res, tries = 0;
while (true) { try { res = await fm.findRecords(DB, LAYOUT, [{ Series: 'GRS' }], { limit: PAGE, offset }); break; } catch (e) { if (String(e.message).includes('[401]')) { res = { records: [] }; break; } if (++tries >= 4) throw e; await new Promise(r => setTimeout(r, 1500 * tries)); } }
total = res?.dataInfo?.totalRecordCount ?? total;
const recs = res.records || []; if (!recs.length) break;
for (const r of recs) {
const f = r.fieldData || {};
const acct = ACCOUNT_FIELDS.filter(k => String(f[k] ?? '').trim() !== '');
const dates = DATE_FIELDS.filter(k => String(f[k] ?? '').trim() !== '');
const reasons = [...acct.map(k => 'acct:' + k), ...dates.map(k => 'date:' + k)];
touched.set(r.recordId, reasons.join(';'));
}
offset += PAGE; if (total && offset > total) break;
}
// intersect with dedup safe-delete plan
function P(t){const R=[];let i=0,f='',r=[],q=false;while(i<t.length){const c=t[i];if(q){if(c===Q){if(t[i+1]===Q){f+=Q;i++;}else q=false;}else f+=c;}else{if(c===Q)q=true;else if(c===','){r.push(f);f='';}else if(c==='\n'){r.push(f);R.push(r);r=[];f='';}else if(c!=='\r')f+=c;}i++;}if(f.length||r.length){r.push(f);R.push(r);}return R;}
const plan = P(fs.readFileSync(process.env.HOME + '/Desktop/GRS_dedup_DELETE_plan_20260819.csv', 'utf8'));
const ph = plan.shift(); const pi = Object.fromEntries(ph.map((x, i) => [x, i]));
const safeDel = plan.filter(r => r.length && r[pi.disposition] === 'safe-delete');
let clientSafe = [], held = [];
for (const r of safeDel) {
const rid = r[pi.delete_record_id];
const reason = touched.get(rid);
if (reason === undefined) { held.push([...r.slice(0,7), 'NOT-FOUND-IN-FULLVIEW']); continue; } // conservative: hold
if (reason) held.push([r[pi.combo], rid, r[pi.mfr_pattern], reason]);
else clientSafe.push([r[pi.combo], rid, r[pi.mfr_pattern], r[pi.keeper_record_id]]);
}
const qq = x => Q + String(x ?? '').replace(/"/g, Q + Q) + Q;
const outSafe = process.env.HOME + '/Desktop/GRS_dedup_CLIENT_SAFE_delete_20260819.csv';
const outHeld = process.env.HOME + '/Desktop/GRS_dedup_HELD_client_touched_20260819.csv';
fs.writeFileSync(outSafe, 'combo,delete_record_id,mfr_pattern,keeper_record_id\n' + clientSafe.map(r => r.map(qq).join(',')).join('\n') + '\n');
fs.writeFileSync(outHeld, 'combo,delete_record_id,mfr_pattern,hold_reason\n' + held.map(r => r.map(qq).join(',')).join('\n') + '\n');
console.log('=== CLIENT-SAFE dedup scope (Steve rule: no acct#, no sample date) ===');
console.log(' dedup safe-delete targets (pre-client-check):', safeDel.length);
console.log(' -> CLIENT-SAFE to delete (never client-touched):', clientSafe.length, '->', outSafe);
console.log(' -> HELD (has client acct# or a sample date):', held.length, '->', outHeld);
console.log(' GRS records scanned for client activity:', touched.size);