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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user