import React, { useState, useEffect, useMemo } from 'react';
import {
Plus,
Minus,
DollarSign,
TrendingUp,
TrendingDown,
Calendar,
PieChart,
Wallet,
CreditCard,
ArrowRight,
ChevronLeft,
ChevronRight,
Briefcase,
Building,
RefreshCw,
Clock,
Layers
} from 'lucide-react';
// Firebase Imports
import { initializeApp } from "firebase/app";
import {
getAuth,
signInAnonymously,
onAuthStateChanged,
signInWithCustomToken
} from "firebase/auth";
import {
getFirestore,
collection,
addDoc,
onSnapshot,
query,
orderBy,
deleteDoc,
doc,
Timestamp,
where
} from "firebase/firestore";
// --- Configuration & Helpers ---
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 : 'powerad-fin-v1';
// Theme Colors - PoweRad Branding
const THEME = {
primary: "bg-red-700 hover:bg-red-800 text-white",
primaryLight: "bg-red-50 text-red-700",
secondary: "bg-slate-900 text-white",
accent: "text-red-600",
success: "text-emerald-600 bg-emerald-50",
danger: "text-rose-600 bg-rose-50",
neutral: "bg-gray-100 text-gray-800",
card: "bg-white shadow-sm border border-slate-100",
};
// Date Helpers
const getStartOfPeriod = (date, period) => {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
if (period === 'weekly') {
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust when day is sunday
d.setDate(diff);
} else if (period === 'monthly') {
d.setDate(1);
} else if (period === 'yearly') {
d.setMonth(0, 1);
}
return d;
};
const getEndOfPeriod = (date, period) => {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
if (period === 'weekly') {
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? 0 : 7);
d.setDate(diff);
} else if (period === 'monthly') {
d.setMonth(d.getMonth() + 1, 0);
} else if (period === 'yearly') {
d.setMonth(11, 31);
}
return d;
};
const formatDate = (date) => {
return new Date(date).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
// --- Main Component ---
export default function PoweRadFinance() {
const [user, setUser] = useState(null);
const [transactions, setTransactions] = useState([]);
const [assets, setAssets] = useState([]);
const [liabilities, setLiabilities] = useState([]);
const [loading, setLoading] = useState(true);
// View State
const [period, setPeriod] = useState('monthly'); // daily, weekly, monthly, yearly
const [currentDate, setCurrentDate] = useState(new Date());
const [activeTab, setActiveTab] = useState('dashboard'); // dashboard, transactions, balance
// Input Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalType, setModalType] = useState('income'); // income, expense, asset, liability
// --- Auth & Data Fetching ---
useEffect(() => {
const initAuth = async () => {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
};
initAuth();
return onAuthStateChanged(auth, setUser);
}, []);
useEffect(() => {
if (!user) return;
const txRef = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions');
const txQuery = query(txRef);
const unsubTx = onSnapshot(txQuery, (snapshot) => {
const data = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
date: doc.data().date?.toDate() || new Date()
}));
setTransactions(data.sort((a, b) => b.date - a.date));
setLoading(false);
}, (error) => console.error("Tx Error", error));
const bsRef = collection(db, 'artifacts', appId, 'users', user.uid, 'balance_sheet');
const bsQuery = query(bsRef);
const unsubBs = onSnapshot(bsQuery, (snapshot) => {
const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setAssets(data.filter(i => i.type === 'asset'));
setLiabilities(data.filter(i => i.type === 'liability'));
}, (error) => console.error("BS Error", error));
return () => {
unsubTx();
unsubBs();
};
}, [user]);
// --- Derived Data ---
const periodData = useMemo(() => {
const start = getStartOfPeriod(currentDate, period);
const end = getEndOfPeriod(currentDate, period);
return transactions.filter(t => t.date >= start && t.date <= end);
}, [transactions, period, currentDate]);
const totals = useMemo(() => {
const income = periodData.filter(t => t.type === 'income').reduce((acc, curr) => acc + Number(curr.amount), 0);
const expenses = periodData.filter(t => t.type === 'expense').reduce((acc, curr) => acc + Number(curr.amount), 0);
const totalAssets = assets.reduce((acc, curr) => acc + Number(curr.amount), 0);
const totalLiabilities = liabilities.reduce((acc, curr) => acc + Number(curr.amount), 0);
return {
income,
expenses,
netProfit: income - expenses,
assets: totalAssets,
liabilities: totalLiabilities,
netWorth: totalAssets - totalLiabilities
};
}, [periodData, assets, liabilities]);
const breakdown = useMemo(() => {
const categories = {};
periodData.filter(t => t.type === 'expense').forEach(t => {
categories[t.category] = (categories[t.category] || 0) + Number(t.amount);
});
return Object.entries(categories)
.sort(([,a], [,b]) => b - a)
.slice(0, 5); // Top 5
}, [periodData]);
// Group Assets & Liabilities
const groupedBalanceSheet = useMemo(() => {
return {
currentAssets: assets.filter(a => a.classification === 'Current Asset' || !a.classification),
fixedAssets: assets.filter(a => a.classification === 'Fixed Asset'),
currentLiabilities: liabilities.filter(l => l.classification === 'Current Liability' || !l.classification),
longTermLiabilities: liabilities.filter(l => l.classification === 'Long-Term Liability'),
};
}, [assets, liabilities]);
// --- Actions ---
const handleAdd = async (formData) => {
if (!user) return;
const txCollection = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions');
const bsCollection = collection(db, 'artifacts', appId, 'users', user.uid, 'balance_sheet');
try {
if (['income', 'expense'].includes(formData.type)) {
await addDoc(txCollection, {
type: formData.type,
amount: Number(formData.amount),
category: formData.category,
frequency: formData.frequency, // New field
description: formData.description,
date: Timestamp.fromDate(new Date(formData.date)),
createdAt: Timestamp.now()
});
} else {
await addDoc(bsCollection, {
type: formData.type,
name: formData.category,
classification: formData.classification, // New field
amount: Number(formData.amount),
description: formData.description,
});
}
setIsModalOpen(false);
} catch (e) {
console.error("Error adding document: ", e);
}
};
const handleDelete = async (collectionName, id) => {
if (!user) return;
try {
await deleteDoc(doc(db, 'artifacts', appId, 'users', user.uid, collectionName, id));
} catch (e) {
console.error("Delete error", e);
}
};
const shiftDate = (direction) => {
const newDate = new Date(currentDate);
if (period === 'daily') newDate.setDate(newDate.getDate() + direction);
if (period === 'weekly') newDate.setDate(newDate.getDate() + (direction * 7));
if (period === 'monthly') newDate.setMonth(newDate.getMonth() + direction);
if (period === 'yearly') newDate.setFullYear(newDate.getFullYear() + direction);
setCurrentDate(newDate);
};
// --- UI Components ---
const Modal = () => (
Add {modalType}
);
// --- Sub-components for Balance Sheet ---
const BalanceGroup = ({ title, items, total, type, onDelete }) => (
{title}
{formatCurrency(total)}
{items.length === 0 ? (
No items in this category
) : (
)}
);
if (loading) return
Loading PoweRad Finance...
;
return (
{isModalOpen &&
}
{/* Header */}
{/* Navigation Tabs */}
{['dashboard', 'transactions', 'balance'].map((tab) => (
))}
{/* --- DASHBOARD VIEW --- */}
{activeTab === 'dashboard' && (
{/* Period Selector */}
{['daily', 'weekly', 'monthly', 'yearly'].map(p => (
))}
{period === 'daily' ? formatDate(currentDate) :
period === 'yearly' ? currentDate.getFullYear() :
period === 'monthly' ? currentDate.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) :
`Week of ${formatDate(getStartOfPeriod(currentDate, 'weekly'))}`
}
{/* Main Stats Grid */}
{/* Income */}
{formatCurrency(totals.income)}
{/* Expenses */}
{formatCurrency(totals.expenses)}
{/* Net Profit */}
= 0 ? 'border-l-slate-800' : 'border-l-orange-500'}`}>
= 0 ? 'text-slate-800' : 'text-orange-600'}`}>
{formatCurrency(totals.netProfit)}
{totals.income > 0 ? ((totals.netProfit / totals.income) * 100).toFixed(1) : 0}% Margin
{/* Quick Chart / Breakdown */}
Top Expenses
{breakdown.length === 0 ? (
No expenses for this period
) : (
{breakdown.map(([cat, amount], i) => (
{cat}
{formatCurrency(amount)}
))}
)}
Financial Health
Total Assets
{formatCurrency(totals.assets)}
Total Liabilities
{formatCurrency(totals.liabilities)}
Net Worth
= 0 ? 'text-slate-900' : 'text-red-600'}`}>
{formatCurrency(totals.netWorth)}
)}
{/* --- TRANSACTIONS VIEW --- */}
{activeTab === 'transactions' && (
Transaction History
| Date |
Type |
Category |
Description |
Amount |
Action |
{periodData.length === 0 ? (
| No transactions found for this period. |
) : (
periodData.map((t) => (
|
{formatDate(t.date)}
|
{t.frequency && t.frequency !== 'One-Time' ? (
{t.frequency}
) : (
One-Time
)}
|
{t.category}
|
{t.description || '-'} |
{t.type === 'income' ? '+' : '-'}{formatCurrency(t.amount)}
|
|
))
)}
)}
{/* --- BALANCE SHEET VIEW --- */}
{activeTab === 'balance' && (
{/* Assets Column */}
Assets
s + i.amount, 0)}
type="asset"
onDelete={handleDelete}
/>
s + i.amount, 0)}
type="asset"
onDelete={handleDelete}
/>
Total Assets
{formatCurrency(totals.assets)}
{/* Liabilities Column */}
Liabilities
s + i.amount, 0)}
type="liability"
onDelete={handleDelete}
/>
s + i.amount, 0)}
type="liability"
onDelete={handleDelete}
/>
Total Liabilities
{formatCurrency(totals.liabilities)}
)}
{/* Floating Action Button (Mobile) */}
);
}