Add customizable dashboard with drag-and-drop section reordering
- Added Capsule app icon and branding assets - Implemented dashboard customization panel with drag-and-drop interface - Added budget progress tracking and quick actions sections - Enhanced dashboard with clickable summary cards (Assets, Debt, Balance, Envelopes, Transactions) - Increased spacing between sections for better readability - Users can now show/hide and reorder dashboard sections - Fixed account balance normalization for liabilities - Improved analytics charts with visibility controls
This commit is contained in:
1 parent
2dcd7b29fb
commit
4d7f301c61
11 files changed
+1823
-137
No files matched your search
@@ -16,6 +16,9 @@ const geistMono = Geist_Mono({
|
||||
export const metadata: Metadata = {
|
||||
title: "Capsule by NeCloud",
|
||||
description: "A modern envelope budgeting application to help you manage your finances effectively",
|
||||
icons: {
|
||||
icon: "/capsule-icon.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+408
-108
@@ -55,6 +55,14 @@ interface Transaction {
|
||||
|
||||
type ViewMode = 'dashboard' | 'accounts' | 'transactions' | 'envelopes' | 'settings';
|
||||
|
||||
const liabilityTypes: Array<Account['type']> = ['credit_card', 'mortgage', 'loan'];
|
||||
const normalizeBalanceByType = (type: Account['type'], balance: number): number => {
|
||||
if (liabilityTypes.includes(type)) {
|
||||
return -Math.abs(balance || 0);
|
||||
}
|
||||
return balance || 0;
|
||||
};
|
||||
|
||||
export default function BudgetDashboard() {
|
||||
const [currentView, setCurrentView] = useState<ViewMode>('dashboard');
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
@@ -68,6 +76,17 @@ export default function BudgetDashboard() {
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [setupCompleted, setSetupCompleted] = useState(false);
|
||||
|
||||
// Dashboard section order and visibility
|
||||
const [dashboardSections, setDashboardSections] = useState([
|
||||
{ id: 'summaryCards', label: 'Summary Cards', visible: true },
|
||||
{ id: 'charts', label: 'Analytics Charts', visible: true },
|
||||
{ id: 'budgetProgress', label: 'Budget Progress', visible: true },
|
||||
{ id: 'quickActions', label: 'Quick Actions', visible: true },
|
||||
{ id: 'recentTransactions', label: 'Recent Transactions', visible: true },
|
||||
]);
|
||||
const [showDashboardSettings, setShowDashboardSettings] = useState(false);
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
|
||||
// Modal states
|
||||
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
|
||||
const [showCreateEnvelope, setShowCreateEnvelope] = useState(false);
|
||||
@@ -94,7 +113,8 @@ export default function BudgetDashboard() {
|
||||
const accountsWithBaseline = (userData.accounts || []).map((account) => {
|
||||
const accountTransactions = transactionsWithDates.filter((transaction) => transaction.accountId === account.id);
|
||||
const transactionTotal = accountTransactions.reduce((sum, transaction) => sum + transaction.amount, 0);
|
||||
const startingBalance = account.startingBalance ?? account.balance - transactionTotal;
|
||||
const inferredBase = account.startingBalance ?? account.balance - transactionTotal;
|
||||
const startingBalance = normalizeBalanceByType(account.type, inferredBase);
|
||||
return { ...account, startingBalance, balance: startingBalance + transactionTotal };
|
||||
});
|
||||
|
||||
@@ -182,7 +202,7 @@ export default function BudgetDashboard() {
|
||||
return accounts.map((account) => {
|
||||
const accountTransactions = accountTransactionMap.get(account.id) || [];
|
||||
const transactionTotal = accountTransactions.reduce((sum, transaction) => sum + transaction.amount, 0);
|
||||
const baseBalance = account.startingBalance ?? account.balance ?? 0;
|
||||
const baseBalance = normalizeBalanceByType(account.type, account.startingBalance ?? account.balance ?? 0);
|
||||
return { ...account, startingBalance: baseBalance, balance: baseBalance + transactionTotal };
|
||||
});
|
||||
};
|
||||
@@ -335,7 +355,7 @@ export default function BudgetDashboard() {
|
||||
|
||||
const handleSetupComplete = (accounts: Account[], envelopes: Envelope[]) => {
|
||||
const accountsWithBaseline = accounts.map((account) => {
|
||||
const baseBalance = account.startingBalance ?? account.balance;
|
||||
const baseBalance = normalizeBalanceByType(account.type, account.startingBalance ?? account.balance);
|
||||
return { ...account, startingBalance: baseBalance, balance: baseBalance };
|
||||
});
|
||||
setAccounts(accountsWithBaseline);
|
||||
@@ -347,7 +367,7 @@ export default function BudgetDashboard() {
|
||||
|
||||
const handleAccountUpdate = (updatedAccount: Account) => {
|
||||
setAccounts((prevAccounts) => {
|
||||
const baseBalance = updatedAccount.startingBalance ?? updatedAccount.balance;
|
||||
const baseBalance = normalizeBalanceByType(updatedAccount.type, updatedAccount.startingBalance ?? updatedAccount.balance);
|
||||
const nextAccounts = prevAccounts.map((account) =>
|
||||
account.id === updatedAccount.id ? { ...updatedAccount, startingBalance: baseBalance } : account,
|
||||
);
|
||||
@@ -357,7 +377,7 @@ export default function BudgetDashboard() {
|
||||
|
||||
const handleAccountAdd = (newAccount: Account) => {
|
||||
setAccounts((prevAccounts) => {
|
||||
const baseBalance = newAccount.startingBalance ?? newAccount.balance;
|
||||
const baseBalance = normalizeBalanceByType(newAccount.type, newAccount.startingBalance ?? newAccount.balance);
|
||||
const nextAccounts = [...prevAccounts, { ...newAccount, startingBalance: baseBalance }];
|
||||
return calculateAccountBalances(nextAccounts, transactions);
|
||||
});
|
||||
@@ -406,7 +426,14 @@ export default function BudgetDashboard() {
|
||||
<div className="px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium" style={{ backgroundColor: 'rgba(30, 115, 190, 0.1)', color: 'var(--color-primary-blue)' }}>Capsule</span>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/images/capsule-logo.svg"
|
||||
alt="Capsule Logo"
|
||||
className="w-12 h-12"
|
||||
/>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--color-dark-navy)' }}>Capsule</span>
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl font-bold leading-tight" style={{ color: 'var(--color-dark-navy)' }}>
|
||||
Take control of your money with smart envelopes
|
||||
</h1>
|
||||
@@ -441,6 +468,17 @@ export default function BudgetDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow-xl rounded-2xl p-6 border border-gray-100">
|
||||
<div className="flex flex-col items-center justify-center mb-8">
|
||||
<img
|
||||
src="/images/capsule-logo.svg"
|
||||
alt="Capsule Logo"
|
||||
className="w-24 h-24 mb-4"
|
||||
/>
|
||||
<h2 className="text-2xl font-bold text-center" style={{ color: 'var(--color-dark-navy)' }}>
|
||||
Capsule
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: 'var(--color-finance-green)' }}>Smart Envelope Budgeting</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="p-4 rounded-xl border" style={{ backgroundColor: 'rgba(30, 115, 190, 0.05)', borderColor: 'var(--color-cloud-blue)' }}>
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--color-primary-blue)' }}>Total Balance</p>
|
||||
@@ -470,14 +508,21 @@ export default function BudgetDashboard() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/images/capsule-logo.svg"
|
||||
alt="Capsule Logo"
|
||||
className="w-10 h-10"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: 'var(--color-dark-navy)' }}>
|
||||
<span className="logo-text">Capsule</span>
|
||||
</h1>
|
||||
<p className="text-xs font-normal" style={{ color: 'var(--color-neutral-gray)' }}>by NeCloud</p>
|
||||
<p className="text-xs font-medium" style={{ color: 'var(--color-primary-blue)' }}>by NeCloud</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<button
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
@@ -588,119 +633,374 @@ export default function BudgetDashboard() {
|
||||
<>
|
||||
{currentView === 'dashboard' && (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h2>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">$</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Balance</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">
|
||||
${(accounts || []).reduce((sum, acc) => sum + acc.balance, 0).toFixed(2)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">E</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Envelopes</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{envelopes.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">T</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Transactions</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{transactions.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Dashboard</h2>
|
||||
<button
|
||||
onClick={() => setShowDashboardSettings(!showDashboardSettings)}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm font-medium"
|
||||
title="Customize dashboard sections"
|
||||
>
|
||||
⚙️ {showDashboardSettings ? 'Hide' : 'Customize'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Charts */}
|
||||
<DashboardCharts
|
||||
accounts={accounts}
|
||||
envelopes={envelopes}
|
||||
transactions={transactions}
|
||||
/>
|
||||
|
||||
{/* Recent Transactions */}
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Recent Transactions</h3>
|
||||
{/* Dashboard Settings Panel */}
|
||||
{showDashboardSettings && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Arrange Dashboard Sections</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">Drag sections to reorder, or toggle visibility with the checkboxes</p>
|
||||
<div className="space-y-2">
|
||||
{dashboardSections.map((section, index) => (
|
||||
<div
|
||||
key={section.id}
|
||||
draggable
|
||||
onDragStart={() => setDraggedIndex(index)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={() => {
|
||||
if (draggedIndex !== null && draggedIndex !== index) {
|
||||
const newSections = [...dashboardSections];
|
||||
const [removed] = newSections.splice(draggedIndex, 1);
|
||||
newSections.splice(index, 0, removed);
|
||||
setDashboardSections(newSections);
|
||||
setDraggedIndex(null);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-between p-4 bg-white border-2 rounded-lg cursor-move hover:border-blue-400 transition-all ${
|
||||
draggedIndex === index ? 'opacity-50 border-blue-400' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-gray-400 text-xl">⋮⋮</span>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={section.visible}
|
||||
onChange={(e) => {
|
||||
const newSections = [...dashboardSections];
|
||||
newSections[index].visible = e.target.checked;
|
||||
setDashboardSections(newSections);
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">{section.label}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (index > 0) {
|
||||
const newSections = [...dashboardSections];
|
||||
[newSections[index - 1], newSections[index]] = [newSections[index], newSections[index - 1]];
|
||||
setDashboardSections(newSections);
|
||||
}
|
||||
}}
|
||||
disabled={index === 0}
|
||||
className="p-1 text-gray-400 hover:text-gray-700 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Move up"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (index < dashboardSections.length - 1) {
|
||||
const newSections = [...dashboardSections];
|
||||
[newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];
|
||||
setDashboardSections(newSections);
|
||||
}
|
||||
}}
|
||||
disabled={index === dashboardSections.length - 1}
|
||||
className="p-1 text-gray-400 hover:text-gray-700 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Move down"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{transactions.slice(-5).reverse().map((transaction) => (
|
||||
<li key={transaction.id}>
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm font-medium text-gray-900">{transaction.description}</p>
|
||||
<p className="ml-2 text-sm text-gray-500">
|
||||
{transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Dynamic Dashboard Sections */}
|
||||
{dashboardSections.filter(s => s.visible).map((section) => {
|
||||
switch (section.id) {
|
||||
case 'summaryCards':
|
||||
return (
|
||||
<div key={section.id} className="mb-12">
|
||||
{(() => {
|
||||
const assetTypes: Account['type'][] = ['checking', 'savings', 'investment'];
|
||||
const debtTypes: Account['type'][] = ['credit_card', 'mortgage', 'loan'];
|
||||
const totalAssets = accounts.filter(acc => assetTypes.includes(acc.type)).reduce((sum, acc) => sum + acc.balance, 0);
|
||||
const totalDebt = Math.abs(accounts.filter(acc => debtTypes.includes(acc.type)).reduce((sum, acc) => sum + acc.balance, 0));
|
||||
const totalBalance = accounts.reduce((sum, acc) => sum + acc.balance, 0);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||
{/* Total Assets */}
|
||||
<button
|
||||
onClick={() => setCurrentView('accounts')}
|
||||
className="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={`text-sm font-medium ${transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${Math.abs(transaction.amount).toFixed(2)}
|
||||
</p>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Assets</dt>
|
||||
<dd className="text-lg font-medium text-green-600">${totalAssets.toFixed(2)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Total Debt */}
|
||||
<button
|
||||
onClick={() => setCurrentView('accounts')}
|
||||
className="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">−</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Debt</dt>
|
||||
<dd className="text-lg font-medium text-red-600">${totalDebt.toFixed(2)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Total Balance */}
|
||||
<button
|
||||
onClick={() => setCurrentView('accounts')}
|
||||
className="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-md flex items-center justify-center" style={{ backgroundColor: 'var(--color-primary-blue)' }}>
|
||||
<span className="text-white text-sm font-medium">$</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Balance</dt>
|
||||
<dd className={`text-lg font-medium ${totalBalance >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
|
||||
${totalBalance.toFixed(2)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Envelopes */}
|
||||
<button
|
||||
onClick={() => setCurrentView('envelopes')}
|
||||
className="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">E</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Envelopes</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{envelopes.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Transactions */}
|
||||
<button
|
||||
onClick={() => setCurrentView('transactions')}
|
||||
className="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">T</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Transactions</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{transactions.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'charts':
|
||||
return (
|
||||
<div key={section.id} className="mb-12">
|
||||
<DashboardCharts
|
||||
accounts={accounts}
|
||||
envelopes={envelopes}
|
||||
transactions={transactions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'budgetProgress':
|
||||
return (
|
||||
<div key={section.id} className="mb-12">
|
||||
{(() => {
|
||||
const now = new Date();
|
||||
const currentMonth = now.getMonth();
|
||||
const currentYear = now.getFullYear();
|
||||
const monthTransactions = transactions.filter(t => {
|
||||
const tDate = t.date instanceof Date ? t.date : new Date(t.date);
|
||||
return tDate.getMonth() === currentMonth && tDate.getFullYear() === currentYear;
|
||||
});
|
||||
const totalBudget = envelopes.reduce((sum, env) => sum + env.allocated, 0);
|
||||
const totalSpent = monthTransactions.filter(t => t.amount < 0).reduce((sum, t) => sum + Math.abs(t.amount), 0);
|
||||
const budgetProgress = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Budget Progress This Month</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Total Spending</span>
|
||||
<span className="text-sm font-medium text-gray-900">${totalSpent.toFixed(2)} of ${totalBudget.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className={`h-3 rounded-full transition-all ${
|
||||
budgetProgress >= 100 ? 'bg-red-500' : budgetProgress >= 80 ? 'bg-yellow-500' : 'bg-green-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(budgetProgress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-2">{budgetProgress}% of monthly budget used</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'quickActions':
|
||||
return (
|
||||
<div key={section.id} className="mb-12">
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Quick Actions</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => setEditingTransaction(transaction)}
|
||||
className="text-indigo-600 hover:text-indigo-900 text-sm font-medium transition-colors"
|
||||
aria-label={`Edit transaction: ${transaction.description}`}
|
||||
title={`Edit transaction: ${transaction.description}`}
|
||||
onClick={() => setCurrentView('transactions')}
|
||||
className="p-4 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors text-center"
|
||||
>
|
||||
Edit
|
||||
<div className="text-xl mb-1">➕</div>
|
||||
<span className="text-sm font-medium text-gray-900">Add Transaction</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to delete this transaction?')) {
|
||||
handleTransactionDelete(transaction.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-900 text-sm font-medium transition-colors"
|
||||
aria-label={`Delete transaction: ${transaction.description}`}
|
||||
title={`Delete transaction: ${transaction.description}`}
|
||||
onClick={() => setCurrentView('envelopes')}
|
||||
className="p-4 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors text-center"
|
||||
>
|
||||
Delete
|
||||
<div className="text-xl mb-1">📦</div>
|
||||
<span className="text-sm font-medium text-gray-900">Manage Envelopes</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentView('accounts')}
|
||||
className="p-4 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors text-center"
|
||||
>
|
||||
<div className="text-xl mb-1">🏦</div>
|
||||
<span className="text-sm font-medium text-gray-900">Accounts</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowGetPaidModal(true)}
|
||||
className="p-4 bg-orange-50 border border-orange-200 rounded-lg hover:bg-orange-100 transition-colors text-center"
|
||||
>
|
||||
<div className="text-xl mb-1">💰</div>
|
||||
<span className="text-sm font-medium text-gray-900">Record Income</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'recentTransactions':
|
||||
return (
|
||||
<div key={section.id} className="mb-12">
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Recent Transactions</h3>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{transactions.slice(-5).reverse().map((transaction) => (
|
||||
<li key={transaction.id}>
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm font-medium text-gray-900">{transaction.description}</p>
|
||||
<p className="ml-2 text-sm text-gray-500">
|
||||
{transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={`text-sm font-medium ${transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${Math.abs(transaction.amount).toFixed(2)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setEditingTransaction(transaction)}
|
||||
className="text-indigo-600 hover:text-indigo-900 text-sm font-medium transition-colors"
|
||||
aria-label={`Edit transaction: ${transaction.description}`}
|
||||
title={`Edit transaction: ${transaction.description}`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to delete this transaction?')) {
|
||||
handleTransactionDelete(transaction.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-900 text-sm font-medium transition-colors"
|
||||
aria-label={`Delete transaction: ${transaction.description}`}
|
||||
title={`Delete transaction: ${transaction.description}`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{currentView === 'accounts' && (
|
||||
|
||||
@@ -29,6 +29,14 @@ const accountTypes = [
|
||||
{ value: 'loan', label: 'Loan', icon: '📋' },
|
||||
];
|
||||
|
||||
const liabilityTypes: Account['type'][] = ['credit_card', 'mortgage', 'loan'];
|
||||
const normalizeBalanceByType = (type: Account['type'], balance: number): number => {
|
||||
if (liabilityTypes.includes(type)) {
|
||||
return -Math.abs(balance || 0);
|
||||
}
|
||||
return balance || 0;
|
||||
};
|
||||
|
||||
const accountColors = [
|
||||
'bg-blue-500', 'bg-green-500', 'bg-purple-500', 'bg-orange-500',
|
||||
'bg-red-500', 'bg-yellow-500', 'bg-pink-500', 'bg-indigo-500'
|
||||
@@ -63,11 +71,13 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedBalance = normalizeBalanceByType(formData.type as Account['type'], formData.balance || 0);
|
||||
|
||||
const account: Account = {
|
||||
id: editingAccount?.id || `${formData.type}-${Date.now()}`,
|
||||
name: formData.name,
|
||||
type: formData.type as Account['type'],
|
||||
balance: formData.balance || 0,
|
||||
balance: normalizedBalance,
|
||||
institution: formData.institution,
|
||||
accountNumber: formData.accountNumber,
|
||||
color: formData.color || 'bg-blue-500',
|
||||
@@ -124,7 +134,9 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h3 className="text-sm font-medium text-gray-500">Total Balance</h3>
|
||||
<p className="text-2xl font-bold text-green-600">${totalBalance.toFixed(2)}</p>
|
||||
<p className={`text-2xl font-bold ${totalBalance >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${totalBalance.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h3 className="text-sm font-medium text-gray-500">Active Accounts</h3>
|
||||
@@ -167,7 +179,9 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right">
|
||||
<p className="font-semibold text-gray-900">${account.balance.toFixed(2)}</p>
|
||||
<p className={`font-semibold ${account.balance < 0 ? 'text-red-600' : 'text-gray-900'}`}>
|
||||
${account.balance.toFixed(2)}
|
||||
</p>
|
||||
<p className={`text-sm ${account.isActive ? 'text-green-600' : 'text-gray-500'}`}>
|
||||
{account.isActive ? 'Active' : 'Inactive'}
|
||||
</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
interface Account {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'checking' | 'savings' | 'credit_card' | 'mortgage' | 'investment' | 'loan';
|
||||
balance: number;
|
||||
color: string;
|
||||
}
|
||||
@@ -26,9 +27,33 @@ interface DashboardChartsProps {
|
||||
accounts: Account[];
|
||||
envelopes: Envelope[];
|
||||
transactions: Transaction[];
|
||||
visibleCharts?: {
|
||||
incomeExpenses: boolean;
|
||||
budgetAlerts: boolean;
|
||||
assetsDebt: boolean;
|
||||
envelopeSpending: boolean;
|
||||
accountBalances: boolean;
|
||||
dailySpending: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function DashboardCharts({ accounts, envelopes, transactions }: DashboardChartsProps) {
|
||||
const assetTypes: Account['type'][] = ['checking', 'savings', 'investment'];
|
||||
const debtTypes: Account['type'][] = ['credit_card', 'mortgage', 'loan'];
|
||||
|
||||
export default function DashboardCharts({ accounts, envelopes, transactions, visibleCharts = {
|
||||
incomeExpenses: true,
|
||||
budgetAlerts: true,
|
||||
assetsDebt: true,
|
||||
envelopeSpending: true,
|
||||
accountBalances: true,
|
||||
dailySpending: true,
|
||||
} }: DashboardChartsProps) {
|
||||
// Separate accounts into assets and debt
|
||||
const assetAccounts = accounts.filter(acc => assetTypes.includes(acc.type));
|
||||
const debtAccounts = accounts.filter(acc => debtTypes.includes(acc.type));
|
||||
const totalAssets = assetAccounts.reduce((sum, acc) => sum + acc.balance, 0);
|
||||
const totalDebt = Math.abs(debtAccounts.reduce((sum, acc) => sum + acc.balance, 0));
|
||||
|
||||
// Calculate spending by envelope
|
||||
const envelopeSpending = envelopes.map(env => ({
|
||||
name: env.name,
|
||||
@@ -38,6 +63,16 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
percentage: env.allocated > 0 ? (Math.abs(env.spent) / env.allocated) * 100 : 0
|
||||
})).sort((a, b) => b.spent - a.spent);
|
||||
|
||||
// Find envelopes with alerts (overspent or near limit >80%)
|
||||
const envelopeAlerts = envelopes.filter(env => {
|
||||
const percentage = env.allocated > 0 ? (Math.abs(env.spent) / env.allocated) * 100 : 0;
|
||||
return percentage >= 80;
|
||||
}).sort((a, b) => {
|
||||
const aPerc = a.allocated > 0 ? (Math.abs(a.spent) / a.allocated) * 100 : 0;
|
||||
const bPerc = b.allocated > 0 ? (Math.abs(b.spent) / b.allocated) * 100 : 0;
|
||||
return bPerc - aPerc;
|
||||
});
|
||||
|
||||
// Calculate total balance across accounts
|
||||
const totalBalance = accounts.reduce((sum, acc) => sum + acc.balance, 0);
|
||||
const accountBalances = accounts.map(acc => ({
|
||||
@@ -47,8 +82,27 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
percentage: totalBalance > 0 ? (acc.balance / totalBalance) * 100 : 0
|
||||
}));
|
||||
|
||||
// Calculate spending over time (last 30 days)
|
||||
// Calculate income vs expenses (this month)
|
||||
const now = new Date();
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
const monthTransactions = transactions.filter(t => {
|
||||
const txDate = t.date instanceof Date ? t.date : new Date(t.date);
|
||||
return txDate >= monthStart && txDate <= monthEnd;
|
||||
});
|
||||
|
||||
const totalIncome = monthTransactions
|
||||
.filter(t => t.amount > 0 && !t.envelopeId)
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const totalExpenses = monthTransactions
|
||||
.filter(t => t.amount < 0)
|
||||
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
|
||||
|
||||
const netIncome = totalIncome - totalExpenses;
|
||||
|
||||
// Calculate spending over time (last 30 days)
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const recentTransactions = transactions.filter(t => {
|
||||
@@ -72,8 +126,112 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
const maxDailySpending = Math.max(...spendingData.map(([_, amount]) => amount), 1);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Income vs Expenses Card */}
|
||||
{visibleCharts.incomeExpenses && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Income vs Expenses (This Month)</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Income</span>
|
||||
<span className="text-xl font-bold text-green-600">${totalIncome.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Expenses</span>
|
||||
<span className="text-xl font-bold text-red-600">${totalExpenses.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold text-gray-900">Net Income</span>
|
||||
<span className={`text-xl font-bold ${netIncome >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${netIncome.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Budget Alerts Card */}
|
||||
{visibleCharts.budgetAlerts && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Budget Alerts</h3>
|
||||
{envelopeAlerts.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{envelopeAlerts.slice(0, 5).map((env) => {
|
||||
const percentage = env.allocated > 0 ? (Math.abs(env.spent) / env.allocated) * 100 : 0;
|
||||
const isOverspent = percentage > 100;
|
||||
return (
|
||||
<div
|
||||
key={env.id}
|
||||
className={`p-3 rounded-lg border-l-4 ${
|
||||
isOverspent
|
||||
? 'bg-red-50 border-red-500'
|
||||
: 'bg-yellow-50 border-yellow-500'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className={`font-medium ${isOverspent ? 'text-red-900' : 'text-yellow-900'}`}>
|
||||
{env.name}
|
||||
</p>
|
||||
<p className={`text-sm ${isOverspent ? 'text-red-700' : 'text-yellow-700'}`}>
|
||||
{isOverspent
|
||||
? `Over budget by $${(Math.abs(env.spent) - env.allocated).toFixed(2)}`
|
||||
: `${percentage.toFixed(0)}% of budget used`}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`text-sm font-semibold ${isOverspent ? 'text-red-600' : 'text-yellow-600'}`}>
|
||||
{percentage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">✓ All envelopes are within budget</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assets & Debt Summary */}
|
||||
{visibleCharts.assetsDebt && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Assets vs Debt</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="font-medium text-gray-700">Total Assets</span>
|
||||
<span className="font-bold text-green-600">${totalAssets.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 ml-1">
|
||||
{assetAccounts.map(acc => acc.name).join(', ') || 'None'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="font-medium text-gray-700">Total Debt</span>
|
||||
<span className="font-bold text-red-600">${totalDebt.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 ml-1">
|
||||
{debtAccounts.map(acc => acc.name).join(', ') || 'None'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold text-gray-900">Net Worth</span>
|
||||
<span className={`text-lg font-bold ${totalAssets - totalDebt >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${(totalAssets - totalDebt).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Envelope Spending Chart */}
|
||||
{visibleCharts.envelopeSpending && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Envelope Spending</h3>
|
||||
{envelopeSpending.length > 0 ? (
|
||||
@@ -104,8 +262,10 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
<p className="text-gray-500 text-sm">No envelope spending data yet.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Account Balances Chart */}
|
||||
{visibleCharts.accountBalances && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Account Balances</h3>
|
||||
{accountBalances.length > 0 ? (
|
||||
@@ -139,8 +299,10 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
<p className="text-gray-500 text-sm">No account data yet.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Daily Spending Chart (Last 7 Days) */}
|
||||
{visibleCharts.dailySpending && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Daily Spending (Last 7 Days)</h3>
|
||||
{spendingData.length > 0 ? (
|
||||
@@ -172,6 +334,7 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D
|
||||
<p className="text-gray-500 text-sm">No spending data for the last 7 days.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+11
-12
@@ -364,18 +364,11 @@ export default function GetPaid({
|
||||
required
|
||||
>
|
||||
<option value="">Select account...</option>
|
||||
{accounts
|
||||
.filter(
|
||||
(account) =>
|
||||
account.type !== 'credit_card' &&
|
||||
account.type !== 'mortgage' &&
|
||||
account.type !== 'loan',
|
||||
)
|
||||
.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.type.replace('_', ' ')})
|
||||
</option>
|
||||
))}
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.type.replace('_', ' ')})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,6 +495,12 @@ export default function GetPaid({
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccountId && accountEnvelopes.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed p-4 text-sm text-gray-700" style={{ borderColor: 'var(--color-cloud-blue)' }}>
|
||||
No envelopes are linked to this account. Create envelopes for this account to distribute income here.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -99,6 +99,14 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
{ value: 'loan', label: 'Loan', icon: '📋' },
|
||||
];
|
||||
|
||||
const liabilityTypes: Account['type'][] = ['credit_card', 'mortgage', 'loan'];
|
||||
const normalizeBalanceByType = (type: Account['type'], balance: number): number => {
|
||||
if (liabilityTypes.includes(type)) {
|
||||
return -Math.abs(balance || 0);
|
||||
}
|
||||
return balance || 0;
|
||||
};
|
||||
|
||||
const envelopeColors = [
|
||||
'bg-green-500', 'bg-blue-500', 'bg-purple-500', 'bg-orange-500',
|
||||
'bg-red-500', 'bg-yellow-500', 'bg-pink-500', 'bg-indigo-500'
|
||||
@@ -212,11 +220,13 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedBalance = normalizeBalanceByType(newAccount.type as Account['type'], newAccount.balance || 0);
|
||||
|
||||
const account: Account = {
|
||||
id: `${newAccount.type}-${Date.now()}`,
|
||||
name: newAccount.name,
|
||||
type: newAccount.type as Account['type'],
|
||||
balance,
|
||||
balance: normalizedBalance,
|
||||
institution: newAccount.institution,
|
||||
accountNumber: newAccount.accountNumber,
|
||||
color: newAccount.color || 'bg-blue-500',
|
||||
@@ -233,7 +243,12 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
};
|
||||
|
||||
const handleAddEnvelope = () => {
|
||||
if (!newEnvelope.name || !newEnvelope.accountId || newEnvelope.allocated === undefined) return;
|
||||
const allocation = newEnvelope.allocated ?? 0;
|
||||
|
||||
if (!newEnvelope.name || !newEnvelope.accountId || allocation <= 0) {
|
||||
alert('Please enter a budget amount greater than $0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: unique name
|
||||
if (envelopes.some(env => env.name.toLowerCase() === newEnvelope.name!.toLowerCase())) {
|
||||
@@ -244,7 +259,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
const envelope: Envelope = {
|
||||
id: `env-${Date.now()}`,
|
||||
name: newEnvelope.name,
|
||||
allocated: newEnvelope.allocated,
|
||||
allocated: allocation,
|
||||
spent: 0,
|
||||
color: newEnvelope.color || 'bg-green-500',
|
||||
accountId: newEnvelope.accountId,
|
||||
@@ -622,11 +637,21 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
return;
|
||||
}
|
||||
|
||||
const standardDefaults: Record<string, number> = {
|
||||
Rent: 1200,
|
||||
Groceries: 500,
|
||||
Utilities: 250,
|
||||
Savings: 300,
|
||||
Transport: 200,
|
||||
Entertainment: 150,
|
||||
Emergency: 200,
|
||||
};
|
||||
|
||||
const standardNames = ['Rent', 'Groceries', 'Utilities', 'Savings', 'Transport', 'Entertainment', 'Emergency'];
|
||||
const newStandardEnvelopes: Envelope[] = standardNames.map((name, idx) => ({
|
||||
id: `env-standard-${Date.now()}-${idx}`,
|
||||
name,
|
||||
allocated: 0,
|
||||
allocated: standardDefaults[name] ?? 100,
|
||||
spent: 0,
|
||||
color: envelopeColors[idx % envelopeColors.length],
|
||||
accountId: targetAccountId,
|
||||
@@ -714,13 +739,13 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddEnvelope}
|
||||
disabled={!newEnvelope.name || !newEnvelope.accountId}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Add Envelope
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddEnvelope}
|
||||
disabled={!newEnvelope.name || !newEnvelope.accountId || !newEnvelope.allocated || newEnvelope.allocated <= 0}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add Envelope
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user