← back to AbramsOS

views/import.ejs

133 lines

<%- include('partials/header', { title: 'Import' }) %>

<section class="import-hero glass">
  <h1>Import receipts from everything you use</h1>
  <p>One button. Pulls Gmail, Drive, and every linked bank or credit card via Plaid (Amex, Wells, Chase, Citi, Cap One, Discover, BofA — 200+ US institutions). Tokenized — your card numbers never touch AbramsOS.</p>
  <button id="importAllBtn" class="import-all-btn" type="button">Import all receipts now</button>
  <div id="importStatus" class="import-status"></div>
  <p class="subtle" style="margin-top:18px">Two-factor verification is required before each import. Your code expires after 60 seconds.</p>
</section>

<section class="connector-grid">
  <% cards.forEach(card => { %>
    <article class="connector-card glass" data-key="<%= card.key %>">
      <header>
        <span class="name">
          <span class="icon"><%= card.icon %></span>
          <%= card.name %>
        </span>
        <span class="status <%= card.connected ? 'connected' : 'not_connected' %>">
          <%= card.connected ? 'Connected' : 'Not connected' %>
        </span>
      </header>
      <p class="desc"><%= card.description %></p>

      <% if (card.deferred) { %>
        <p class="subtle" style="font-size:11px">Coming in the next milestone.</p>
      <% } else if (card.connected) { %>
        <% card.accounts.forEach(a => { %>
          <div class="row-actions" style="font-size:12px;color:var(--text-dim)">
            <span><%= a.external_subject_id %></span>
            <% if (a.last_sync_at) { %>
              <span>· last sync <%= new Date(a.last_sync_at).toLocaleString() %></span>
            <% } %>
            <% if (a.message_count != null) { %>
              <span>· <%= a.message_count %> msgs</span>
            <% } %>
          </div>
        <% }) %>
        <% if (card.provider === 'plaid') { %>
          <div class="row-actions">
            <button class="button" type="button" onclick="openPlaidLink()">Add another bank or card</button>
          </div>
        <% } %>
      <% } else { %>
        <% if (card.provider === 'google') { %>
          <a class="button primary" href="<%= card.connectPath %>">Connect <%= card.name %></a>
        <% } else if (card.provider === 'plaid') { %>
          <button class="button primary" type="button" onclick="openPlaidLink()">Connect Plaid (sandbox)</button>
          <p class="subtle" style="font-size:11px">Sandbox creds: user_good / pass_good</p>
        <% } %>
      <% } %>
    </article>
  <% }) %>
</section>

<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
<script>
(function(){
  const status = document.getElementById('importStatus');
  const btn = document.getElementById('importAllBtn');

  async function importAll() {
    btn.disabled = true;
    status.textContent = 'Starting import…';
    try {
      const r = await fetch('/import/run', { method: 'POST' });
      if (r.status === 403) {
        // Step-up expired mid-page; bounce to step-up
        location.href = '/step-up?next=' + encodeURIComponent('/import');
        return;
      }
      const j = await r.json();
      if (j.ok) {
        const totals = (j.results || []).map(x =>
          x.status === 200
            ? `${x.provider}: ${x.body.purchases ?? 0} new`
            : `${x.provider}: ${x.body?.error || 'error'}`
        );
        status.textContent = totals.length ? totals.join(' · ') : (j.message || 'no connectors');
      } else {
        status.textContent = 'Error: ' + (j.error || 'unknown');
      }
    } catch (e) {
      status.textContent = 'Error: ' + e.message;
    } finally {
      btn.disabled = false;
      setTimeout(() => location.reload(), 2200);
    }
  }
  btn.addEventListener('click', importAll);

  // Plaid Link
  let plaidHandler = null;
  window.openPlaidLink = async function () {
    status.textContent = 'Opening Plaid Link…';
    try {
      const r = await fetch('/api/plaid/link/token', { method: 'POST' });
      const j = await r.json();
      if (!j.link_token) {
        status.textContent = 'Plaid not configured: ' + (j.error || 'set PLAID_CLIENT_ID + PLAID_SECRET via /secrets');
        return;
      }
      plaidHandler = Plaid.create({
        token: j.link_token,
        onSuccess: async (publicToken, metadata) => {
          status.textContent = 'Linking ' + (metadata.institution?.name || 'institution') + '…';
          const ex = await fetch('/api/plaid/exchange', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ public_token: publicToken, institution_name: metadata.institution?.name }),
          });
          const ej = await ex.json();
          if (ej.ok) {
            status.textContent = 'Connected. Click Import all to sync.';
            setTimeout(() => location.reload(), 1500);
          } else {
            status.textContent = 'Plaid exchange failed: ' + (ej.error || 'unknown');
          }
        },
        onExit: (err) => {
          if (err) status.textContent = 'Plaid Link exited: ' + err.error_message;
        },
      });
      plaidHandler.open();
    } catch (e) {
      status.textContent = 'Error: ' + e.message;
    }
  };
})();
</script>

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