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 =
Syncing Finances...
Monthly Cash In: ${MONTHLY_AVG_INCOME.toLocaleString(undefined, { maximumFractionDigits: 0 })}
{scanError}
Projected Surplus
${monthlySurplus.toLocaleString(undefined, { maximumFractionDigits: 0 })}
Bills & Mandatory
${totalFixed.toLocaleString(undefined, { maximumFractionDigits: 0 })}
Statement Spending
${totalVariable.toLocaleString(undefined, { maximumFractionDigits: 0 })}
{expense.name}
{expense.category}
{expense.amount === 0 ? 'PAID' : `$${expense.amount.toLocaleString()}`}
{expense.type === 'user' && ( )}Comparing {selectedMonth} performance with your historical patterns.
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.