[object Object]

← back to Dw Photo Capture

Scan: partial/fuzzy wallpaper-file lookup — new /api/wallpaper-search substring-matches any part of combo sku / Mfr Pattern / JS Pattern / Internal Description (FileMaker *q* OR-across-fields), with OCR trailing-char trim tolerance (217500→21750→CHC-21750-style), grouped+coalesced per SKU. Manual search box in the scan modal + auto partial-fallback when exact identify misses; tap a candidate → full detail card

8837b5ffd66559f1fb040606c4b8dec7a6d5f7f5 · 2026-07-08 12:51:20 -0700 · Steve Abrams

Files touched

Diff

commit 8837b5ffd66559f1fb040606c4b8dec7a6d5f7f5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 12:51:20 2026 -0700

    Scan: partial/fuzzy wallpaper-file lookup — new /api/wallpaper-search substring-matches any part of combo sku / Mfr Pattern / JS Pattern / Internal Description (FileMaker *q* OR-across-fields), with OCR trailing-char trim tolerance (217500→21750→CHC-21750-style), grouped+coalesced per SKU. Manual search box in the scan modal + auto partial-fallback when exact identify misses; tap a candidate → full detail card
---
 public/index.html | 36 ++++++++++++++++++++++++++++++++++--
 server.js         | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 70 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index ffcea6e..40266fc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -178,6 +178,17 @@
   .wp-d-row{display:flex;justify-content:space-between;gap:8px;font-size:12px;border-bottom:1px dotted #2a2721;padding-bottom:3px}
   .wp-d-lbl{color:#8f887a}.wp-d-val{color:#eadfc6;font-weight:600;text-align:right}
   .wp-d-desc{margin-top:8px;font-size:11px;color:#b3aa93;line-height:1.4}
+  .fb-search{display:flex;gap:7px;margin:10px 0 2px}
+  .fb-search input{flex:1;min-width:0;padding:9px 11px;font-size:14px;border:1px solid var(--line);border-radius:9px;background:#0f0e0b;color:#eadfc6}
+  .fb-search input::placeholder{color:#6f6857}
+  .fb-search button{flex:none;padding:9px 14px;font-size:13px;font-weight:600;border:1px solid var(--line);border-radius:9px;background:#1a1813;color:#c8bfa8;cursor:pointer}
+  .fb-search button:hover{background:#242017}
+  .wp-c-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}
+  .wp-c{border:1px solid var(--line);border-radius:10px;padding:8px 10px;background:#0f0e0b;cursor:pointer}
+  .wp-c:hover{border-color:#c8bfa8;background:#161410}
+  .wp-c-top{display:flex;justify-content:space-between;gap:8px;align-items:baseline}
+  .wp-c-top b{font-size:13px;color:#eadfc6}.wp-c-sku{font-size:11px;color:#8f887a}
+  .wp-c-sub{font-size:11px;color:#9a917d;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
   .fb-thumb{width:100%;aspect-ratio:1/1;border-radius:10px;background:#000 center/cover no-repeat;display:flex;align-items:center;justify-content:center;font-size:30px}
   #fbResult{margin-top:12px}
   .fb-hit{display:flex;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);border-radius:12px;margin-top:8px}
@@ -440,6 +451,7 @@
       <button class="fb-tile" id="fbFrontTile"><div class="fb-thumb" id="fbFrontThumb">🎨</div><span>Front · pattern</span></button>
     </div>
     <button class="samp-print" id="fbGo" disabled>🔎 Identify</button>
+    <div class="fb-search"><input id="fbSearchInput" type="search" inputmode="search" autocomplete="off" placeholder="or type any part of a SKU / description — e.g. 21750"><button type="button" id="fbSearchBtn">Search</button></div>
     <div class="samp-note" id="fbNote"></div>
     <div id="fbResult"></div>
   </div>
@@ -1534,12 +1546,14 @@ async function fbIdentify(){
   $('#fbGo').disabled=false;
 }
 // On any scan, pull the master product record from the WALLPAPER FileMaker file and show its details.
-async function loadWallpaperDetails(p){ const box=$('#fbWpDetails'); if(!box) return;
+async function loadWallpaperDetails(p){ const box=$('#fbWpDetails')||$('#fbResult'); if(!box) return;
   const sku=encodeURIComponent(p.dw_sku||p.internal_sku||''), code=encodeURIComponent(p.mfr_code||p.mfr_sku||'');
   if(!sku && !code) return;
   box.innerHTML='<div class="wp-d-hd">Wallpaper file…</div>';
   let r; try{ r=await(await fetch(`/api/wallpaper-details?sku=${sku}&code=${code}`)).json(); }catch(e){ box.innerHTML=''; return; }
-  if(!r||!r.ok||!r.found||!r.details){ box.innerHTML='<div class="wp-d-hd wp-d-empty">No matching record in the wallpaper file.</div>'; return; }
+  if(!r||!r.ok||!r.found||!r.details){                         // exact miss → try a partial/fuzzy lookup
+    box.innerHTML='<div class="wp-d-hd wp-d-empty">No exact wallpaper-file record — searching partial…</div>';
+    wallpaperSearch(p.mfr_code||p.mfr_sku||p.dw_sku||'', box); return; }
   const d=r.details;
   const rows=[['Mfr #','mfr_pattern'],['Collection','series'],['Width','width'],['Repeat','repeat'],
     ['Retail','retail'],['Net','net'],['Cost','cost'],['Roll','roll_size'],['Minimum','minimum'],
@@ -1550,6 +1564,22 @@ async function loadWallpaperDetails(p){ const box=$('#fbWpDetails'); if(!box) re
   const desc=d.internal_desc?`<div class="wp-d-desc">${d.internal_desc}</div>`:'';
   box.innerHTML=`<div class="wp-d-card"><div class="wp-d-hd">📒 Wallpaper file${r.matched>1?` · ${r.matched} records`:''}</div><div class="wp-d-grid">${rows.join('')}</div>${desc}</div>`;
 }
+// Partial / fuzzy search of the WALLPAPER file by any part of a SKU or description. Renders a candidate
+// list into `box` (defaults to #fbResult for the manual search box).
+async function wallpaperSearch(q, box){ box=box||$('#fbResult'); if(!box) return;
+  q=(q||'').trim(); if(q.replace(/[^A-Za-z0-9]/g,'').length<3){ box.innerHTML='<div class="wp-d-hd wp-d-empty">Type at least 3 characters.</div>'; return; }
+  box.innerHTML='<div class="wp-d-hd">Searching the wallpaper file…</div>';
+  let r; try{ r=await(await fetch('/api/wallpaper-search?q='+encodeURIComponent(q))).json(); }catch(e){ box.innerHTML='<div class="wp-d-hd wp-d-empty">Search failed.</div>'; return; }
+  const cands=(r&&r.candidates)||[];
+  if(!cands.length){ box.innerHTML=`<div class="wp-d-hd wp-d-empty">No wallpaper-file match for “${q}”.</div>`; return; }
+  const rowHtml=c=>{ const bits=[c.width&&('W '+c.width),c.repeat&&('R '+c.repeat),c.retail&&('$'+c.retail),c.supplier,c.line].filter(Boolean).join(' · ');
+    return `<div class="wp-c" onclick="loadWallpaperDetailsBySku('${(c.combo_sku||'').replace(/'/g,'')}','${(c.mfr_pattern||'').replace(/'/g,'')}')">
+      <div class="wp-c-top"><b>${c.mfr_pattern||c.combo_sku||'—'}</b><span class="wp-c-sku">${c.combo_sku||''}</span></div>
+      <div class="wp-c-sub">${[c.series,bits].filter(Boolean).join(' · ')||c.internal_desc||''}</div></div>`; };
+  box.innerHTML=`<div class="wp-d-hd">📒 ${cands.length} partial match${cands.length>1?'es':''} for “${q}”</div><div class="wp-c-list">${cands.map(rowHtml).join('')}</div>`;
+}
+// tapping a candidate loads its full detail card
+async function loadWallpaperDetailsBySku(sku,code){ await loadWallpaperDetails({dw_sku:sku,mfr_code:code}); }
 // INCOMING SAMPLES (Check Sample In): show the ordered-not-received queue with image+spec; on a scan, flag the match.
 let _incoming=[];
 async function loadIncoming(){ const box=$('#fbIncoming'); if(!box) return; box.innerHTML='<div class="inc-hd">Loading samples coming in…</div>';
@@ -1590,6 +1620,8 @@ $('#fbFrontTile').addEventListener('click',()=>$('#fbFrontInput').click());
 $('#fbBackInput').addEventListener('change',e=>fbRead(e.target.files[0],true));
 $('#fbFrontInput').addEventListener('change',e=>fbRead(e.target.files[0],false));
 $('#fbGo').addEventListener('click',fbIdentify);
+$('#fbSearchBtn').addEventListener('click',()=>wallpaperSearch($('#fbSearchInput').value));
+$('#fbSearchInput').addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); wallpaperSearch(e.target.value); } });
 
 // ── Add / Update item: capture up to 10 photos + 6 videos → Shopify (draft-create OR live-update) ──
 const MAX_PHOTOS=10, MAX_VIDEOS=6;
diff --git a/server.js b/server.js
index c16ff3f..9dc0227 100644
--- a/server.js
+++ b/server.js
@@ -1447,6 +1447,42 @@ const appHandler = (req, res) => {
     return;
   }
 
+  // Partial / fuzzy lookup in the WALLPAPER file — "we may miss the prefix". Substring-matches the query
+  // against combo sku, Mfr Pattern, JS Pattern AND Internal Description (FileMaker `*q*` wildcards, OR
+  // across fields). Tolerates a mis-scanned trailing char by progressively trimming from the right
+  // (so "217500" still finds "CHC-21750" on the shared "21750"). Results grouped+coalesced per SKU.
+  if (u.pathname === '/api/wallpaper-search' && req.method === 'GET') {
+    if (!FM_ENABLED()) return send(res, 200, { ok: false, err: 'FileMaker not configured' });
+    (async () => {
+      const raw = (u.searchParams.get('q') || '').trim();
+      const alnum = raw.replace(/[^A-Za-z0-9]/g, '');
+      if (alnum.length < 3) return send(res, 200, { ok: true, candidates: [], note: 'need ≥3 chars' });
+      const LAYOUT2 = process.env.FM_WP_LAYOUT || 'Basic List of Fields';
+      const MAP = [['mfr_pattern','Mfr Pattern'],['js_pattern','JS Pattern'],['series','Series'],
+        ['width','Width'],['repeat','Repeat'],['retail','Retail Price'],['net','Net Price'],['cost','Cost'],
+        ['supplier','Supplier'],['roll_size','HP Roll Size'],['minimum','Minimum'],['border','Border'],
+        ['five_ten','Five Ten Code'],['line','Line'],['internal_desc','Internal Description'],['combo_sku','combo sku']];
+      const FIELDS = ['combo sku', 'Mfr Pattern', 'JS Pattern', 'Internal Description'];
+      // token list: full alnum, then trimmed 1-char-at-a-time (OCR dropped/added a trailing digit)
+      const tokens = []; for (let t = alnum; t.length >= 4 && tokens.length < 3; t = t.slice(0, -1)) tokens.push(t);
+      let recs = [];
+      for (const tok of tokens) {
+        const q = FIELDS.map(f => ({ [f]: '*' + tok + '*' }));    // array = OR across the 4 fields
+        try { const r = await FM.fmFind(FM_DB, LAYOUT2, q, { limit: 40, portal: [] }); recs = r.records || []; }
+        catch (e) { return send(res, 200, { ok: false, err: e.message }); }
+        if (recs.length) break;                                    // stop at the first token that hits
+      }
+      // group + coalesce per SKU (the mega-table has several rows per SKU; specs live on the master row)
+      const groups = new Map();
+      for (const rec of recs) { const f = rec.fieldData;
+        const key = (f['combo sku'] || f['Mfr Pattern'] || ('r' + rec.recordId)).toString();
+        let g = groups.get(key); if (!g) { g = {}; groups.set(key, g); }
+        for (const [k, fld] of MAP) { if (!g[k]) { const v = (f[fld] || '').toString().trim(); if (v && v !== '=') g[k] = v; } } }
+      send(res, 200, { ok: true, candidates: [...groups.values()].slice(0, 25), matched: groups.size, query: alnum });
+    })();
+    return;
+  }
+
   // Print-sticker "send": STAMP the sample as sent (Date WP Sample Sent = today — Steve's "field to
   // fill in") and, if a flag field is configured, ALSO flag the record so the Mac's FileMaker Pro
   // poller runs the physical Zebra print. Write is dryRun unless FM_WRITE=1 (protects the live file).

← c3d8429 Scan → show WALLPAPER FileMaker details: new /api/wallpaper-  ·  back to Dw Photo Capture  ·  Scan fixes: (1) scrub DW's OWN header from identification — 5cea2c8 →