← back to Dw Pitch Followup
Flag invoices placed SINCE samples sent (possible conversion): card banner + panel detail with #, total, sku/item, quote/booked, date
4f8a57a13aa8529ef05339b2104d53177a05763c · 2026-07-07 15:53:03 -0700 · Steve Abrams
Files touched
M public/index.htmlM scripts/build-lists.js
Diff
commit 4f8a57a13aa8529ef05339b2104d53177a05763c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 7 15:53:03 2026 -0700
Flag invoices placed SINCE samples sent (possible conversion): card banner + panel detail with #, total, sku/item, quote/booked, date
---
public/index.html | 18 ++++++++++++++++--
scripts/build-lists.js | 16 ++++++++++++++--
2 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/public/index.html b/public/index.html
index a6a1864..ca26ae7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -67,6 +67,8 @@
.chip.sent{color:var(--green);border-color:var(--green)}
.chip.life{color:var(--green);border-color:var(--green);font-weight:600}
.proj{font-size:calc(11px*var(--scale));color:var(--gold)}
+ .postflag{background:#3a2a12;color:var(--amber);border:1px solid var(--amber);border-radius:6px;padding:4px 9px;font-size:calc(11px*var(--scale));font-weight:600;margin:3px 0;line-height:1.4}
+ .postflag a{color:var(--amber)}
.acts{display:flex;gap:7px;margin-top:2px;flex-wrap:wrap}
/* RIGHT PANEL */
@@ -176,6 +178,18 @@ const dateChip=r=>r.when_iso?`<span class="chip" title="${esc(r.when_iso)}">🕓
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;
+// Compact card flag: any invoice placed since we sent samples (possible conversion).
+function postFlag(r){
+ const p=Array.isArray(r.post_sample_invoices)?r.post_sample_invoices:[]; if(!p.length) return '';
+ const detail=p.slice(0,3).map(i=>`#${esc(i.invoice)} ${esc(money(i.total))}`).join(' · ')+(p.length>3?` +${p.length-3} more`:'');
+ return `<div class="postflag">🧾 Ordered since sampling — ${detail}</div>`;
+}
+// Panel: full detail — #, total, sku/item, quote/booked, date.
+function postSampleBlock(r){
+ const p=Array.isArray(r.post_sample_invoices)?r.post_sample_invoices:[]; if(!p.length) return '';
+ const rows=p.map(i=>`<div class="inv"><span class="chip ${i.booked?'g':'a'}">${i.booked?'BOOKED':'QUOTE'}</span> <a href="${fmpInvoice(i.invoice)}">#${esc(i.invoice)}</a> <b>${esc(money(i.total))}</b>${i.item?` · ${esc(i.item)}`:''}${i.date?` · ${esc(fmtDay(i.date))}`:''}</div>`).join('');
+ return `<div class="postflag" style="margin-bottom:12px">🧾 <b>Ordered since we sent samples${r.post_sample_since?` (since ${esc(fmtDay(r.post_sample_since))})`:''}:</b>${rows}</div>`;
+}
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);
@@ -224,7 +238,7 @@ 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}
+ ${facts}${projLine}${postFlag(r)}
<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>
@@ -276,7 +290,7 @@ function merchBlock(r){
return out;
}
function panelContextHTML(list,r){
- let html='';
+ let html=postSampleBlock(r);
if(r.project) html+=`<div class="drow">🏠 <b>Current project:</b> ${esc(lbl(r.project))}</div>`;
if(list!=='list3') html+=merchBlock(r);
html+=samplesBlock(r);
diff --git a/scripts/build-lists.js b/scripts/build-lists.js
index ae8a923..8c51ec7 100644
--- a/scripts/build-lists.js
+++ b/scripts/build-lists.js
@@ -264,10 +264,12 @@ async function enrichMerch(accounts) {
const key = acct + '|' + inv; if (seen.has(key)) continue; seen.add(key);
const booked = !!fmToISO(fd['Date Order Booked']);
const dISO = fmToISO(fd['Date Order Booked'] || fd['Date']);
+ // the item/sku on this order (line-1 detail), for the "ordered since sampling" flag
+ const item = clean(fd['DETAIL 1(1)'] || fd['Detail 1 Mfr Number(1)'] || '').slice(0, 60);
// project/room descriptor for this order (Sidemark first, else the line detail)
const proj = clean(fd['Sidemark 1(1)'] || '') || clean(fd['DETAIL 1(1)'] || '');
const g = map.get(acct) || { invoices: [], lifetime: 0, quoted: 0, project: '', projectISO: '' };
- g.invoices.push({ invoice: inv, date: dISO, total, booked });
+ g.invoices.push({ invoice: inv, date: dISO, total, booked, item });
if (booked) g.lifetime += total; else g.quoted += total;
if (dISO && dISO > (g.projectISO || '') && proj) { g.project = proj; g.projectISO = dISO; }
map.set(acct, g);
@@ -360,7 +362,17 @@ async function main() {
withMerch++;
}
}
- console.log(`[build-lists] enriched: ${withSamples} rows w/ samples, ${withMerch} rows w/ merch orders, ${withName} rows w/ contact first name`);
+ // FLAG any invoice dated on/after the client started sampling — a possible conversion.
+ let withPost = 0;
+ for (const r of [...list1, ...list2]) {
+ const invs = Array.isArray(r.merch_invoices) ? r.merch_invoices : [];
+ const sampleDates = (Array.isArray(r.samples_sent) ? r.samples_sent : []).map((s) => s && s.date).filter(Boolean).sort();
+ const ref = sampleDates[0] || r.when_iso || '';
+ if (!invs.length || !ref) continue;
+ const post = invs.filter((inv) => inv.date && inv.date >= ref).map((inv) => ({ invoice: inv.invoice, total: inv.total, date: inv.date, booked: inv.booked, item: inv.item || '' }));
+ if (post.length) { r.post_sample_invoices = post; r.post_sample_since = ref; withPost++; }
+ }
+ console.log(`[build-lists] enriched: ${withSamples} rows w/ samples, ${withMerch} rows w/ merch orders, ${withName} rows w/ contact first name, ${withPost} rows w/ orders SINCE sampling`);
const payload = {
generatedAt: new Date().toISOString(),
← b6e5737 Fix sticking Send button (reset #pSend/#pRegen/#pCopy on pan
·
back to Dw Pitch Followup
·
Auto-swap to a thank-you letter when client has a BOOKED ord d191c5c →