[object Object]

← back to Mfr Review Viewer Corruption

viewer: auto-apply keep/delete rule (delete broken no-sample masters, keep real sample records) + sample-history badges

8c74f843d675a1df555ecb440ec4cf43c001cd14 · 2026-08-27 09:00:49 -0700 · Steve Abrams

Files touched

Diff

commit 8c74f843d675a1df555ecb440ec4cf43c001cd14
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 27 09:00:49 2026 -0700

    viewer: auto-apply keep/delete rule (delete broken no-sample masters, keep real sample records) + sample-history badges
---
 lib/candidates.mjs | 37 ++++++++++++++++++++++++++++++++++++-
 public/index.html  | 29 +++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+), 1 deletion(-)

diff --git a/lib/candidates.mjs b/lib/candidates.mjs
index af5b83b..2a86dde 100644
--- a/lib/candidates.mjs
+++ b/lib/candidates.mjs
@@ -157,6 +157,19 @@ async function fmCandidates(dwSku) {
     const mfrNote = mfrPattern || (/#/.test(chaseMemo) ? chaseMemo : '');
     const noteMfr = parseNoteMfr(mfrNote) || parseNoteMfr(chaseMemo);
 
+    // ---- sample-order history (Steve's rule: a "master" = a record where a client
+    // sample was NEVER ordered). Both fields live on the Full View layout. Non-empty
+    // in EITHER = a client sample was ordered against this record -> a KEEP record. ----
+    const sampleReq = String(fd['today for client'] || '').trim();
+    const sampleSent = String(fd['Date WP Sample Sent'] || '').trim();
+    const sampleOrdered = !!(sampleReq || sampleSent);
+    const sampleDate = sampleSent || sampleReq || '';
+    // ---- placeholder mfr = the parsed code is purely the DW# numeric tail (no real
+    // alpha code) i.e. the "DW# == Mfr SKU" corruption, not a real manufacturer code. ----
+    const codeHasAlpha = /[A-Za-z]/.test(noteMfr || '');
+    const codeDigits = String(noteMfr || mfrPattern || '').replace(/[^0-9]/g, '');
+    const mfrPlaceholder = !codeHasAlpha && !!tail && codeDigits === tail;
+
     const cand = {
       recordId: rec.recordId,
       comboSku: String(fd['combo sku'] || '').trim(),
@@ -173,6 +186,9 @@ async function fmCandidates(dwSku) {
       mfrNote,                                       // free-text carrier of the real code
       noteMfr,                                       // best-guess real mfr parsed from the note
       skuMatch,                                      // true = Series/combo normalizes to this dw_sku
+      sampleOrdered,                                 // true = a client sample WAS ordered (a KEEP record)
+      sampleDate,                                    // the sample sent/request date for display
+      mfrPlaceholder,                                // true = mfr is just the DW# placeholder (no real code)
     };
     byId.set(rec.recordId, cand);
   }
@@ -248,5 +264,24 @@ export async function candidatesForSku(dwSku) {
     fmCandidates(dwSku).catch((e) => ({ filemaker: [], fmError: e.message })),
     unifiedCandidates(dwSku).catch(() => []),
   ]);
-  return { filemaker: fm.filemaker || [], unified: unified || [], fmError: fm.fmError || null };
+  const recs = fm.filemaker || [];
+
+  // ---- apply Steve's rule (2026-08-27): DELETE the broken no-sample "masters",
+  // KEEP the real sample-order records. A record is DELETE-suggested iff it has NO
+  // sample history AND its mfr is just the DW# placeholder — GUARDED so we never
+  // delete a SKU to zero: at least one KEEP must remain. A KEEP = a record that
+  // either had a client sample ordered OR carries a real (non-placeholder) mfr. ----
+  const isKeepWorthy = (r) => r.sampleOrdered || (!r.mfrPlaceholder && /[A-Za-z]/.test(r.noteMfr || r.mfrPattern || ''));
+  const keepers = recs.filter(isKeepWorthy);
+  // the real mfr to pre-fill: prefer a sample-ordered record's real code, else any real code.
+  const realSrc = keepers.find((r) => r.sampleOrdered && /[A-Za-z]/.test(r.noteMfr || '')) || keepers.find((r) => /[A-Za-z]/.test(r.noteMfr || ''));
+  const realMfr = realSrc ? (realSrc.noteMfr || realSrc.mfrPattern || '') : '';
+  const keepRid = realSrc ? String(realSrc.recordId) : (keepers[0] ? String(keepers[0].recordId) : '');
+  const canPrune = keepers.length > 0; // guard: only suggest deletes if a keeper survives
+  for (const r of recs) {
+    r.deleteSuggested = canPrune && !r.sampleOrdered && r.mfrPlaceholder && String(r.recordId) !== keepRid;
+  }
+
+  return { filemaker: recs, unified: unified || [], fmError: fm.fmError || null,
+    suggestion: { realMfr, keepRid, deleteRids: recs.filter((r) => r.deleteSuggested).map((r) => String(r.recordId)) } };
 }
