[object Object]

← back to Dw Pitch Followup

auto-save: 2026-07-07T14:31:09 (2 files) — public/index.html scripts/build-lists.js

28a382be343a0e6869f7a37c54fcf67f493299df · 2026-07-07 14:31:11 -0700 · Steve Abrams

Files touched

Diff

commit 28a382be343a0e6869f7a37c54fcf67f493299df
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 14:31:11 2026 -0700

    auto-save: 2026-07-07T14:31:09 (2 files) — public/index.html scripts/build-lists.js
---
 public/index.html      |  2 +-
 scripts/build-lists.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 36bfe04..cd3f7a0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -189,7 +189,7 @@ function samplesBlock(r){
   const li = (arr,cls,suffix)=>arr.slice(0,12).map(s=>{
     const name = typeof s==='string'?s:(s.label||s.sku||'sample');
     const when = (s&&s.date)?` · ${esc(fmtDay(s.date))}`:'';
-    return `<li><span class="dot ${cls}"></span>${esc(lbl(name))}${suffix?when:''}</li>`;
+    return `<li><span class="dot ${cls}"></span>${esc(name)}${suffix?when:''}</li>`;
   }).join('');
   let out='';
   if(sent.length) out+=`<div class="drow"><b>Samples sent (${sent.length})</b><ul class="slist">${li(sent,'ok',true)}</ul></div>`;
diff --git a/scripts/build-lists.js b/scripts/build-lists.js
index fb8bb8d..c8310d6 100644
--- a/scripts/build-lists.js
+++ b/scripts/build-lists.js
@@ -10,7 +10,7 @@
 import fs from 'node:fs';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
-import { findAll, findProjectsByAccounts, fmDate, daysAgo } from '../lib/fm.js';
+import { findAll, findProjectsByAccounts, findRecords, fmDate, daysAgo } from '../lib/fm.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const DATA_DIR = path.join(__dirname, '..', 'data');
@@ -23,6 +23,9 @@ const FIRM_ORDER_MIN = Number(process.env.FIRM_ORDER_MIN || 50);          // Lis
 const OVERDUE_DAYS = Number(process.env.OVERDUE_DAYS || 8);               // List 3: requested > N days ago
 const OVERDUE_MAX_DAYS = Number(process.env.OVERDUE_MAX_DAYS || 180);     // List 3: ignore ghost memos older than this
 const CAP = Number(process.env.LIST_CAP || 600);
+// Merchandise vs sample invoices: real orders are generally > $250; sample invoices run
+// ~$4.25–$35 (Steve 2026-07-07). Anything at/below this is treated as a sample, not merch.
+const MERCH_MIN = Number(process.env.MERCH_MIN || 250);
 
 // --- generic helpers ------------------------------------------------
 // Pick the first populated value whose key matches any of the regexes.
@@ -202,14 +205,91 @@ async function buildList3() {
   return out.slice(0, CAP);
 }
 
+// ====================================================================
+// ENRICHMENT — attach to list1/list2 rows: samples sent/pending + merch invoices
+// ====================================================================
+// Bulk OR-find by account, in chunks (Data API caps a single find's page size).
+async function findByAccounts(db, layout, field, accounts, { chunk = 30, per = 60 } = {}) {
+  const out = [];
+  const uniq = [...new Set(accounts.filter(Boolean).map(String))];
+  for (let i = 0; i < uniq.length; i += chunk) {
+    const slice = uniq.slice(i, i + chunk);
+    const query = slice.map((a) => ({ [field]: `==${a}` }));
+    try {
+      const r = await findRecords(db, layout, query, { limit: slice.length * per });
+      out.push(...(r.records || []));
+    } catch (e) { if (String(e.fmCode) !== '401') throw e; } // 401 = no matches
+  }
+  return out;
+}
+
+// Per account → which samples SHIPPED (Date WP Sample Sent present) vs still PENDING.
+async function enrichSamples(accounts) {
+  const recs = await findByAccounts('WALLPAPER', 'REPORT ON SAMPLES ORDERED', 'account', accounts, { chunk: 30, per: 60 });
+  const map = new Map();
+  for (const rec of recs) {
+    const fd = rec.fieldData;
+    const acct = String(fd['account'] || '').trim(); if (!acct) continue;
+    const label = clean(fd['combo sku'] || '') || clean(fd['Mfr Pattern'] || '') || 'sample';
+    const sent = fmToISO(fd['Date WP Sample Sent']);
+    const g = map.get(acct) || { sent: [], pending: [] };
+    if (sent) g.sent.push({ label, date: sent }); else g.pending.push({ label });
+    map.set(acct, g);
+  }
+  for (const g of map.values()) {
+    g.sent.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
+    g.sent = g.sent.slice(0, 20); g.pending = g.pending.slice(0, 20);
+  }
+  return map;
+}
+
+// Per account → MERCHANDISE invoices (real orders > MERCH_MIN; sample invoices excluded).
+async function enrichMerch(accounts) {
+  const recs = await findByAccounts('invoice', 'Order Detail', 'Account #', accounts, { chunk: 25, per: 40 });
+  const map = new Map();
+  const seen = new Set();
+  for (const rec of recs) {
+    const fd = rec.fieldData;
+    const acct = String(fd['Account #'] || '').trim(); if (!acct) continue;
+    const inv = clean(fd['Invoice'] || ''); if (!inv) continue;
+    const total = nval(fd['merch tot2'] || fd['MERCHANDISE TOTAL(1)'] || fd['TOTAL 1(1)'] || 0);
+    if (total <= MERCH_MIN) continue; // real order vs sample invoice
+    const key = acct + '|' + inv; if (seen.has(key)) continue; seen.add(key);
+    const dISO = fmToISO(fd['Date Order Booked'] || fd['Date']);
+    const g = map.get(acct) || { invoices: [], total: 0 };
+    g.invoices.push({ invoice: inv, date: dISO, total });
+    g.total += total;
+    map.set(acct, g);
+  }
+  for (const g of map.values()) {
+    g.invoices.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
+    g.invoices = g.invoices.slice(0, 12);
+  }
+  return map;
+}
+
 // ====================================================================
 async function main() {
   const t0 = Date.now();
   console.log('[build-lists] FileMaker reads (read-only)…');
   const [list1, list2, list3] = [await buildList1(), await buildList2(), await buildList3()];
+
+  // Enrich client lists (1 & 2) with samples sent/pending + merchandise invoices.
+  console.log('[build-lists] enriching samples + merchandise invoices…');
+  const accounts = [...new Set([...list1, ...list2].map((r) => String(r.account)))];
+  const [samplesMap, merchMap] = [await enrichSamples(accounts), await enrichMerch(accounts)];
+  let withSamples = 0, withMerch = 0;
+  for (const r of [...list1, ...list2]) {
+    const s = samplesMap.get(String(r.account));
+    if (s && (s.sent.length || s.pending.length)) { r.samples_sent = s.sent; r.samples_pending = s.pending; withSamples++; }
+    const m = merchMap.get(String(r.account));
+    if (m && m.invoices.length) { r.merch_invoices = m.invoices; r.merch_total = m.total; withMerch++; }
+  }
+  console.log(`[build-lists] enriched: ${withSamples} rows w/ samples, ${withMerch} rows w/ merch orders`);
+
   const payload = {
     generatedAt: new Date().toISOString(),
-    thresholds: { ratio_min: 0.9, lookback_sent_days: LOOKBACK_SENT_DAYS, invoice_since: INVOICE_SINCE, firm_order_min: FIRM_ORDER_MIN, overdue_days: OVERDUE_DAYS, cap: CAP },
+    thresholds: { ratio_min: 0.9, lookback_sent_days: LOOKBACK_SENT_DAYS, invoice_since: INVOICE_SINCE, firm_order_min: FIRM_ORDER_MIN, merch_min: MERCH_MIN, overdue_days: OVERDUE_DAYS, cap: CAP },
     counts: { list1: list1.length, list2: list2.length, list3: list3.length },
     list1, list2, list3,
   };

← c660a28 Compact cards + Columns/One-list layout toggle + ▸details ex  ·  back to Dw Pitch Followup  ·  Enrich cards with samples sent/pending + merchandise invoice 56b2381 →