← back to Butlr

views/public/wizard-2-business.ejs

246 lines

<%- include('../partials/head', { title, meta_desc }) %>
<%- include('../partials/header') %>

<main class="wizard">
  <section class="section">
    <div class="wrap narrow">
      <p class="muted small"><a href="/new">← Back</a></p>
      <%- include('../partials/wizard-progress', { step }) %>

      <h1>Who do we call?</h1>
      <p class="muted">Search by name (after you share location or enter ZIP), pick from the directory, or scroll down to enter manually.</p>

      <!-- Location-gated business search (OpenStreetMap Nominatim) -->
      <section style="padding:16px; border:1px solid var(--line); border-radius:10px; background:var(--panel); margin-bottom:20px;">
        <div id="loc-gate">
          <strong>📍 Location required for search</strong>
          <p class="muted small" style="margin:6px 0 12px;">Searching globally for "AAA" returns junk. Share location OR enter a US ZIP to bias to your area (25 km radius).</p>
          <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
            <button type="button" id="loc-btn" style="padding:8px 16px;">📍 Use my location</button>
            <span class="muted small">or</span>
            <input id="zip-input" inputmode="numeric" maxlength="5" placeholder="ZIP" style="padding:8px 12px; width:80px; border:1px solid var(--line); border-radius:6px;" />
            <button type="button" id="zip-btn" style="padding:8px 16px;">Use ZIP</button>
            <span id="loc-status" class="muted small"></span>
          </div>
        </div>

        <div id="search-active" style="display:none;">
          <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
            <strong>🔎 Search businesses</strong>
            <a href="#" id="loc-change" class="muted small">change location</a>
          </div>
          <input id="search-input" placeholder="Start typing a business name..." style="width:100%; box-sizing:border-box; padding:10px 14px; border:1px solid var(--line); border-radius:6px; font-size:16px;" />
          <div id="search-results" style="display:flex; flex-direction:column; gap:6px; margin-top:10px;"></div>
        </div>
      </section>

      <% if (typeof errors !== 'undefined' && errors && errors.length) { %>
        <div class="form-errors"><strong>Fix:</strong><ul><% errors.forEach(function(e) { %><li><%= e %></li><% }); %></ul></div>
      <% } %>

      <form method="POST" action="/new/details" class="wizard-form" autocomplete="off">
        <%- include('../partials/wizard-state', { state, skip: ['business_slug', 'business_name', 'business_phone'] }) %>

        <% if (businesses && businesses.length) { %>
          <fieldset>
            <legend>Pick from the directory <span class="muted small">(<%= businesses.length %> preset business<%= businesses.length === 1 ? '' : 'es' %> for <%= preset && preset.label %>)</span></legend>
            <div class="business-pick">
              <% businesses.forEach(function(b) { var selected = state.business_slug === b.slug; %>
                <label class="business-pick-card <%= selected ? 'selected' : '' %>">
                  <input type="radio" name="business_slug" value="<%= b.slug %>" <%= selected ? 'checked' : '' %>>
                  <div class="business-pick-body">
                    <strong><%= b.name %></strong>
                    <span class="muted small"><%= b.phone %></span>
                    <% if (b.ivr_notes) { %><span class="muted small business-pick-ivr">IVR: <%= b.ivr_notes %></span><% } %>
                  </div>
                </label>
              <% }); %>
            </div>
          </fieldset>
        <% } %>

        <fieldset>
          <legend>Or enter a custom business</legend>
          <label class="business-pick-card business-pick-custom <%= state.business_slug === '__custom__' ? 'selected' : '' %>">
            <input type="radio" name="business_slug" value="__custom__" <%= state.business_slug === '__custom__' ? 'checked' : '' %>>
            <span>Custom — fill in below</span>
          </label>
          <label class="field">
            <span class="field-label">Business name</span>
            <input type="text" name="business_name" value="<%= state.business_name || '' %>" maxlength="120" placeholder="e.g. My local clinic billing dept">
          </label>
          <label class="field">
            <span class="field-label">Phone number to call</span>
            <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 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>
    </div>
  </section>
</main>

<%- 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());
    });
  });
})();

