← 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
M public/index.htmlM scripts/build-lists.jsM server.js
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 →