[object Object]

← back to Dw Pitch Followup

Letter greeting uses FileMaker contact first name (Find-a-client Name field); block letter + red 'Reorder Samples??' when samples ordered but 0 shipped (panel Generate disabled too)

eb905c70c7bdc6518fab65535013034321bb9434 · 2026-07-07 15:20:07 -0700 · Steve Abrams

Files touched

Diff

commit eb905c70c7bdc6518fab65535013034321bb9434
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 15:20:07 2026 -0700

    Letter greeting uses FileMaker contact first name (Find-a-client Name field); block letter + red 'Reorder Samples??' when samples ordered but 0 shipped (panel Generate disabled too)
---
 public/index.html      | 13 +++++++++++--
 scripts/build-lists.js | 25 ++++++++++++++++++++++---
 server.js              |  4 +++-
 3 files changed, 36 insertions(+), 6 deletions(-)

diff --git a/public/index.html b/public/index.html
index 9fd05fd..0f70958 100644
--- a/public/index.html
+++ b/public/index.html
@@ -25,6 +25,8 @@
   .btn.gold{background:var(--gold);color:#1a1611;border-color:var(--gold);font-weight:600}
   .btn.send{background:#33280f;color:var(--gold);border-color:var(--gold)}
   .btn.send:hover{background:var(--gold);color:#1a1611}
+  .btn.reorder{background:#3a1512;color:var(--red);border-color:var(--red);font-weight:700}
+  .reorder-note{color:var(--red);font-weight:600;margin:6px 0}
   .spacer{flex:1}
   .selcount{color:var(--gold);font-size:13px}
   .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
@@ -167,6 +169,8 @@ const fmtWhen=iso=>{if(!iso)return'';const d=new Date(iso);return isNaN(d)?'':d.
 const fmtDay=iso=>{if(!iso)return'';const d=new Date(iso);return isNaN(d)?'':d.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})};
 const dateChip=r=>r.when_iso?`<span class="chip" title="${esc(r.when_iso)}">🕓 ${esc(r.when_label||'when')}: ${esc(fmtDay(r.when_iso))}</span>`:'';
 const money=n=>'$'+(Number(n)||0).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
+// Nothing shipped yet: samples are pending but ZERO have been sent → block the letter, prompt a reorder.
+const needsReorder=r=>Array.isArray(r.samples_pending)&&r.samples_pending.length>0&&Array.isArray(r.samples_sent)&&r.samples_sent.length===0;
 function stripMfr(s){return String(s||'').replace(/[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*/g,t=>/\d/.test(t)?'':t).replace(/\s*[|·•—–-]+\s*/g,' · ').replace(/(?:^[\s·]+|[\s·]+$)/g,'').replace(/(?:·\s*){2,}/g,'· ').replace(/\bSample\b\s*$/i,'').replace(/\s{2,}/g,' ').trim().replace(/[·\s]+$/,'')}
 function showMfr(){return !!document.getElementById('showMfr')?.checked}
 const lbl=s=>showMfr()?String(s||'').trim():stripMfr(s);
@@ -216,7 +220,9 @@ function cardHTML(list,r){
   return `<div class="card ${sel?'sel':''}" data-k="${esc(k)}" data-list="${list}">
     <div class="top"><input type="checkbox" ${sel?'checked':''} data-sel/>${head}</div>
     ${facts}${projLine}
-    <div class="acts"><button class="btn gold" data-gen>Generate letter</button><button class="btn" data-view>Details ▸</button></div>
+    <div class="acts">${needsReorder(r)
+      ? `<button class="btn reorder" data-view>Reorder Samples??</button>`
+      : `<button class="btn gold" data-gen>Generate letter</button><button class="btn" data-view>Details ▸</button>`}</div>
   </div>`;
 }
 
@@ -280,9 +286,11 @@ function openPanelFor(card,{generate=false}={}){
   // reset letter area
   document.getElementById('pSubj').textContent=''; document.getElementById('pNote').textContent=''; document.getElementById('pNote').className='ctxnote';
   document.getElementById('pBody').value=''; document.getElementById('pVariant').textContent=''; document.getElementById('pSendStat').textContent='';
+  const reorder=needsReorder(r); const pGen=document.getElementById('pGen'); pGen.disabled=reorder;
+  if(reorder){ document.getElementById('pSubj').innerHTML='<span class="reorder-note">Reorder Samples?? — nothing has shipped yet, so there’s nothing to follow up on.</span>'; }
   openPanel();
   if(list!=='list3') loadLastCorr(r);
-  if(generate) composeLetter({});
+  if(generate && !reorder) composeLetter({});
 }
 
 async function loadLastCorr(r){
@@ -302,6 +310,7 @@ function paintLastCorr(wrap,span,val){
 
 async function composeLetter(opts={}){
   if(!active)return; const {row:r,list}=active;
+  if(needsReorder(r)){ document.getElementById('pSubj').innerHTML='<span class="reorder-note">Reorder Samples?? — nothing shipped yet.</span>'; return; }
   const subj=document.getElementById('pSubj'),note=document.getElementById('pNote'),ta=document.getElementById('pBody'),vlabel=document.getElementById('pVariant'),rbtn=document.getElementById('pRegen');
   const regen=!!opts.regenerate; const mfr=showMfr()?1:0;
   const seed=regen?active.seed+1:active.seed; active.seed=seed;
diff --git a/scripts/build-lists.js b/scripts/build-lists.js
index 1363a53..646a64b 100644
--- a/scripts/build-lists.js
+++ b/scripts/build-lists.js
@@ -277,6 +277,23 @@ async function enrichMerch(accounts) {
   return map;
 }
 
+// Per account → the client contact's first name (from Clients "Find a client" — a lean,
+// portal-free layout with a real Name field, e.g. "Debbie Morris"). Used for the greeting.
+async function enrichNames(accounts) {
+  const recs = await findByAccounts('Clients', 'Find a client', 'Account Number', accounts, { chunk: 40, per: 3 });
+  const map = new Map();
+  for (const rec of recs) {
+    const fd = rec.fieldData;
+    const acct = String(fd['Account Number'] || '').trim(); if (!acct || map.has(acct)) continue;
+    let nm = clean(fd['Name'] || '');
+    if (!nm) continue;
+    let first = nm.split(/[\s,]+/).find((w) => w && !/^\d+$/.test(w)) || '';
+    if (first && first === first.toUpperCase() && first.length > 1) first = first[0] + first.slice(1).toLowerCase();
+    if (first) map.set(acct, first);
+  }
+  return map;
+}
+
 // ====================================================================
 async function main() {
   const t0 = Date.now();
@@ -286,9 +303,11 @@ async function main() {
   // 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;
+  const [samplesMap, merchMap, namesMap] = [await enrichSamples(accounts), await enrichMerch(accounts), await enrichNames(accounts)];
+  let withSamples = 0, withMerch = 0, withName = 0;
   for (const r of [...list1, ...list2]) {
+    const fn = namesMap.get(String(r.account));
+    if (fn) { r.first_name = fn; withName++; }
     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));
@@ -300,7 +319,7 @@ async function main() {
       withMerch++;
     }
   }
-  console.log(`[build-lists] enriched: ${withSamples} rows w/ samples, ${withMerch} rows w/ merch orders`);
+  console.log(`[build-lists] enriched: ${withSamples} rows w/ samples, ${withMerch} rows w/ merch orders, ${withName} rows w/ contact first name`);
 
   const payload = {
     generatedAt: new Date().toISOString(),
diff --git a/server.js b/server.js
index ae40beb..f3dfdf2 100644
--- a/server.js
+++ b/server.js
@@ -224,6 +224,8 @@ function composeDeterministic(list, row, ctx, seed, showMfr) {
   // Identifies client type; rotates the 3-name sign-off; lists the actual samples w/ ship
   // dates; asks for project details before a closing question; always ends the ask with a
   // question; adds a PS. designerwallcoverings.com + phone are linkified when SENT (HTML).
+  // Prefer the real contact first name from FileMaker's Name field; fall back to parsing company.
+  const greetName = (row.first_name && String(row.first_name).trim()) ? String(row.first_name).trim() : name;
   const type = String(row.client_type || '').toLowerCase();
   const isTrade = /interior designer|architect|designer/.test(type);
 
@@ -264,7 +266,7 @@ function composeDeterministic(list, row, ctx, seed, showMfr) {
   // ~3 tight sentences: (1) follow-up + final-purchase ask, (2) witty samples/stock + we're-here-to-help
   // [+trade], (3) project-details line then a closing question. Then the sample reminder + sign-off + PS.
   const body =
-    `Hi ${name},\n\n` +
+    `Hi ${greetName},\n\n` +
     `${g} Just checking in on the samples we sent${projRef} — any favorites so far, and when are you thinking of making your final purchase? ${wit} We’re here to help however we can.${tradeLine}\n\n` +
     `If a sample has been specified for a project, please let us know the Project Name and city/state as well as who will be purchasing the item. ${q}\n\n` +
     (sampleItems.length ? `Here’s a quick reminder of your recent samples…\n${sampleItems.slice(0, 12).join('\n')}\n\n` : '') +

← 2246a8e Rewrite client letter to spec: ~3 sentences, client-type awa  ·  back to Dw Pitch Followup  ·  Email: thumbnail + product-link gallery for recent samples ( b2b886f →