[object Object]

← back to Dw Photo Capture

Generalize spec capture to any spec sheet or sample: read product material (Wallcovering/Fabric/Trim/…), drive Shopify product_type + metafield off it, persist full spec set to new_items_staging.specs jsonb

d548ec82b15af2f7e88fd3d9da3b6454e8383b1d · 2026-07-06 19:48:01 -0700 · Steve Abrams

Files touched

Diff

commit d548ec82b15af2f7e88fd3d9da3b6454e8383b1d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 19:48:01 2026 -0700

    Generalize spec capture to any spec sheet or sample: read product material (Wallcovering/Fabric/Trim/…), drive Shopify product_type + metafield off it, persist full spec set to new_items_staging.specs jsonb
---
 public/index.html |  2 +-
 server.js         | 24 ++++++++++++++++--------
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/public/index.html b/public/index.html
index 8dc8bba..8d1a65e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1449,7 +1449,7 @@ async function addExtract(){ if(!_addPhoto)return; $('#addNote').textContent='
     const f=r.fields||{}; _addExtracted=f; const setIf=(id,v)=>{ if(v&&!$('#'+id).value) $('#'+id).value=v; };
     setIf('addMfr',f.mfr_sku); setIf('addName',f.pattern_name); setIf('addColor',f.color); setIf('addPrice',String(f.price||'').replace(/[^0-9.]/g,''));
     if(r.vendor_matched){ await loadVendors(); const opt=[...$('#addVendor').options].find(o=>o.value.toLowerCase()===r.vendor_matched.toLowerCase()); if(opt){ $('#addVendor').value=opt.value; if(opt.dataset.vid&&!$('#addVid').value)$('#addVid').value=opt.dataset.vid; } }
-    const specs=[['Collection',f.collection],['Width',f.width],['Roll length',f.roll_length],['Repeat',f.repeat],['Match',f.pattern_match],['Substrate',f.substrate],['How sold',f.how_sold],['Price code',f.price_code]].filter(x=>x[1]);
+    const specs=[['Material',f.material],['Collection',f.collection],['Width',f.width],['Roll length',f.roll_length],['Repeat',f.repeat],['Match',f.pattern_match],['Substrate / contents',f.substrate],['How sold',f.how_sold],['Price code',f.price_code]].filter(x=>x[1]);
     $('#addSpecs').innerHTML = specs.length? ('<div class="samp-meta" style="margin:10px 0 2px">Captured specs:</div>'+specs.map(s=>`<div class="add-spec"><span>${s[0]}</span><b>${s[1]}</b></div>`).join('')) : '';
     $('#addNote').textContent='✓ Imported from label'; addPromptMissing();
   }catch(e){ $('#addNote').textContent=''; }
diff --git a/server.js b/server.js
index 7691b4b..fbec075 100644
--- a/server.js
+++ b/server.js
@@ -1104,10 +1104,12 @@ const appHandler = (req, res) => {
       let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { err: 'bad json' }); }
       if (!p.dataUrl) return send(res, 400, { err: 'dataUrl required' });
       const b64 = p.dataUrl.replace(/^data:image\/\w+;base64,/, '');
-      const prompt = 'Read this wallcovering/fabric SAMPLE label and extract EVERY spec you can find. '
-        + 'Reply ONLY as compact JSON with EXACTLY these keys (use "" if a field is not on the label, '
+      const prompt = 'This image is a product SAMPLE label OR a printed SPEC SHEET for an interior product '
+        + '(wallcovering, fabric, trim, drapery, rug, or similar) from ANY manufacturer. Extract EVERY spec you can find. '
+        + 'Reply ONLY as compact JSON with EXACTLY these keys (use "" for any field not present, '
         + 'preserve SKU/model codes and numbers EXACTLY, do not invent): '
         + '{"vendor":"<manufacturer/brand>",'
+        + '"material":"<product type: Wallcovering / Fabric / Trim / Drapery / Rug / etc.>",'
         + '"mfr_sku":"<manufacturer number / item code>",'
         + '"pattern_name":"<pattern or design name>",'
         + '"color":"<colorway / color name>",'