diff --git a/public/index.html b/public/index.html
index 0a8e8dc..0666bb9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -123,6 +123,10 @@
   .prune-summary { font-size:11px; color:var(--muted); margin-top:2px; }
   .prune-summary .n-del { color:var(--red); font-weight:700; }
   .prune-summary .n-keep { color:var(--teal); font-weight:700; }
+  .samp-badge { display:inline-block; font-size:10px; padding:1px 6px; border-radius:8px; margin-right:5px; font-weight:600; }
+  .samp-yes { background:#123024; color:#7fe0b0; }
+  .samp-no  { background:#2a2320; color:#d9b48a; }
+  .samp-sug { background:#3a1a1f; color:#ff9aa6; border:1px solid #6b2b33; }
 
   /* ---- Sticky action bar ---- */
   .action { position:fixed; left:280px; right:0; bottom:0; background:var(--panel);
@@ -368,12 +372,31 @@ async function toggleCandidates(sku, tog, panel){
     const r = await fetch('api/candidates?dw_sku='+encodeURIComponent(sku));
     const j = await r.json();
     state.cand[sku] = j;
+    applySuggestion(sku, j);        // auto-apply Steve's keep/delete rule (once per SKU)
     renderCandidates(sku, panel);
   } catch(e){
     panel.innerHTML = '<div class="cand-err">Failed to load candidates: '+esc(e.message)+'</div>';
   }
 }
 
+// Auto-apply the backend's keep/delete suggestion ONCE per SKU. Steve's rule
+// (2026-08-27): DELETE the broken no-sample "masters", KEEP the real sample-order
+// records. Pre-marks the suggested deletes, remembers the keep, and pre-fills the
+// real mfr — the reviewer can still override any of it before staging.
+function applySuggestion(sku, j){
+  if(!state.applied) state.applied = {};
+  if(state.applied[sku]) return;                    // never clobber a prior manual choice
+  state.applied[sku] = true;
+  const sug = j && j.suggestion; if(!sug) return;
+  if(sug.deleteRids && sug.deleteRids.length){
+    const set = state.delRids[sku] || new Set();
+    for(const rid of sug.deleteRids) set.add(String(rid));
+    state.delRids[sku] = set;
+  }
+  if(sug.keepRid) state.keepRid[sku] = String(sug.keepRid);
+  if(sug.realMfr) chooseMfr(sku, sug.realMfr);      // pre-fill the confirmed real mfr
+}
+
 // Write a chosen real mfr into this row's confirmed-mfr input + edits state, re-render card.
 function chooseMfr(sku, value){
   state.edits[sku] = value;
@@ -435,6 +458,12 @@ function renderCandidates(sku, panel){
           `<span class="cand-line cand-meta">`+
             (c.name?`${esc(c.name)} · `:'')+(c.color?`${esc(c.color)} · `:'')+(c.vid?`vid ${esc(c.vid)}`:'')+
           `</span>`+
+          `<span class="cand-line">`+
+            (c.sampleOrdered
+              ? `<span class="samp-badge samp-yes" title="a client sample WAS ordered against this record — KEEP">🧾 sampled${c.sampleDate?' '+esc(c.sampleDate):''}</span>`
+              : `<span class="samp-badge samp-no" title="no client sample ever ordered — a 'master' template (deletable when its mfr is just the DW# placeholder)">⛔ never sampled</span>`)+
+            (c.deleteSuggested?`<span class="samp-badge samp-sug" title="auto-suggested for deletion by the rule (no sample + placeholder mfr, a keeper survives)">auto-delete</span>`:'')+
+          `</span>`+
           (real?`<span class="cand-line">→ real mfr: <span class="cand-realmfr">${esc(real)}</span></span>`:'')+
         `</span>
         <span class="del-wrap" title="Mark THIS master for deletion (keep the rest)">

← 8e3663b mfr-review-viewer: gated delete-record (keep-rest) staging w  ·  back to Mfr Review Viewer Corruption  ·  viewer: per-chip red 'Remove suggested (N)' button — one-cli 377ff9b →