266 lines
9.5 KiB
HTML
266 lines
9.5 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<title>Plaid Data Dashboard</title>
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||
|
|
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
|
||
|
|
<style>
|
||
|
|
body {
|
||
|
|
font-family: Arial, sans-serif;
|
||
|
|
max-width: 1200px;
|
||
|
|
margin: 0 auto;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
.panel {
|
||
|
|
background: #f5f5f5;
|
||
|
|
padding: 15px;
|
||
|
|
margin: 10px 0;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
.data-section {
|
||
|
|
margin: 20px 0;
|
||
|
|
padding: 20px;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
button {
|
||
|
|
padding: 10px 20px;
|
||
|
|
margin: 5px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 4px;
|
||
|
|
background: #007bff;
|
||
|
|
color: white;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
button:hover {
|
||
|
|
background: #0056b3;
|
||
|
|
}
|
||
|
|
.account-card {
|
||
|
|
background: white;
|
||
|
|
padding: 15px;
|
||
|
|
margin: 10px 0;
|
||
|
|
border-radius: 4px;
|
||
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
|
|
}
|
||
|
|
.transaction-item {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 10px;
|
||
|
|
border-bottom: 1px solid #eee;
|
||
|
|
}
|
||
|
|
.positive { color: green; }
|
||
|
|
.negative { color: red; }
|
||
|
|
#logs {
|
||
|
|
max-height: 200px;
|
||
|
|
overflow-y: auto;
|
||
|
|
font-family: monospace;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<h1>Plaid Data Dashboard</h1>
|
||
|
|
|
||
|
|
<div class="panel">
|
||
|
|
<h3>Step 1: Link Bank Account</h3>
|
||
|
|
<button id="newTokenButton">Get New Link Token</button>
|
||
|
|
<button id="linkButton">Connect Bank Account</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="data-section">
|
||
|
|
<h3>Accounts Overview</h3>
|
||
|
|
<button onclick="fetchAccounts()">Refresh Accounts</button>
|
||
|
|
<div id="accountsData"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="data-section">
|
||
|
|
<h3>Transactions</h3>
|
||
|
|
<button onclick="fetchTransactions()">Load Transactions</button>
|
||
|
|
<div id="transactionsData"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="data-section">
|
||
|
|
<h3>Income Information</h3>
|
||
|
|
<button onclick="fetchIncome()">Load Income Data</button>
|
||
|
|
<div id="incomeData"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="panel">
|
||
|
|
<h3>Debug Logs</h3>
|
||
|
|
<div id="logs"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let linkToken = null;
|
||
|
|
let accessToken = null;
|
||
|
|
let linkHandler = null;
|
||
|
|
|
||
|
|
function addLog(message, data = null) {
|
||
|
|
const logDiv = document.createElement('div');
|
||
|
|
logDiv.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
|
||
|
|
if (data) {
|
||
|
|
logDiv.innerHTML += `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
||
|
|
}
|
||
|
|
const logs = document.getElementById('logs');
|
||
|
|
logs.insertBefore(logDiv, logs.firstChild);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getNewLinkToken() {
|
||
|
|
try {
|
||
|
|
addLog('Getting new link token...');
|
||
|
|
const response = await fetch('http://localhost:3000/api/create_link_token', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ userId: 'test-user-' + Date.now() })
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
if (data.link_token) {
|
||
|
|
linkToken = data.link_token;
|
||
|
|
addLog('Got new link token', { link_token: data.link_token });
|
||
|
|
initializePlaidLink();
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
addLog('Error getting link token', { error: err.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchAccounts() {
|
||
|
|
if (!accessToken) {
|
||
|
|
addLog('No access token available. Please connect your bank first.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const response = await fetch('http://localhost:3000/api/accounts', {
|
||
|
|
headers: { 'plaid-access-token': accessToken }
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
displayAccounts(data.accounts);
|
||
|
|
} catch (err) {
|
||
|
|
addLog('Error fetching accounts', { error: err.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchTransactions() {
|
||
|
|
if (!accessToken) {
|
||
|
|
addLog('No access token available. Please connect your bank first.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const response = await fetch('http://localhost:3000/api/transactions', {
|
||
|
|
headers: { 'plaid-access-token': accessToken }
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
displayTransactions(data.transactions);
|
||
|
|
} catch (err) {
|
||
|
|
addLog('Error fetching transactions', { error: err.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchIncome() {
|
||
|
|
if (!accessToken) {
|
||
|
|
addLog('No access token available. Please connect your bank first.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const response = await fetch('http://localhost:3000/api/income', {
|
||
|
|
headers: { 'plaid-access-token': accessToken }
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
displayIncome(data);
|
||
|
|
} catch (err) {
|
||
|
|
addLog('Error fetching income', { error: err.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function displayAccounts(accounts) {
|
||
|
|
const container = document.getElementById('accountsData');
|
||
|
|
container.innerHTML = accounts.map(account => `
|
||
|
|
<div class="account-card">
|
||
|
|
<h4>${account.name} (${account.subtype})</h4>
|
||
|
|
<p>Balance: $${account.balances.current.toFixed(2)}</p>
|
||
|
|
<p>Available: $${account.balances.available ? account.balances.available.toFixed(2) : 'N/A'}</p>
|
||
|
|
<p>Type: ${account.type}</p>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function displayTransactions(transactions) {
|
||
|
|
const container = document.getElementById('transactionsData');
|
||
|
|
if (!transactions || transactions.length === 0) {
|
||
|
|
container.innerHTML = '<p>No transactions found</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="transaction-header">
|
||
|
|
<p>Found ${transactions.length} transactions</p>
|
||
|
|
</div>
|
||
|
|
${transactions.map(trans => `
|
||
|
|
<div class="transaction-item">
|
||
|
|
<div class="transaction-info">
|
||
|
|
<strong>${trans.date}</strong>
|
||
|
|
<span>${trans.merchant_name || trans.name}</span>
|
||
|
|
</div>
|
||
|
|
<span class="${trans.amount >= 0 ? 'negative' : 'positive'}">
|
||
|
|
${trans.amount >= 0 ? '-' : '+'}${Math.abs(trans.amount).toFixed(2)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
`).join('')}
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function displayIncome(incomeData) {
|
||
|
|
const container = document.getElementById('incomeData');
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="panel">
|
||
|
|
<h4>Income Summary</h4>
|
||
|
|
<p>Latest Monthly Income: ${incomeData.latest_monthly_income || 'N/A'}</p>
|
||
|
|
<p>Projected Monthly Income: ${incomeData.projected_monthly_income || 'N/A'}</p>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializePlaidLink() {
|
||
|
|
linkHandler = Plaid.create({
|
||
|
|
token: linkToken,
|
||
|
|
onSuccess: async (public_token, metadata) => {
|
||
|
|
addLog('Link Success - Got public token', { metadata });
|
||
|
|
try {
|
||
|
|
const response = await fetch('http://localhost:3000/api/exchange_public_token', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ public_token })
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
if (response.ok) {
|
||
|
|
accessToken = data.access_token;
|
||
|
|
addLog('Exchange successful');
|
||
|
|
// Automatically fetch accounts after successful connection
|
||
|
|
fetchAccounts();
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
addLog('Exchange error', { error: err.message });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onExit: (err, metadata) => {
|
||
|
|
if (err != null) {
|
||
|
|
addLog('Link error', { error: err, metadata });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize event listeners
|
||
|
|
document.getElementById('newTokenButton').onclick = getNewLinkToken;
|
||
|
|
document.getElementById('linkButton').onclick = () => {
|
||
|
|
if (linkHandler) {
|
||
|
|
linkHandler.open();
|
||
|
|
} else {
|
||
|
|
addLog('Please get a new link token first');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initial setup
|
||
|
|
getNewLinkToken();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|