[object Object]

← back to Pitch Guard

Pitch Guard: auto-remove a draft from the list once its followup sends successfully (trash + drop card)

ca01242b41c3b870c10f8ee347f9baec08d22458 · 2026-06-30 05:18:33 -0700 · Steve

Files touched

Diff

commit ca01242b41c3b870c10f8ee347f9baec08d22458
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 05:18:33 2026 -0700

    Pitch Guard: auto-remove a draft from the list once its followup sends successfully (trash + drop card)
---
 public/index.html | 39 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 36 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 9f4542f..8421e9c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -154,6 +154,11 @@
   .msg.open .m-snip{display:none}
   .sig-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--redbg);color:var(--red);
     border-radius:3px;padding:1px 5px}
+  /* warm-lead signals (invoice/samples WE sent) — these mean "follow up", not "stop" */
+  .warm-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--greenbg);color:var(--green);
+    border-radius:3px;padding:1px 5px}
+  .cand.warm{box-shadow:inset 3px 0 0 var(--green)}
+  .cand .warms{margin-top:4px;display:flex;gap:5px;flex-wrap:wrap}
   /* ── right / pitch ── */
   .pitch-empty{padding:48px 24px;text-align:center;color:var(--muted);font-size:13px}
   .pitch{display:flex;flex-direction:column;height:100%}
@@ -244,6 +249,8 @@
       <select id="sortSel">
         <option value="newest">Newest first</option>
         <option value="oldest">Oldest first</option>
+        <option value="invoice">Invoice sent first</option>
+        <option value="samples">Samples sent first</option>
         <option value="name">Recipient name A→Z</option>
         <option value="subject">Subject A→Z</option>
         <option value="risk">Risk (high → clear)</option>
@@ -492,6 +499,19 @@ function sortDrafts(arr){
   else a.sort((x,y)=>ts(y)-ts(x)); // newest
   return a;
 }
+// Followup candidates carry warm signals (INVOICE SENT / SAMPLES SENT we sent). Default
+// order = warm leads first, then longest-quiet. The 'invoice'/'samples' sort options
+// pull that signal to the very top — the "find who needs a follow" criteria Steve asked for.
+function sortCandidates(arr){
+  const a = arr.slice();
+  const hasInv = c => (c.warmSignals||[]).includes('INVOICE SENT') ? 1 : 0;
+  const hasSam = c => (c.warmSignals||[]).includes('SAMPLES SENT') ? 1 : 0;
+  const warmN  = c => (c.warmSignals||[]).length ? 1 : 0;
+  if(sortMode==='invoice') a.sort((x,y)=> hasInv(y)-hasInv(x) || (y.sentInvoiceTs||0)-(x.sentInvoiceTs||0) || y.daysSince-x.daysSince);
+  else if(sortMode==='samples') a.sort((x,y)=> hasSam(y)-hasSam(x) || (y.sentSamplesTs||0)-(x.sentSamplesTs||0) || y.daysSince-x.daysSince);
+  else a.sort((x,y)=> warmN(y)-warmN(x) || y.daysSince-x.daysSince); // warm-first, then longest-quiet
+  return a;
+}
 function draftChip(d){
   const c = verdictOf(d.toEmail);
   let badge = '<span class="badge r-pending">·</span>';
@@ -509,13 +529,15 @@ function candidateSectionHTML(){
   if(candScanning) body='<div class="empty" style="padding:24px"><div class="spin"></div> scanning sent mail for ghosted quotes…</div>';
   else if(!candScanned) body='<div class="vgroup-note">Not scanned yet — click <b>Scan</b> to find quote/pricing threads we replied to that got no client response in 5+ days and have no followup drafted.</div>';
   else if(!CANDIDATES.length) body='<div class="vgroup-note">No followup candidates — every ghosted quote in range already has a draft.'+(candScannedAt?' <span style="opacity:.7">· auto-scanned '+fmtDate(candScannedAt)+'</span>':'')+'</div>';
-  else body=(candScannedAt?'<div class="vgroup-note">Auto-scans daily · last scan '+fmtDate(candScannedAt)+' — review, then ✎ Draft it.</div>':'')+CANDIDATES.map(c=>
-    '<div class="cand" data-email="'+esc(c.contact)+'">'+
+  else body=(candScannedAt?'<div class="vgroup-note">Auto-scans daily · last scan '+fmtDate(candScannedAt)+' — review, then ✎ Draft it. <b>Green chips</b> = invoice/samples we sent (top follow-up priority).</div>':'')+sortCandidates(CANDIDATES).map(c=>{
+    const warms=(c.warmSignals||[]).map(w=>'<span class="warm-chip">'+esc(w)+'</span>').join(' ');
+    return '<div class="cand'+((c.warmSignals||[]).length?' warm':'')+'" data-email="'+esc(c.contact)+'">'+
       '<button class="cand-draft" data-draft-cand="'+esc(c.contact)+'" title="Create the followup draft">✎ Draft it</button>'+
       '<div class="who">'+esc(c.contactName)+(c.uncertain?' <span title="Reply/draft check was throttled — shown so it is not silently dropped; verify the thread before sending" style="color:#b8860b;font-size:11px">⚠ verify</span>':'')+'</div>'+
       '<div class="subj">'+esc(c.subject)+'</div>'+
       '<div class="meta">'+esc(c.contact)+' · '+c.daysSince+'d no reply</div>'+
-    '</div>').join('');
+      (warms?'<div class="warms">'+warms+'</div>':'')+
+    '</div>';}).join('');
   return '<div class="cgroup-hd'+(candOpen?'':' closed')+'" id="cgroupHd" title="Quotes with no client reply in 5+ days — the 5-day followup rule">'+
       '<span class="arrow">▼</span> Followup Candidates '+
       '<span class="vct">'+(candScanned?CANDIDATES.length:'—')+'</span>'+
@@ -856,6 +878,17 @@ async function sendPitch(d){
     if(out.error) throw new Error(out.error);
     $('#sendState').innerHTML='<span class="sent-stamp">✓ sent in-thread</span>';
     toast('Followup sent to '+d.toEmail);
+    // Process finished successfully → auto-remove this draft (Steve 2026-06-30). The followup
+    // was sent, so the leftover Gmail draft is done: trash it (reversible → Gmail Trash, 30-day
+    // recovery) and drop it from the list immediately so it can't linger or be re-sent.
+    try{ const tr=await fetch('/api/draft/'+encodeURIComponent(d.id)+'/trash',{method:'POST'}); await tr.json(); }catch(_){}
+    DRAFTS = DRAFTS.filter(x=>x.id!==d.id);
+    if(CURRENT===d.id){
+      CURRENT=null;
+      const corr=$('#corr'); if(corr) corr.innerHTML='<div class="empty"><div class="big">✉</div>Pick a draft to see every thread<br>with that contact before you decide.</div>';
+      const pit=$('#pitch'); if(pit) pit.innerHTML='<div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>';
+    }
+    renderDrafts();
   }catch(e){ $('#sendState').textContent=''; sb.disabled=false; toast('Send failed: '+e.message,1); }
 }
 

← bb14d71 snapshot before invoice/samples-sent followup-signal feature  ·  back to Pitch Guard  ·  feature: surface invoice-sent & samples-sent as warm followu aae4190 →