import React, { useState, useEffect, useMemo, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import {
getFirestore,
collection,
doc,
setDoc,
onSnapshot,
query,
addDoc,
deleteDoc,
updateDoc
} from 'firebase/firestore';
import {
getAuth,
signInAnonymously,
signInWithCustomToken,
onAuthStateChanged
} from 'firebase/auth';
import {
LayoutDashboard,
TrendingUp,
TrendingDown,
Settings,
PlusCircle,
Trash2,
Edit3,
X,
Wallet,
HandCoins,
History,
Cloud,
User
} from 'lucide-react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js';
import { Line } from 'react-chartjs-2';
// Register ChartJS
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);
// --- Firebase Config ---
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 : 'fintrack-pro-v1';
export default function App() {
const [user, setUser] = useState(null);
const [activeTab, setActiveTab] = useState('dashboard');
const [transactions, setTransactions] = useState([]);
const [categories, setCategories] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState('transaction');
const [editingItem, setEditingItem] = useState(null);
const [dateFilter, setDateFilter] = useState('all');
const [formData, setFormData] = useState({
amount: '', description: '', categoryId: '', type: 'expense',
isRepayment: false, date: new Date().toISOString().split('T')[0]
});
const [catFormData, setCatFormData] = useState({
name: '', type: 'expense', color: '#6366f1'
});
// --- Auth Logic ---
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 (err) {
console.error("Auth Error:", err);
}
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, setUser);
return () => unsubscribe();
}, []);
// --- Data Sync ---
useEffect(() => {
if (!user) return;
// Sync Categories
const catCol = collection(db, 'artifacts', appId, 'users', user.uid, 'categories');
const unsubCats = onSnapshot(catCol, (snapshot) => {
const cats = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
// Initialize default categories if none exist
if (cats.length === 0) {
const defaults = [
{ name: 'Salary', type: 'income', color: '#10b981' },
{ name: 'Food', type: 'expense', color: '#ef4444' },
{ name: 'Rent', type: 'expense', color: '#f59e0b' },
{ name: 'Lending', type: 'debt', color: '#3b82f6' },
{ name: 'Borrowing', type: 'debt', color: '#8b5cf6' },
];
defaults.forEach(d => addDoc(catCol, d));
}
setCategories(cats);
}, (err) => console.error("Firestore Error:", err));
// Sync Transactions
const txCol = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions');
const unsubTxs = onSnapshot(txCol, (snapshot) => {
const txs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setTransactions(txs);
}, (err) => console.error("Firestore Error:", err));
return () => { unsubCats(); unsubTxs(); };
}, [user]);
// --- Calculations ---
const filteredTransactions = useMemo(() => {
const sorted = [...transactions].sort((a, b) => new Date(b.date) - new Date(a.date));
if (dateFilter === 'all') return sorted;
const now = new Date();
return sorted.filter(tx => {
const txDate = new Date(tx.date);
if (dateFilter === 'week') {
const weekAgo = new Date();
weekAgo.setDate(now.getDate() - 7);
return txDate >= weekAgo;
}
return true;
});
}, [transactions, dateFilter]);
const totals = useMemo(() => {
return filteredTransactions.reduce((acc, curr) => {
const cat = categories.find(c => c.id === curr.categoryId);
const val = parseFloat(curr.amount) || 0;
const type = curr.type || cat?.type;
if (type === 'income') acc.income += val;
else if (type === 'expense') acc.expense += val;
else if (type === 'debt') {
const isBorrowing = cat?.name.toLowerCase().includes('borrow');
if (curr.isRepayment) {
if (isBorrowing) acc.debt -= val; else acc.lent -= val;
} else {
if (isBorrowing) acc.debt += val; else acc.lent += val;
}
}
return acc;
}, { income: 0, expense: 0, debt: 0, lent: 0 });
}, [filteredTransactions, categories]);
// --- Chart Data ---
const chartData = useMemo(() => {
const sorted = [...filteredTransactions].sort((a, b) => new Date(a.date) - new Date(b.date));
const labels = [...new Set(sorted.map(t => t.date))];
const incomeData = labels.map(date =>
sorted.filter(t => t.date === date && (t.type === 'income')).reduce((s, c) => s + c.amount, 0)
);
const expenseData = labels.map(date =>
sorted.filter(t => t.date === date && (t.type === 'expense')).reduce((s, c) => s + c.amount, 0)
);
return {
labels,
datasets: [
{ label: 'Income', data: incomeData, borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', fill: true, tension: 0.4 },
{ label: 'Expenses', data: expenseData, borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.1)', fill: true, tension: 0.4 }
]
};
}, [filteredTransactions]);
// --- Actions ---
const handleTxSubmit = async (e) => {
e.preventDefault();
if (!user) return;
const txData = { ...formData, amount: parseFloat(formData.amount) };
const txCol = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions');
if (editingItem) {
await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'transactions', editingItem.id), txData);
} else {
await addDoc(txCol, txData);
}
setIsModalOpen(false);
};
const handleCatSubmit = async (e) => {
e.preventDefault();
if (!user) return;
const catCol = collection(db, 'artifacts', appId, 'users', user.uid, 'categories');
if (editingItem) {
await updateDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'categories', editingItem.id), catFormData);
} else {
await addDoc(catCol, catFormData);
}
setIsModalOpen(false);
};
const openModal = (mode, item = null) => {
setModalMode(mode);
setEditingItem(item);
if (mode === 'transaction') {
setFormData(item || {
amount: '', description: '', type: 'expense',
categoryId: categories.find(c => c.type === 'expense')?.id || '',
isRepayment: false, date: new Date().toISOString().split('T')[0]
});
} else {
setCatFormData(item || { name: '', type: 'expense', color: '#6366f1' });
}
setIsModalOpen(true);
};
if (!user) return
Initializing Secure Session...
;
return (
{/* Sidebar */}
{/* Main content */}
{activeTab === 'dashboard' && (
{/* Stats */}
{[
{ label: 'Balance', val: totals.income - totals.expense, color: 'bg-indigo-600', icon: Wallet },
{ label: 'Income', val: totals.income, color: 'bg-emerald-500', icon: TrendingUp },
{ label: 'Expenses', val: totals.expense, color: 'bg-rose-500', icon: TrendingDown },
{ label: 'Money Lent', val: totals.lent, color: 'bg-blue-500', icon: HandCoins },
].map(stat => (
{stat.label}
₹{stat.val.toLocaleString()}
))}
{/* Chart */}
CASH FLOW
{['all', 'week'].map(f => (
))}
{/* Debt View */}
Liability Portfolio
₹{totals.debt.toLocaleString()}
Total active borrowings
Payback Status
{totals.debt > 0 ? 'Action Required' : 'Cleared'}
{/* Recent Activity */}
LATEST RECORDS
{filteredTransactions.slice(0, 5).map(tx => {
const cat = categories.find(c => c.id === tx.categoryId);
return (
{tx.isRepayment ? : }
{tx.description}
{cat?.name} • {tx.date}
{tx.isRepayment ? '↓' : (tx.type === 'income' ? '+' : '-')} ₹{tx.amount.toLocaleString()}
);
})}
)}
{activeTab === 'ledger' && (
| Date |
Description |
Category |
Amount |
Action |
{filteredTransactions.map(tx => {
const cat = categories.find(c => c.id === tx.categoryId);
return (
| {tx.date} |
{tx.description}
{tx.isRepayment && Repayment}
|
{cat?.name}
|
₹{tx.amount.toLocaleString()}
|
|
);
})}
)}
{activeTab === 'settings' && (
{categories.map(cat => (
))}
)}
{/* Modal */}
{isModalOpen && (
{editingItem ? 'Edit' : 'Record'} {modalMode}
{modalMode === 'transaction' ? (
) : (
)}
)}
{/* Mobile Nav */}
);
}