import type { TransactionMatch, YNABCategory, APIResponse } from './types'; let currentSessionId: string | null = null; let matches: TransactionMatch[] = []; let categories: YNABCategory[] = []; // Initialize date inputs with default values (last 30 days) const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); const startDateInput = document.getElementById('startDate') as HTMLInputElement; const endDateInput = document.getElementById('endDate') as HTMLInputElement; const useUnapprovedCheckbox = document.getElementById('useUnapproved') as HTMLInputElement; const dateRangeSection = document.getElementById('dateRangeSection') as HTMLDivElement; startDateInput.value = startDate.toISOString().split('T')[0]; endDateInput.value = endDate.toISOString().split('T')[0]; // Fetch categories on page load fetchCategories(); // Event listeners document.getElementById('fetchBtn')?.addEventListener('click', fetchMatches); document.getElementById('approveBtn')?.addEventListener('click', approveMatches); // Toggle date range visibility based on checkbox useUnapprovedCheckbox.addEventListener('change', () => { if (useUnapprovedCheckbox.checked) { dateRangeSection.style.display = 'none'; } else { dateRangeSection.style.display = 'block'; } }); async function fetchCategories() { try { const response = await fetch('/api/categories'); const data: APIResponse = await response.json(); if (data.success && data.data) { categories = data.data; console.log(`Loaded ${categories.length} categories`); } } catch (error) { console.error('Error fetching categories:', error); } } async function fetchMatches() { const useUnapproved = useUnapprovedCheckbox.checked; const startDate = (document.getElementById('startDate') as HTMLInputElement).value; const endDate = (document.getElementById('endDate') as HTMLInputElement).value; // Validate inputs if (!useUnapproved && (!startDate || !endDate)) { showError('Please select both start and end dates or use unapproved transactions'); return; } // Show loading state showSection('loading'); hideError(); try { const body: any = { useUnapproved }; if (!useUnapproved) { body.startDate = startDate; body.endDate = endDate; } const response = await fetch('/api/fetch-matches', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); const data: APIResponse<{ sessionId: string; stats: any }> = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to fetch matches'); } currentSessionId = data.data!.sessionId; // Display statistics displayStats(data.data!.stats); // Fetch the matches await loadMatches(); // Show matches section showSection('matches'); } catch (error) { showSection('error'); showError(error instanceof Error ? error.message : 'An error occurred'); } } async function loadMatches() { if (!currentSessionId) return; try { const response = await fetch(`/api/matches/${currentSessionId}`); const data: APIResponse = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load matches'); } matches = data.data!; displayMatches(); } catch (error) { showError(error instanceof Error ? error.message : 'Failed to load matches'); } } function displayStats(stats: any) { const statsContainer = document.getElementById('stats')!; statsContainer.innerHTML = `
Total Transactions
${stats.total}
Matched
${stats.matched}
High Confidence
${stats.highConfidence}
Medium Confidence
${stats.mediumConfidence}
Low Confidence
${stats.lowConfidence}
`; } function displayMatches() { const matchesList = document.getElementById('matchesList')!; matchesList.innerHTML = ''; matches.forEach((match, index) => { const card = createMatchCard(match, index); matchesList.appendChild(card); }); } function createMatchCard(match: TransactionMatch, index: number): HTMLDivElement { const card = document.createElement('div'); card.className = `match-card ${match.status === 'approved' ? 'status-approved' : ''} ${match.status === 'rejected' ? 'status-rejected' : ''} ${!match.amazonOrder ? 'no-match' : ''}`; const amount = Math.abs(match.ynabTransaction.amount / 1000); const confidenceClass = match.matchConfidence >= 0.8 ? 'confidence-high' : match.matchConfidence >= 0.5 ? 'confidence-medium' : 'confidence-low'; const confidencePercent = Math.round(match.matchConfidence * 100); card.innerHTML = `
${match.ynabTransaction.date} • ${match.ynabTransaction.account_name}
${match.ynabTransaction.payee_name || 'Unknown Payee'}
$${amount.toFixed(2)}
${match.ynabTransaction.memo ? `
${match.ynabTransaction.memo}
` : ''}
${match.amazonOrder ? `
${confidencePercent}% Match
` : '
No Match
'}
${match.amazonOrder ? `
Amazon Order #${match.amazonOrder.orderId}
${match.amazonOrder.items.map(item => `• ${item.title} (${item.quantity}x $${item.price.toFixed(2)})`).join('
')}
Total: $${match.amazonOrder.total.toFixed(2)}
View on Amazon →
` : `
No matching Amazon order found within the date and amount tolerance.
`} `; return card; } function renderCategoryOptions(suggestedId?: string): string { const grouped = new Map(); categories.forEach((cat) => { if (!grouped.has(cat.category_group_name)) { grouped.set(cat.category_group_name, []); } grouped.get(cat.category_group_name)!.push(cat); }); let html = ''; grouped.forEach((cats, groupName) => { html += ``; cats.forEach((cat) => { const selected = cat.id === suggestedId ? 'selected' : ''; html += ``; }); html += ''; }); return html; } (window as any).updateMatchStatus = async (index: number, status: 'approved' | 'rejected') => { if (!currentSessionId) return; // Get the selected category const categorySelect = document.getElementById(`category-${index}`) as HTMLSelectElement; const categoryId = categorySelect?.value || null; const suggestedCategory = categoryId ? categories.find((c) => c.id === categoryId) || null : null; try { const response = await fetch(`/api/matches/${currentSessionId}/${index}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status, suggestedCategory }), }); const data: APIResponse = await response.json(); if (data.success && data.data) { matches[index] = data.data; displayMatches(); } } catch (error) { console.error('Error updating match status:', error); showError('Failed to update match status'); } }; async function approveMatches() { if (!currentSessionId) return; const approvedCount = matches.filter((m) => m.status === 'approved').length; if (approvedCount === 0) { showError('No matches approved. Please approve at least one match before applying.'); return; } if (!confirm(`Apply ${approvedCount} approved match(es) to YNAB?`)) { return; } showSection('loading'); try { const response = await fetch(`/api/approve-matches/${currentSessionId}`, { method: 'POST', }); const data: APIResponse = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to apply matches'); } const results = data.data!; const successCount = results.filter((r) => r.success).length; const failCount = results.length - successCount; alert( `Successfully updated ${successCount} transaction(s)!${ failCount > 0 ? `\n${failCount} failed.` : '' }` ); showSection('matches'); } catch (error) { showSection('matches'); showError(error instanceof Error ? error.message : 'Failed to apply matches'); } } function showSection(section: 'loading' | 'error' | 'matches' | 'stats') { document.getElementById('loadingSection')!.style.display = section === 'loading' ? 'block' : 'none'; document.getElementById('errorSection')!.style.display = section === 'error' ? 'block' : 'none'; document.getElementById('matchesSection')!.style.display = section === 'matches' ? 'block' : 'none'; document.getElementById('statsSection')!.style.display = section === 'stats' || section === 'matches' ? 'block' : 'none'; } function showError(message: string) { const errorMessage = document.getElementById('errorMessage')!; errorMessage.textContent = message; document.getElementById('errorSection')!.style.display = 'block'; } function hideError() { document.getElementById('errorSection')!.style.display = 'none'; }