[object Object]

← back to Butlr

wizard step 2: DNC pre-check on phone field + directory presets

0dd1395a649b894d82aefb4f0baddf1a212d99cd · 2026-05-13 10:19:46 -0700 · SteveStudio2

views/public/wizard-2-business.ejs:
- Custom-phone input fires /api/dnc-check on blur — shows 🚫 + red
  explainer + disables "Next" button if number is on DNC.
- Selecting a directory preset radio also pre-checks (pulls phone from
  the muted sibling line). Catches the case where Steve's curated
  businesses.json includes a number that's been added to suppression.
- ✓ "not on DNC" or "(dry-run mode)" confirmation on allowed.

Same contract as the /calls quick-dial form (deployed prior). Now
users see DNC status BEFORE submitting either dial path.

Files touched

Diff

commit 0dd1395a649b894d82aefb4f0baddf1a212d99cd
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 10:19:46 2026 -0700

    wizard step 2: DNC pre-check on phone field + directory presets
    
    views/public/wizard-2-business.ejs:
    - Custom-phone input fires /api/dnc-check on blur — shows 🚫 + red
      explainer + disables "Next" button if number is on DNC.
    - Selecting a directory preset radio also pre-checks (pulls phone from
      the muted sibling line). Catches the case where Steve's curated
      businesses.json includes a number that's been added to suppression.
    - ✓ "not on DNC" or "(dry-run mode)" confirmation on allowed.
    
    Same contract as the /calls quick-dial form (deployed prior). Now
    users see DNC status BEFORE submitting either dial path.
---
 views/public/wizard-2-business.ejs | 55 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 53 insertions(+), 2 deletions(-)

diff --git a/views/public/wizard-2-business.ejs b/views/public/wizard-2-business.ejs
index 6dc3bcf..6a2b05f 100644
--- a/views/public/wizard-2-business.ejs
+++ b/views/public/wizard-2-business.ejs
@@ -47,12 +47,13 @@
           </label>
           <label class="field">
             <span class="field-label">Phone number to call</span>
-            <input type="tel" name="business_phone" value="<%= state.business_phone || '' %>" maxlength="20" placeholder="1-800-...">
+            <input type="tel" id="wizard-business-phone" name="business_phone" value="<%= state.business_phone || '' %>" maxlength="20" placeholder="1-800-...">
+            <span id="wizard-dnc-status" class="muted small" style="display:block;font-size:11px;min-height:14px;margin-top:4px;"></span>
           </label>
         </fieldset>
 
         <div class="form-actions">
-          <button type="submit" class="btn primary">Next: what you want done →</button>
+          <button id="wizard-next-btn" type="submit" class="btn primary">Next: what you want done →</button>
           <button type="submit" formmethod="POST" formaction="/new" formnovalidate class="btn ghost">← Back</button>
         </div>
       </form>
@@ -62,3 +63,53 @@
 
 <%- include('../partials/footer') %>
 <script src="/js/wizard.js" defer></script>
+<script>
+(function(){
+  // DNC pre-check on phone field — same contract as /calls quick-dial.
+  // Fires on blur of the custom-phone input OR when the user picks a
+  // directory preset (the radio button has a sibling muted "phone" line).
+  const phoneEl = document.getElementById('wizard-business-phone');
+  const stat = document.getElementById('wizard-dnc-status');
+  const nextBtn = document.getElementById('wizard-next-btn');
+  if (!phoneEl || !stat || !nextBtn) return;
+
+  async function check(num) {
+    if (!num) { stat.textContent = ''; nextBtn.disabled = false; return; }
+    stat.textContent = 'checking DNC…';
+    stat.style.color = 'var(--ink-dim)';
+    try {
+      const r = await fetch('/api/dnc-check?phone=' + encodeURIComponent(num));
+      const j = await r.json();
+      if (j.allowed) {
+        stat.textContent = j.dry_run_bypass ? '✓ ok (dry-run mode)' : '✓ not on DNC';
+        stat.style.color = 'var(--accent,#1a56db)';
+        nextBtn.disabled = false;
+      } else {
+        const msg = j.reason === 'federal_dnc' ? 'on federal DNC — Butlr will not dial this number'
+          : j.reason === 'internal_suppression' ? 'in internal suppression — Butlr will not dial this number'
+          : j.reason === 'gate_error' ? 'DNC list unavailable — fail-closed; cannot dial right now'
+          : 'blocked: ' + (j.reason || 'unknown');
+        stat.textContent = '🚫 ' + msg;
+        stat.style.color = '#991b1b';
+        nextBtn.disabled = true;
+      }
+    } catch (e) {
+      stat.textContent = '(DNC check failed; will re-check on submit)';
+      stat.style.color = 'var(--ink-dim)';
+      nextBtn.disabled = false;
+    }
+  }
+
+  phoneEl.addEventListener('blur', () => check(phoneEl.value.trim()));
+
+  // Also check when a preset is selected — pull the phone from the radio's
+  // sibling muted line and trigger.
+  document.querySelectorAll('input[name="business_slug"]').forEach(r => {
+    r.addEventListener('change', () => {
+      if (r.value === '__custom__') return;  // custom uses the input field
+      const phoneLine = r.parentElement.querySelector('.business-pick-body .muted.small');
+      if (phoneLine) check(phoneLine.textContent.trim());
+    });
+  });
+})();
+</script>

← 73c5692 dnc: pre-check endpoint + /calls quick-dial inline warning  ·  back to Butlr  ·  cost-tracker: log Vapi voice_min + Twilio voice_min + SMS to eb1df5e →