[object Object]

← back to Dw Photo Capture

Check Sample In: 'Received' button stamps Date WP Sample Sent=today (Steve's receive-stamp field) via POST /api/mark-received (fmSet), drops the card from the incoming queue; scan-match surfaces a one-tap 'Check in — received today'. Input-guarded (400 on missing recordId/bad json), no live write on my tests

999db250f4869b7827a5d0ba15951e5a66e0f1f4 · 2026-07-08 06:48:37 -0700 · Steve Abrams

Files touched

Diff

commit 999db250f4869b7827a5d0ba15951e5a66e0f1f4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 06:48:37 2026 -0700

    Check Sample In: 'Received' button stamps Date WP Sample Sent=today (Steve's receive-stamp field) via POST /api/mark-received (fmSet), drops the card from the incoming queue; scan-match surfaces a one-tap 'Check in — received today'. Input-guarded (400 on missing recordId/bad json), no live write on my tests
---
 public/index.html | 20 ++++++++++++++++----
 server.js         | 21 +++++++++++++++++++++
 2 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index 939fe32..cfb838c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -158,6 +158,8 @@
   .inc-thumb{width:104px;height:82px;background:#000 center/cover no-repeat;display:flex;align-items:center;justify-content:center;font-size:22px;color:#5a544a}
   .inc-meta{padding:5px 6px}.inc-pat{font-size:11px;color:#e7ddc7;line-height:1.25;max-height:28px;overflow:hidden}
   .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}
   .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}
@@ -1487,12 +1489,22 @@ async function loadIncoming(){ const box=$('#fbIncoming'); if(!box) return; box.
   _incoming=(r&&r.samples)||[];
   if(!_incoming.length){ box.innerHTML=r&&r.ok?'<div class="inc-hd">No samples currently marked ordered-not-received.</div>':''; return; }
   box.innerHTML='<div class="inc-hd"><b>📦 '+_incoming.length+' coming in</b> — ordered, not yet received. Scan an arrival to check it in.</div>'
-    +'<div class="inc-strip">'+_incoming.map((s,i)=>`<div class="inc-card" data-i="${i}" data-sku="${(s.dw_sku||'').toUpperCase()}" data-mfr="${(s.mfr_sku||s.mfr_pattern||'').toUpperCase().replace(/[^A-Z0-9]/g,'')}"><div class="inc-thumb"${s.image?` style="background-image:url('${s.image}')"`:''}>${s.image?'':'📦'}</div><div class="inc-meta"><div class="inc-pat">${(s.pattern||s.mfr_pattern||s.combo_sku||'—')}</div><div class="inc-sub">${[s.vendor,s.vendor_ordered].filter(Boolean).join(' · ')}</div></div></div>`).join('')+'</div>';
+    +'<div class="inc-strip">'+_incoming.map((s,i)=>`<div class="inc-card" data-i="${i}" data-rid="${s.recordId||''}" data-sku="${(s.dw_sku||'').toUpperCase()}" data-mfr="${(s.mfr_sku||s.mfr_pattern||'').toUpperCase().replace(/[^A-Z0-9]/g,'')}"><div class="inc-thumb"${s.image?` style="background-image:url('${s.image}')"`:''}>${s.image?'':'📦'}</div><div class="inc-meta"><div class="inc-pat">${(s.pattern||s.mfr_pattern||s.combo_sku||'—')}</div><div class="inc-sub">${[s.vendor,s.vendor_ordered].filter(Boolean).join(' · ')}</div><button class="inc-recv" onclick="event.stopPropagation();markReceived('${s.recordId||''}',this)">✓ Received</button></div></div>`).join('')+'</div>';
   box.querySelectorAll('.inc-card').forEach(c=>c.addEventListener('click',()=>{ const s=_incoming[+c.dataset.i]; $('#fbNote').innerHTML=`📦 <b>${s.pattern||s.mfr_pattern||s.combo_sku}</b> — ${[s.vendor,s.vendor_ordered&&('ordered '+s.vendor_ordered),s.client&&('for '+s.client)].filter(Boolean).join(' · ')}`; }));
 }
-function matchIncoming(p){ const pk=(p.dw_sku||'').toUpperCase(), pm=(p.mfr_sku||p.mfr_code||'').toUpperCase().replace(/[^A-Z0-9]/g,''); let hit=false;
-  document.querySelectorAll('#fbIncoming .inc-card').forEach(c=>{ const m=(pk&&c.dataset.sku===pk)||(pm&&c.dataset.mfr&&(c.dataset.mfr===pm||c.dataset.mfr.startsWith(pm))); c.classList.toggle('hit',!!m); if(m){ hit=true; c.scrollIntoView({inline:'center',block:'nearest',behavior:'smooth'}); } });
-  if(hit) $('#fbNote').innerHTML+=' · <b style="color:#2bd06a">✓ this is an incoming sample</b>'; }
+// Check a scanned/arrived sample IN → stamps Date WP Sample Sent=today in FileMaker, which also removes
+// it from the incoming queue. `el` is the ✓ button (card-level) or the scan-match button (in fbNote).
+async function markReceived(rid,el){ if(!rid){ return; } const card=el&&el.closest?el.closest('.inc-card'):null;
+  if(el){ el.disabled=true; el.textContent='…'; }
+  let r; try{ r=await(await fetch('/api/mark-received',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({recordId:rid})})).json(); }catch(e){ r={ok:false,err:'network'}; }
+  if(r&&r.ok){ if(card){ card.style.transition='opacity .3s'; card.style.opacity='0'; setTimeout(()=>card.remove(),300); } _incoming=_incoming.filter(s=>String(s.recordId)!==String(rid));
+    const hd=document.querySelector('#fbIncoming .inc-hd'); if(hd) hd.innerHTML='<b>📦 '+_incoming.length+' coming in</b> — ordered, not yet received. Scan an arrival to check it in.';
+    $('#fbNote').innerHTML=`✅ <b style="color:#2bd06a">Checked in — received ${r.received}</b>`;
+  } else if(el){ el.disabled=false; el.textContent='✓ Received'; $('#fbNote').innerHTML=`⚠️ Couldn't check in: ${(r&&r.err)||'error'}`; } }
+function matchIncoming(p){ const pk=(p.dw_sku||'').toUpperCase(), pm=(p.mfr_sku||p.mfr_code||'').toUpperCase().replace(/[^A-Z0-9]/g,''); let hit=null;
+  document.querySelectorAll('#fbIncoming .inc-card').forEach(c=>{ const m=(pk&&c.dataset.sku===pk)||(pm&&c.dataset.mfr&&(c.dataset.mfr===pm||c.dataset.mfr.startsWith(pm))); c.classList.toggle('hit',!!m); if(m){ hit=c; c.scrollIntoView({inline:'center',block:'nearest',behavior:'smooth'}); } });
+  if(hit){ const rid=hit.dataset.rid||''; $('#fbNote').innerHTML+=' · <b style="color:#2bd06a">✓ this is an incoming sample</b>'
+    +(rid?` <button class="inc-recv" style="margin-left:8px" onclick="markReceived('${rid}',this)">✓ Check in — received today</button>`:''); } }
 function openFbModal(){ _fbBack=_fbFront=_fbQR=null; _fbLookup=false; $('#fbBackThumb').style.backgroundImage=''; $('#fbBackThumb').textContent='📄'; $('#fbFrontThumb').style.backgroundImage=''; $('#fbFrontThumb').textContent='🎨'; $('#fbBackTile').classList.remove('set','need'); $('#fbFrontTile').classList.remove('set','need'); $('#fbGo').disabled=true; $('#fbNote').textContent=''; $('#fbResult').innerHTML=''; $('#fbIncoming').innerHTML='';
   $('#fbModal').querySelector('.samp-pat').textContent='Identify a sample';
   $('#fbModal').querySelector('.samp-meta').textContent='Photograph the BACK label, the FRONT pattern, or both — best ID uses both.';
diff --git a/server.js b/server.js
index f5f63e9..e565847 100644
--- a/server.js
+++ b/server.js
@@ -1361,6 +1361,27 @@ const appHandler = (req, res) => {
       cached_at: _incomingCache.at || null, loading: _incomingCache.at === 0 });
   }
 
