[object Object]

← back to Ventura Corridor

iter 70: /pitches.html bulk-set-followup — extends iter 66 multi-select toolbar with ⏰ Schedule follow-up field option; selecting it hides the value-select and reveals a date input + 4 preset buttons (+3d / +1w / +2w / +1mo) that auto-populate the date; bulkApply detects next_followup_at field and uses date input value; /api/pitches/bulk allowlist extended to accept next_followup_at + followup_notes; setFollowupDate(N) helper computes ISO date N days out; tested end-to-end: walk 30 doors → tick visible rows → ⏰ + +2w → all 30 follow-ups scheduled in one click, trigger fires followup_scheduled events for each via iter-51 audit pipeline

d0c4c91a195b1a7e2661ac685e96010a734f5ba8 · 2026-05-06 15:33:21 -0700 · SteveStudio2

Files touched

Diff

commit d0c4c91a195b1a7e2661ac685e96010a734f5ba8
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 15:33:21 2026 -0700

    iter 70: /pitches.html bulk-set-followup — extends iter 66 multi-select toolbar with ⏰ Schedule follow-up field option; selecting it hides the value-select and reveals a date input + 4 preset buttons (+3d / +1w / +2w / +1mo) that auto-populate the date; bulkApply detects next_followup_at field and uses date input value; /api/pitches/bulk allowlist extended to accept next_followup_at + followup_notes; setFollowupDate(N) helper computes ISO date N days out; tested end-to-end: walk 30 doors → tick visible rows → ⏰ + +2w → all 30 follow-ups scheduled in one click, trigger fires followup_scheduled events for each via iter-51 audit pipeline
---
 public/pitches.html | 36 +++++++++++++++++++++++++++++++++---
 src/server/index.ts |  2 +-
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/public/pitches.html b/public/pitches.html
index 8df5386..3eb8d56 100644
--- a/public/pitches.html
+++ b/public/pitches.html
@@ -209,8 +209,16 @@
     <option value="outreach_channel">Set channel</option>
     <option value="priority">Set priority</option>
     <option value="pitch_type">Set pitch_type</option>
+    <option value="next_followup_at">⏰ Schedule follow-up</option>
   </select>
   <select id="mass-value" style="background:var(--noir);border:1px solid var(--rule);color:var(--ink);padding:6px 10px;font-size:11px"></select>
+  <input id="mass-date" type="date" style="background:var(--noir);border:1px solid var(--rule);color:var(--ink);padding:6px 10px;font-size:11px;display:none">
+  <span id="mass-date-presets" style="display:none;gap:4px">
+    <button onclick="setFollowupDate(3)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+3d</button>
+    <button onclick="setFollowupDate(7)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+1w</button>
+    <button onclick="setFollowupDate(14)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+2w</button>
+    <button onclick="setFollowupDate(30)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+1mo</button>
+  </span>
   <button onclick="bulkApply(true)" style="background:transparent;border:1px solid var(--metal);color:var(--metal);padding:6px 12px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer">Preview</button>
   <button onclick="bulkApply(false)" style="background:transparent;border:1px solid var(--metal-glow);color:var(--metal-glow);padding:6px 12px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer">⚡ Apply</button>
 </div>
@@ -436,7 +444,23 @@ const MASS_OPTIONS = {
 function refreshMassValueOptions() {
   const f = document.getElementById('mass-field').value;
   const v = document.getElementById('mass-value');
-  v.innerHTML = MASS_OPTIONS[f].map(o => `<option value="${o}">${o}</option>`).join('');
+  const d = document.getElementById('mass-date');
+  const p = document.getElementById('mass-date-presets');
+  if (f === 'next_followup_at') {
+    v.style.display = 'none';
+    d.style.display = 'inline-block';
+    p.style.display = 'inline-flex';
+    if (!d.value) setFollowupDate(7);
+  } else {
+    v.style.display = 'inline-block';
+    d.style.display = 'none';
+    p.style.display = 'none';
+    v.innerHTML = (MASS_OPTIONS[f] || []).map(o => `<option value="${o}">${o}</option>`).join('');
+  }
+}
+function setFollowupDate(daysFromNow) {
+  const dt = new Date(Date.now() + daysFromNow * 86400000);
+  document.getElementById('mass-date').value = dt.toISOString().slice(0, 10);
 }
 document.getElementById('mass-field').addEventListener('change', refreshMassValueOptions);
 
@@ -489,8 +513,14 @@ async function bulkApply(dryRun) {
   }
   if (!ids.length) { alert('No rows selected and none visible.'); return; }
   const field = document.getElementById('mass-field').value;
-  let value = document.getElementById('mass-value').value;
-  if (field === 'priority') value = parseInt(value, 10);
+  let value;
+  if (field === 'next_followup_at') {
+    value = document.getElementById('mass-date').value;
+    if (!value) { alert('Pick a follow-up date first (or use a preset).'); return; }
+  } else {
+    value = document.getElementById('mass-value').value;
+    if (field === 'priority') value = parseInt(value, 10);
+  }
   if (dryRun) {
     alert(`Preview: would set ${field}=${value} on ${ids.length} ${scope} pitch${ids.length === 1 ? '' : 'es'}. No changes made.`);
     return;
diff --git a/src/server/index.ts b/src/server/index.ts
index ab47feb..72e38a0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1085,7 +1085,7 @@ app.post('/api/pitches/bulk', express.json(), async (req, res) => {
     if (!ids.length) return res.status(400).json({ error: 'no ids' });
     if (ids.length > 2000) return res.status(400).json({ error: 'max 2000 ids per call' });
 
-    const allowed = ['status','outreach_channel','priority','notes','pitch_type'];
+    const allowed = ['status','outreach_channel','priority','notes','pitch_type','next_followup_at','followup_notes'];
     const fields: Record<string, any> = {};
     for (const k of allowed) if (k in (req.body?.fields || {})) fields[k] = req.body.fields[k];
     if (!Object.keys(fields).length) return res.status(400).json({ error: 'no fields' });

← 140c1a6 iter 69: BTRC data freshness pill on /today.html — /api/data  ·  back to Ventura Corridor  ·  iter 71: stale-pitch alerts on /today.html — /api/stale-pitc 1482b8f →