← back to Butlr

views/admin/contacts.ejs

111 lines

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

<main class="container" style="max-width: 900px; padding: 24px;">

  <p style="margin-bottom: 8px;"><a href="/admin">← Admin</a></p>
  <h1 style="margin: 0 0 6px;">Contacts</h1>
  <p class="muted" style="margin: 0 0 16px;">Lazy-search your Mac Contacts · click any to set goal + dial · <span id="svc-status">checking service…</span></p>

  <input id="q" autofocus placeholder="Search contacts by name (e.g. 'Sam')" style="width:100%; box-sizing:border-box; padding:12px 16px; font-size:16px; border:1px solid var(--line); border-radius:8px; margin-bottom:16px;"/>

  <div id="results" style="display:flex; flex-direction:column; gap:8px;"></div>

  <!-- Dial modal -->
  <div id="dial-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center;">
    <div style="background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:24px; max-width:520px; width:90%;">
      <h3 style="margin:0 0 12px;" id="dial-title">Call</h3>
      <p class="muted" style="margin:0 0 16px;" id="dial-subtitle"></p>
      <label style="display:block; margin-bottom:12px;">
        <span class="muted" style="font-size:12px;">Goal for this call</span>
        <textarea id="dial-goal" rows="3" placeholder="e.g. Quick hello to check in" style="width:100%; box-sizing:border-box; padding:10px; border:1px solid var(--line); border-radius:6px; margin-top:4px;"></textarea>
      </label>
      <div style="display:flex; gap:8px; justify-content:flex-end;">
        <button id="dial-cancel" style="padding:10px 16px;">Cancel</button>
        <button id="dial-go" style="padding:10px 16px; background:#dbeafe; border:1px solid #2563eb; color:#1e3a8a; font-weight:600;">📞 Dial via Butlr</button>
      </div>
    </div>
  </div>

  <script>
  const q = document.getElementById('q');
  const results = document.getElementById('results');
  const svcStatus = document.getElementById('svc-status');

  // Check Mac2 contacts service health
  fetch('/admin/api/contacts-health').then(r => r.json()).then(j => {
    svcStatus.textContent = j.ok ? `service online — ${j.count} contacts indexed` : 'service offline';
    svcStatus.style.color = j.ok ? 'var(--ink-dim)' : '#d33';
  }).catch(() => { svcStatus.textContent = 'service offline'; svcStatus.style.color = '#d33'; });

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

  async function doSearch(query) {
    if (!query || query.length < 2) { results.innerHTML = ''; return; }
    results.innerHTML = '<p class="muted">searching…</p>';
    try {
      const r = await fetch('/admin/api/contacts-search?q=' + encodeURIComponent(query));
      const j = await r.json();
      if (!j.ok) { results.innerHTML = `<p class="muted" style="color:#d33;">${j.error || 'search failed'}</p>`; return; }
      if (!j.contacts || j.contacts.length === 0) {
        results.innerHTML = '<p class="muted">no matches</p>';
        return;
      }
      results.innerHTML = j.contacts.map(c => {
        const phones = (c.phones || []).map(p => `<a href="#" class="phone" data-number="${p}" data-name="${c.name}" style="margin-right:8px; padding:4px 10px; background:#dbeafe; border:1px solid #2563eb; color:#1e3a8a; border-radius:4px; text-decoration:none; font-size:13px;">📞 ${p}</a>`).join('');
        return `<div style="padding:12px 16px; border:1px solid var(--line); border-radius:8px; background:var(--panel);">
          <div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
            <strong>${c.name || '(no name)'}</strong>
            ${phones || '<span class="muted">no phone</span>'}
          </div>
        </div>`;
      }).join('');
      // Wire phone clicks
      document.querySelectorAll('.phone').forEach(el => {
        el.addEventListener('click', e => {
          e.preventDefault();
          openDial(el.dataset.name, el.dataset.number);
        });
      });
    } catch (e) {
      results.innerHTML = `<p style="color:#d33;">error: ${e.message}</p>`;
    }
  }

  // Dial modal
  const modal = document.getElementById('dial-modal');
  let pending = null;
  function openDial(name, number) {
    pending = { name, number };
    document.getElementById('dial-title').textContent = `Call ${name}`;
    document.getElementById('dial-subtitle').textContent = number;
    document.getElementById('dial-goal').value = '';
    modal.style.display = 'flex';
  }
  document.getElementById('dial-cancel').addEventListener('click', () => { modal.style.display = 'none'; pending = null; });
  document.getElementById('dial-go').addEventListener('click', async () => {
    if (!pending) return;
    const goal = document.getElementById('dial-goal').value.trim() || `Brief test call to ${pending.name}. Be polite. End politely when goodbye.`;
    const r = await fetch('/admin/api/dial', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({ name: pending.name, number: pending.number, goal })
    });
    const j = await r.json();
    if (j.ok) {
      window.location = '/listen/' + j.call_id;
    } else {
      alert('Dial failed: ' + (j.error || 'unknown'));
    }
  });

  </script>

</main>

<%- include('../partials/footer') %>