← 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
M public/pitches.htmlM src/server/index.ts
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 →