+  // Check a physically-arrived sample IN: stamp `Date WP Sample Sent` = today on its record. Per Steve's
+  // workflow this field IS the received-stamp — an empty value means "still coming in" (exactly what the
+  // incoming query filters on), so stamping it records the arrival date AND drops the record out of the
+  // incoming queue automatically.
+  if (u.pathname === '/api/mark-received' && req.method === 'POST') {
+    if (!FM_ENABLED()) return send(res, 200, { ok: false, err: 'FileMaker not configured' });
+    let body = ''; req.on('data', c => { body += c; if (body.length > 64 * 1024) req.destroy(); });
+    req.on('end', async () => {
+      let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { ok: false, err: 'bad json' }); }
+      const recordId = (p.recordId || '').toString().trim();
+      if (!recordId) return send(res, 400, { ok: false, err: 'recordId required' });
+      const today = todayMDY();
+      try { await FM.fmSet(FM_DB, FM_LAYOUT, recordId, { 'Date WP Sample Sent': today }); }
+      catch (e) { return send(res, 200, { ok: false, err: e.message }); }
+      // drop it from the cache immediately so the queue reflects the check-in without waiting for a refresh
+      _incomingCache.samples = _incomingCache.samples.filter(s => s.recordId !== recordId);
+      send(res, 200, { ok: true, recordId, received: today });
+    });
+    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).

← 427db51 Check Sample In: incoming-queue panel — shows vendor-ordered  ·  back to Dw Photo Capture  ·  Incoming queue: best-effort image enrichment — pass 2 in the 62ce2a4 →