// ── Location-gated places search (Nominatim) ────────────────────────
(function() {
  const locGate    = document.getElementById('loc-gate');
  const searchBox  = document.getElementById('search-active');
  const locStatus  = document.getElementById('loc-status');
  const locBtn     = document.getElementById('loc-btn');
  const zipBtn     = document.getElementById('zip-btn');
  const zipInput   = document.getElementById('zip-input');
  const locChange  = document.getElementById('loc-change');
  const qInput     = document.getElementById('search-input');
  const results    = document.getElementById('search-results');
  let location = null;  // {lat,lng} or {zip}

  // Restore prior location from localStorage
  try {
    const saved = JSON.parse(localStorage.getItem('butlr.search.location') || 'null');
    if (saved && (saved.zip || (saved.lat && saved.lng))) {
      location = saved;
      showSearchBox();
    }
  } catch {}

  function setLoc(loc) {
    location = loc;
    try { localStorage.setItem('butlr.search.location', JSON.stringify(loc)); } catch {}
    showSearchBox();
  }
  function showSearchBox() {
    locGate.style.display = 'none';
    searchBox.style.display = '';
    qInput.focus();
  }
  function showGate() {
    locGate.style.display = '';
    searchBox.style.display = 'none';
    location = null;
    try { localStorage.removeItem('butlr.search.location'); } catch {}
  }

  locBtn.addEventListener('click', () => {
    if (!navigator.geolocation) { locStatus.textContent = 'geolocation not supported'; locStatus.style.color = '#d33'; return; }
    locStatus.textContent = 'requesting…';
    navigator.geolocation.getCurrentPosition(
      pos => { setLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); },
      err => { locStatus.textContent = '✗ ' + err.message; locStatus.style.color = '#d33'; },
      { enableHighAccuracy: false, timeout: 8000 }
    );
  });
  zipBtn.addEventListener('click', () => {
    const z = (zipInput.value || '').trim();
    if (!/^\d{5}$/.test(z)) { locStatus.textContent = 'enter a 5-digit ZIP'; locStatus.style.color = '#d33'; return; }
    setLoc({ zip: z });
  });
  if (locChange) locChange.addEventListener('click', e => { e.preventDefault(); showGate(); });

  let timer = null;
  qInput.addEventListener('input', () => {
    clearTimeout(timer);
    timer = setTimeout(() => doSearch(qInput.value.trim()), 350);
  });

  async function doSearch(q) {
    if (!q || q.length < 2) { results.innerHTML = ''; return; }
    if (!location) return;
    results.innerHTML = '<p class="muted small">searching…</p>';
    const params = new URLSearchParams({ q });
    if (location.zip) params.set('zip', location.zip);
    if (location.lat) { params.set('lat', location.lat); params.set('lng', location.lng); }
    try {
      const r = await fetch('/api/places/search?' + params.toString());
      const j = await r.json();
      if (!j.ok) { results.innerHTML = '<p class="muted small" style="color:#d33;">' + (j.message || j.error) + '</p>'; return; }
      if (!j.results.length) { results.innerHTML = '<p class="muted small">no nearby matches</p>'; return; }
      results.innerHTML = j.results.map((b, i) => `
        <div class="place-result" data-i="${i}" style="padding:10px 12px; border:1px solid var(--line); border-radius:6px; cursor:pointer; background:var(--panel-2);">
          <strong>${escapeHtml(b.name)}</strong>
          ${b.phone ? `<span class="muted small" style="margin-left:8px;">📞 ${escapeHtml(b.phone)}</span>` : '<span class="muted small" style="margin-left:8px; color:#d33;">no phone on file</span>'}
          <div class="muted small">${escapeHtml(b.city_state || b.address.split(',').slice(0,3).join(','))}</div>
        </div>
      `).join('');
      results.querySelectorAll('.place-result').forEach((el, idx) => {
        el.addEventListener('click', () => pickResult(j.results[idx]));
      });
    } catch (e) {
      results.innerHTML = '<p class="muted small" style="color:#d33;">' + e.message + '</p>';
    }
  }

  function pickResult(b) {
    // Set the "custom" radio + fill name/phone fields if present.
    const customRadio = document.querySelector('input[name="business_slug"][value="__custom__"]');
    if (customRadio) { customRadio.checked = true; customRadio.dispatchEvent(new Event('change', { bubbles: true })); }
    const nameI = document.querySelector('input[name="business_name"]');
    const phoneI = document.querySelector('input[name="business_phone"]');
    if (nameI) nameI.value = b.name;
    if (phoneI && b.phone) phoneI.value = b.phone;
    if (!b.phone && phoneI) { phoneI.focus(); phoneI.placeholder = 'no phone on file — enter manually'; }
    // Scroll to the custom-entry block
    if (phoneI) phoneI.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }

  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
  }
})();
</script>