import React, { useState, useMemo, useRef, useEffect } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, updateDoc, deleteDoc, query } from 'firebase/firestore'; import { Wallet, Car, Home, Utensils, TrendingUp, Calendar, CreditCard, CheckCircle2, ChevronRight, ArrowRight, RefreshCw, Zap, Building2, ShieldCheck, ShoppingBag, Scissors, Hammer, Gamepad2, BarChart3, PlusCircle, History, ChevronDown, Camera, Loader2, FileText, AlertCircle, ArrowUpRight, Plus, X, Cloud, Fuel, Layers } from 'lucide-react'; // --- Firebase Configuration --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'wealthsimple-budget-tracker'; const App = () => { // User & Auth State const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // Constants const BI_WEEKLY_INCOME = 1865; const MONTHLY_AVG_INCOME = (BI_WEEKLY_INCOME * 26) / 12; const apiKey = ""; const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; // Budget State const [availableYears, setAvailableYears] = useState([2026]); const [selectedYear, setSelectedYear] = useState(2026); const [months, setMonths] = useState(['March 2026', 'April 2026']); const [selectedMonth, setSelectedMonth] = useState('April 2026'); const [monthlyData, setMonthlyData] = useState({}); const [customFixedExpenses, setCustomFixedExpenses] = useState([]); const [savingsGoal, setSavingsGoal] = useState(20000); const [isPostJuly, setIsPostJuly] = useState(false); // UI States const [view, setView] = useState('dashboard'); const [isScanning, setIsScanning] = useState(false); const [scanError, setScanError] = useState(null); const [showAddBillModal, setShowAddBillModal] = useState(false); const [newBill, setNewBill] = useState({ name: '', amount: '', category: 'Bills' }); const fileInputRef = useRef(null); // 1. Authentication Lifecycle useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (user) => { setUser(user); if (!user) setLoading(false); }); return () => unsubscribe(); }, []); // 2. Data Synchronization Lifecycle useEffect(() => { if (!user) return; const userRootPath = `/artifacts/${appId}/users/${user.uid}`; // Listen to Settings const settingsUnsub = onSnapshot(doc(db, userRootPath, 'settings', 'global'), (snapshot) => { if (snapshot.exists()) { const data = snapshot.data(); if (data.savingsGoal) setSavingsGoal(data.savingsGoal); if (data.isPostJuly !== undefined) setIsPostJuly(data.isPostJuly); if (data.availableYears) setAvailableYears(data.availableYears); if (data.months) setMonths(data.months); } else { setDoc(doc(db, userRootPath, 'settings', 'global'), { savingsGoal: 20000, isPostJuly: false, availableYears: [2026], months: ['March 2026', 'April 2026'] }); } }, (error) => console.error("Settings listener error:", error)); // Listen to Monthly Data const dataUnsub = onSnapshot(collection(db, userRootPath, 'monthlyData'), (snapshot) => { const data = {}; snapshot.forEach(doc => { data[doc.id] = doc.data(); }); setMonthlyData(data); setLoading(false); }, (error) => console.error("Monthly data listener error:", error)); // Listen to Fixed Expenses const fixedUnsub = onSnapshot(collection(db, userRootPath, 'fixedExpenses'), (snapshot) => { const bills = []; snapshot.forEach(doc => { bills.push({ id: doc.id, ...doc.data() }); }); setCustomFixedExpenses(bills); }, (error) => console.error("Fixed expenses listener error:", error)); return () => { settingsUnsub(); dataUnsub(); fixedUnsub(); }; }, [user]); // Firestore Helpers const handleVariableChange = async (key, value) => { if (!user) return; const numericValue = parseInt(value) || 0; const monthRef = doc(db, `artifacts/${appId}/users/${user.uid}/monthlyData`, selectedMonth); await setDoc(monthRef, { [key]: numericValue }, { merge: true }); }; const addNewMonth = async () => { if (!user) return; const lastMonthStr = months[months.length - 1]; const [mName, yStr] = lastMonthStr.split(' '); let monthIdx = MONTH_NAMES.indexOf(mName); let year = parseInt(yStr); monthIdx++; if (monthIdx > 11) { monthIdx = 0; year++; } const nextMonthName = `${MONTH_NAMES[monthIdx]} ${year}`; if (months.includes(nextMonthName)) return; const newYears = availableYears.includes(year) ? availableYears : [...availableYears, year].sort(); const newMonthsList = [...months, nextMonthName]; await updateDoc(doc(db, `artifacts/${appId}/users/${user.uid}/settings/global`), { availableYears: newYears, months: newMonthsList }); await setDoc(doc(db, `artifacts/${appId}/users/${user.uid}/monthlyData`, nextMonthName), currentSpending || {}); setSelectedMonth(nextMonthName); setSelectedYear(year); }; const handleAddBill = async () => { if (!user || !newBill.name || !newBill.amount) return; const id = Date.now().toString(); await setDoc(doc(db, `artifacts/${appId}/users/${user.uid}/fixedExpenses`, id), { name: newBill.name, amount: parseFloat(newBill.amount), category: newBill.category, type: 'user' }); setNewBill({ name: '', amount: '', category: 'Bills' }); setShowAddBillModal(false); }; const removeBill = async (id) => { if (!user) return; await deleteDoc(doc(db, `artifacts/${appId}/users/${user.uid}/fixedExpenses`, id)); }; // AI Scanning Logic (Support Multiple Files) const scanStatement = async (e) => { const files = Array.from(e.target.files); if (!files.length || !user) return; setIsScanning(true); setScanError(null); try { // Process all images to Base64 const imageParts = await Promise.all(files.map(async (file) => { const base64 = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.readAsDataURL(file); }); return { inlineData: { mimeType: file.type, data: base64 } }; })); const prompt = `You are an expert financial assistant. Analyze the provided credit card statement image(s). If multiple images are provided, sum the totals across ALL statements. Extract and categorize spending into these exact keys: - 'dining': Coffee, restaurants, cafes, fast food. - 'shopping': General retail, Vaughan Mills, clothing, gifts. - 'personalCare': Hair salons, pharmacies, grooming. - 'homeHardware': Home Depot, Canadian Tire, hardware stores. - 'leisure': Entertainment, golf, games, movies. - 'subscriptions': Netflix, Google, YouTube, Discord, mobile apps. - 'transportation': Gas, Shell, Esso, parking, transit. Return ONLY a raw JSON object with these keys and their total numeric values (rounded to nearest dollar). If a category has no transactions, set it to 0. Important: Sum the values from all images before returning the object.`; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }, ...imageParts] }], generationConfig: { responseMimeType: "application/json" } }) }); const result = await response.json(); const parsedData = JSON.parse(result.candidates?.[0]?.content?.parts?.[0]?.text); // ADDITIVE LOGIC: Add newly scanned data to current data const updatedMonthlyData = { ...currentSpending }; Object.keys(parsedData).forEach(key => { updatedMonthlyData[key] = (updatedMonthlyData[key] || 0) + parsedData[key]; }); await setDoc(doc(db, `artifacts/${appId}/users/${user.uid}/monthlyData`, selectedMonth), updatedMonthlyData, { merge: true }); } catch (err) { console.error("Scan Error:", err); setScanError("AI Analysis failed. Make sure images are clear and try again."); } finally { setIsScanning(false); if (fileInputRef.current) fileInputRef.current.value = ""; // Clear input for next scan } }; // Derived Values const filteredMonths = months.filter(m => m.includes(selectedYear.toString())); const currentSpending = monthlyData[selectedMonth] || { dining: 0, shopping: 0, personalCare: 0, homeHardware: 0, leisure: 0, subscriptions: 0, transportation: 0 }; const prevMonthIndex = months.indexOf(selectedMonth) - 1; const prevSpending = prevMonthIndex >= 0 ? monthlyData[months[prevMonthIndex]] : null; const fixedExpenses = useMemo(() => { const carPaymentIsDone = isPostJuly || (selectedYear >= 2026 && MONTH_NAMES.indexOf(selectedMonth.split(' ')[0]) >= 7); const systemBills = [ { id: 'car', name: 'Car Payment', amount: carPaymentIsDone ? 0 : 600, category: 'Auto', type: 'system' }, { id: 'savings', name: 'Mandatory Savings', amount: 400, category: 'Savings', type: 'system' }, { id: 'insurance', name: 'Auto Insurance', amount: 165, category: 'Auto', type: 'system' }, { id: 'alectra', name: 'Alectra', amount: 174.36, category: 'Utilities', type: 'system' }, { id: 'enbridge', name: 'Enbridge', amount: 115, category: 'Utilities', type: 'system' }, { id: 'enercare', name: 'Enercare', amount: 53.17, category: 'Utilities', type: 'system' }, { id: 'bell', name: 'Bell Mobility', amount: 112.99, category: 'Bills', type: 'system' }, { id: 'public', name: 'Public Mobile', amount: 45.20, category: 'Bills', type: 'system' }, ]; return [...systemBills, ...customFixedExpenses].map(expense => { let icon = ; if (expense.category === 'Auto') icon = ; if (expense.category === 'Utilities') icon = ; if (expense.category === 'Savings') icon = ; return { ...expense, icon }; }); }, [isPostJuly, selectedMonth, selectedYear, customFixedExpenses]); const totalFixed = fixedExpenses.reduce((acc, curr) => acc + curr.amount, 0); const totalVariable = Object.values(currentSpending).reduce((acc, curr) => acc + curr, 0); const totalMonthlyExpenses = totalFixed + totalVariable; const monthlySurplus = MONTHLY_AVG_INCOME - totalMonthlyExpenses; const monthsToGoal = monthlySurplus > 0 ? savingsGoal / monthlySurplus : Infinity; const perPaycheckSurplus = (monthlySurplus * 12) / 26; const perPaycheckBills = (totalFixed * 12) / 26; if (loading) { return (