@@ -1645,23 +1647,29 @@ async function createNewItem(p, b64, dryRun) {
     const variants = [{ option1: 'Roll', sku: dwsku, inventory_management: 'shopify', inventory_quantity: 0 },
       { option1: 'Sample', sku: dwsku + '-Sample', price: '4.25', inventory_management: 'shopify', inventory_quantity: 0 }];
     if (price) variants[0].price = price;
+    const material = (p.material || '').trim() || 'Wallcovering';
     const payload = { product: {
-      title, vendor, product_type: 'Wallcovering', status: 'draft', tags: ['new-from-scan', 'display_variant', color ? ('color:' + color) : ''].filter(Boolean).join(', '),
+      title, vendor, product_type: material, status: 'draft', tags: ['new-from-scan', 'display_variant', color ? ('color:' + color) : ''].filter(Boolean).join(', '),
       options: [{ name: 'Size' }], variants,
       images: b64 ? [{ attachment: b64, filename: `${dwsku}.jpg` }] : [],
       metafields: [mf('custom', 'manufacturer_sku', mfr), mf('dwc', 'manufacturer_sku', mfr),
         mf('custom', 'pattern_name', name), mf('custom', 'color', color), mf('global', 'Brand', vendor),
         mf('global', 'Collection', p.collection), mf('global', 'width', p.width), mf('global', 'length', p.roll_length),
         mf('global', 'Pattern-Repeat', p.repeat), mf('global', 'Match', p.pattern_match), mf('global', 'Content', p.substrate),
-        mf('dwc', 'sold_by', p.how_sold), mf('custom', 'price_code', p.price_code)].filter(Boolean)
+        mf('dwc', 'sold_by', p.how_sold), mf('custom', 'price_code', p.price_code), mf('custom', 'material', material)].filter(Boolean)
     } };
     const cr = await shopifyReq('POST', '/products.json', payload);
     if (cr.status < 200 || cr.status >= 300) return { ok: false, err: `Shopify create HTTP ${cr.status}: ${(cr.raw || '').slice(0, 160)}` };
     const pid = cr.body && cr.body.product && cr.body.product.id;
-    // stage into dw_unified (additive table — does NOT touch canonical catalog rows)
-    const stageSQL = `insert into new_items_staging (dw_sku, mfr_sku, vendor, vid, pattern_name, color, price, shopify_product_id, created_via)
-      values ($$${dwsku}$$,$$${mfr}$$,$$${vendor}$$,$$${p.vid || ''}$$,$$${name}$$,$$${color}$$,${price || 'NULL'},${pid || 'NULL'},$$scan$$) on conflict do nothing`;
-    execFile(PSQL, ['-d', DW_DB, '-c', 'create table if not exists new_items_staging (id bigserial primary key, dw_sku text, mfr_sku text, vendor text, vid text, pattern_name text, color text, price numeric, shopify_product_id bigint, created_via text, created_at timestamptz default now()); ' + stageSQL], { timeout: 8000 }, () => {});
+    // stage into dw_unified (additive table — does NOT touch canonical catalog rows).
+    // `specs` jsonb keeps the FULL captured spec set — every field off any spec sheet/sample, nothing dropped.
+    const specsObj = { material, collection: p.collection || '', width: p.width || '', roll_length: p.roll_length || '',
+      repeat: p.repeat || '', pattern_match: p.pattern_match || '', substrate: p.substrate || '',
+      how_sold: p.how_sold || '', price_code: p.price_code || '' };
+    const specsJson = JSON.stringify(specsObj).replace(/\$\$/g, '$');   // keep dollar-quoting safe
+    const stageSQL = `insert into new_items_staging (dw_sku, mfr_sku, vendor, vid, pattern_name, color, price, specs, shopify_product_id, created_via)
+      values ($$${dwsku}$$,$$${mfr}$$,$$${vendor}$$,$$${p.vid || ''}$$,$$${name}$$,$$${color}$$,${price || 'NULL'},$$${specsJson}$$::jsonb,${pid || 'NULL'},$$scan$$) on conflict do nothing`;
+    execFile(PSQL, ['-d', DW_DB, '-c', 'create table if not exists new_items_staging (id bigserial primary key, dw_sku text, mfr_sku text, vendor text, vid text, pattern_name text, color text, price numeric, shopify_product_id bigint, created_via text, created_at timestamptz default now()); alter table new_items_staging add column if not exists specs jsonb; ' + stageSQL], { timeout: 8000 }, () => {});
     if (pid) CATALOG.push({ product_id: pid, title, status: 'DRAFT', dw_sku: dwsku, mfr, price, image: null, done: false });
     return { ok: true, product_id: pid, dw_sku: dwsku, title, status: 'draft', preview };
   } catch (e) { return { ok: false, err: e.message }; }

← 3fcbec5 Capture full wallcovering spec set on scan (repeat/width/rol  ·  back to Dw Photo Capture  ·  FileMaker sticker setup: STEP 1 now requires placing web_pri 2857339 →