← 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') %>