Syncing Finances...

); } return (
{/* Navigation Bar */}
{['dashboard', 'trends', 'automation'].map((v) => ( ))}
{/* Hero Section */}

{selectedMonth} Summary

Monthly Cash In: ${MONTHLY_AVG_INCOME.toLocaleString(undefined, { maximumFractionDigits: 0 })}

{scanError && (

{scanError}

)} {view === 'dashboard' && (
{/* Stats Cards */}

Projected Surplus

${monthlySurplus.toLocaleString(undefined, { maximumFractionDigits: 0 })}

Bills & Mandatory

${totalFixed.toLocaleString(undefined, { maximumFractionDigits: 0 })}

Statement Spending

${totalVariable.toLocaleString(undefined, { maximumFractionDigits: 0 })}

{/* Dynamic Spending Controls */}

Variable Categories

Cloud Synced
{[ { key: 'dining', label: 'Dining & Coffee', icon: , max: 1000 }, { key: 'transportation', label: 'Gas & Parking', icon: , max: 500 }, { key: 'shopping', label: 'Misc Shopping', icon: , max: 800 }, { key: 'personalCare', label: 'Personal Care', icon: , max: 500 }, { key: 'homeHardware', label: 'Home & Hardware', icon: , max: 600 }, { key: 'leisure', label: 'Leisure & Fun', icon: , max: 500 }, { key: 'subscriptions', label: 'Subscriptions', icon: , max: 300 } ].map((item) => (
${currentSpending[item.key] || 0}
handleVariableChange(item.key, e.target.value)} className="w-full h-1.5 bg-[#F2EFED] rounded-full appearance-none cursor-pointer accent-black transition-all hover:h-2" />
))}
{/* Fixed Expenses Tracking */}

Fixed & Essential

{fixedExpenses.map((expense, idx) => (
{React.cloneElement(expense.icon, { size: 20, strokeWidth: 2.5 })}

{expense.name}

{expense.category}

{expense.amount === 0 ? 'PAID' : `$${expense.amount.toLocaleString()}`}

{expense.type === 'user' && ( )}
))}
)} {view === 'trends' && (

Trend Analysis

Comparing {selectedMonth} performance with your historical patterns.

{Object.keys(currentSpending).map((key) => { const current = currentSpending[key] || 0; const baseline = prevSpending ? (prevSpending[key] || 0) : 150; const diff = current - baseline; const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); return (
{label}
0 ? 'text-red-500 bg-red-50 px-3 py-1 rounded-full' : 'text-[#00835F] bg-emerald-50 px-3 py-1 rounded-full'}> {diff > 0 ? `+ $${diff}` : `- $${Math.abs(diff)}`}
baseline ? 'bg-black' : 'bg-[#00835F]'}`} style={{ width: `${Math.min(100, (current / 800) * 100)}%` }} />
); })}
)} {view === 'automation' && (

Automate Wealth

Wealthsimple flows ensure you pay yourself first. Your bi-weekly strategy for ${BI_WEEKLY_INCOME.toLocaleString()}:

Bi-Weekly Bill Sweep

${perPaycheckBills.toFixed(0)}

Transfer to your dedicated Bills Account on payday.

Invest / Save Sweep

${perPaycheckSurplus.toFixed(0)}

Direct deposit to TFSA/FHSA every payday.

)} {/* Add Bill Modal */} {showAddBillModal && (

New Bill

setNewBill({...newBill, name: e.target.value})} className="w-full bg-[#FBF9F7] border border-[#E5E2DE] px-6 py-4 rounded-[24px] outline-none focus:border-black font-bold text-lg" />
setNewBill({...newBill, amount: e.target.value})} className="w-full bg-[#FBF9F7] border border-[#E5E2DE] px-6 py-4 rounded-[24px] outline-none focus:border-black font-bold text-lg" />
)}
); }; export default App;