[object Object]

← back to Dw Photo Capture

Scan → show WALLPAPER FileMaker details: new /api/wallpaper-details (queries 'Basic List of Fields' by combo sku→Mfr Pattern, coalesces non-empty specs across the mega-table's multiple rows, portal:[] so no stall) + loadWallpaperDetails() renders a spec card (Mfr#/Collection/Width/Repeat/Retail/Net/Cost/Roll/Min/Border/Price-code/Line/SKU) under every scan result. 58k+ SKUs carry rich FM specs; sparse scraped lines show what FM has

c3d842972ed0308a1817a2bc4ff628d12360369e · 2026-07-08 12:42:42 -0700 · Steve Abrams

Files touched

Diff

commit c3d842972ed0308a1817a2bc4ff628d12360369e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 12:42:42 2026 -0700

    Scan → show WALLPAPER FileMaker details: new /api/wallpaper-details (queries 'Basic List of Fields' by combo sku→Mfr Pattern, coalesces non-empty specs across the mega-table's multiple rows, portal:[] so no stall) + loadWallpaperDetails() renders a spec card (Mfr#/Collection/Width/Repeat/Retail/Net/Cost/Roll/Min/Border/Price-code/Line/SKU) under every scan result. 58k+ SKUs carry rich FM specs; sparse scraped lines show what FM has
---
 public/index.html | 26 +++++++++++++++++++++++++-
 server.js         | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index bfeab04..ffcea6e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -172,6 +172,12 @@
   .inc-sub{font-size:10px;color:#9a917d;margin-top:2px}
   .inc-recv{margin-top:5px;width:100%;padding:5px 4px;font-size:11px;font-weight:600;border:1px solid #2bd06a;background:rgba(43,208,106,.12);color:#2bd06a;border-radius:7px;cursor:pointer}
   .inc-recv:hover{background:rgba(43,208,106,.22)}.inc-recv:disabled{opacity:.55;cursor:default}
+  .wp-d-card{margin-top:10px;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:#0f0e0b}
+  .wp-d-hd{font-size:12px;font-weight:600;color:#c8bfa8;margin-bottom:8px}.wp-d-empty{color:#7d7663;font-weight:400}
+  .wp-d-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:5px 14px}
+  .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-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}
@@ -1521,11 +1527,29 @@ async function fbIdentify(){
     const p=r.primary, conf=(r.confidence||'').replace('-','');
     const via=r.source==='qr'?'🔗 QR':r.source==='code'?'📄 label code':'🎨 visual';
     $('#fbNote').innerHTML=`Source: ${via}${r.corroborated?' · ✓ front+back agree':''}`;
-    $('#fbResult').innerHTML=`<div class="fb-hit">${p.image?`<img src="${p.image}">`:''}<div><b>${p.pattern||p.mfr_sku||p.dw_sku||'—'}</b><span class="fb-conf ${conf}">${r.confidence}</span><br><small>${[p.vendor,p.dw_sku,p.mfr_code].filter(Boolean).join(' · ')}</small></div></div>`+fbVisualHtml(r.visual);
+    $('#fbResult').innerHTML=`<div class="fb-hit">${p.image?`<img src="${p.image}">`:''}<div><b>${p.pattern||p.mfr_sku||p.dw_sku||'—'}</b><span class="fb-conf ${conf}">${r.confidence}</span><br><small>${[p.vendor,p.dw_sku,p.mfr_code].filter(Boolean).join(' · ')}</small></div></div>`+`<div id="fbWpDetails"></div>`+fbVisualHtml(r.visual);
+    loadWallpaperDetails(p);   // pull the master product record from the WALLPAPER FileMaker file
     if(!_fbLookup){ matchIncoming(p); checkSampleRequest(p.dw_sku||p.mfr_code, p.mfr_code); }   // Check-Sample-In: flag the incoming match + print popup; pure Look-Up SKU skips both
   }catch(e){ $('#fbNote').textContent='Failed: '+e.message; }
   $('#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;
+  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; }
+  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'],
+    ['Border','border'],['Price code','five_ten'],['Line','line'],['SKU','combo_sku']]
+    .filter(([,k])=>d[k]).map(([lbl,k])=>{
+      let v=d[k]; if((k==='retail'||k==='net'||k==='cost')&&/^\d/.test(v)) v='$'+v;
+      return `<div class="wp-d-row"><span class="wp-d-lbl">${lbl}</span><span class="wp-d-val">${v}</span></div>`; });
+  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>`;
+}
 // 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>';
diff --git a/server.js b/server.js
index 571516b..c16ff3f 100644
--- a/server.js
+++ b/server.js
@@ -1415,6 +1415,38 @@ const appHandler = (req, res) => {
     return;
   }
 
+  // Full product record from the WALLPAPER FileMaker file, keyed by the scanned SKU. WALLPAPER2 is one
+  // mega-table (product master + sample-request rows share a combo sku), so a lookup returns several
+  // records — most spec fields blank on the sample rows. We COALESCE the non-empty value per field across
+  // all matches so the master record's specs surface. 'Basic List of Fields' has the spec fields + NO
+  // portals (so it can't stall like the sample-request layout does).
+  if (u.pathname === '/api/wallpaper-details' && req.method === 'GET') {
+    if (!FM_ENABLED()) return send(res, 200, { ok: false, err: 'FileMaker not configured' });
+    (async () => {
+      const sku = (u.searchParams.get('sku') || '').trim();     // house combo sku (DWxx…)
+      const code = (u.searchParams.get('code') || '').trim();   // mfr pattern (vendor code)
+      if (!sku && !code) return send(res, 400, { ok: false, err: 'sku or code required' });
+      const LAYOUT2 = process.env.FM_WP_LAYOUT || 'Basic List of Fields';
+      // fields to surface: [clean key, FileMaker field name]
+      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']];
+      let recs = [];
+      try {
+        if (sku) { const r = await FM.fmFind(FM_DB, LAYOUT2, [{ 'combo sku': '==' + sku }], { limit: 20, portal: [] }); recs = r.records || []; }
+        if (!recs.length && code) { const r = await FM.fmFind(FM_DB, LAYOUT2, [{ 'Mfr Pattern': '==' + code }], { limit: 20, portal: [] }); recs = r.records || []; }
+      } catch (e) { return send(res, 200, { ok: false, err: e.message }); }
+      if (!recs.length) return send(res, 200, { ok: true, found: false, details: null, matched: 0 });
+      const details = {};
+      for (const [key, fld] of MAP) {                           // coalesce: first non-empty value across matches
+        for (const rec of recs) { const v = (rec.fieldData[fld] || '').toString().trim(); if (v && v !== '=') { details[key] = v; break; } }
+      }
+      send(res, 200, { ok: true, found: true, details, matched: recs.length });
+    })();
+    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).

← d4db33e doc: cross-reference the shared Date WP Sample Sent writer (  ·  back to Dw Photo Capture  ·  Scan: partial/fuzzy wallpaper-file lookup — new /api/wallpap 8837b5f →