diff --git a/capsule-logo.svg b/capsule-logo.svg new file mode 100644 index 0000000..3cdb815 --- /dev/null +++ b/capsule-logo.svg @@ -0,0 +1,1123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/capsule.png b/capsule.png new file mode 100644 index 0000000..e14d6b1 Binary files /dev/null and b/capsule.png differ diff --git a/public/capsule-icon.svg b/public/capsule-icon.svg new file mode 100644 index 0000000..394b7d0 --- /dev/null +++ b/public/capsule-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + $ + \ No newline at end of file diff --git a/public/images/capsule-logo.svg b/public/images/capsule-logo.svg new file mode 100644 index 0000000..021ccba --- /dev/null +++ b/public/images/capsule-logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/capsule-logo1.svg b/public/images/capsule-logo1.svg new file mode 100644 index 0000000..d3b717f --- /dev/null +++ b/public/images/capsule-logo1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + $ + + \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ffc7b0..c35014d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ diff --git a/src/app/page.tsx b/src/app/page.tsx index a4bc25f..b5e740a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -55,6 +55,14 @@ interface Transaction { type ViewMode = 'dashboard' | 'accounts' | 'transactions' | 'envelopes' | 'settings'; +const liabilityTypes: Array = ['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('dashboard'); const [user, setUser] = useState(null); @@ -68,6 +76,17 @@ export default function BudgetDashboard() { const [transactions, setTransactions] = useState([]); 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(null); + // Modal states const [editingTransaction, setEditingTransaction] = useState(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() {
- Capsule +
+ Capsule Logo + Capsule +

Take control of your money with smart envelopes

@@ -441,6 +468,17 @@ export default function BudgetDashboard() {
+
+ Capsule Logo +

+ Capsule +

+

Smart Envelope Budgeting

+

Total Balance

@@ -470,14 +508,21 @@ export default function BudgetDashboard() {
-
+
+
+ Capsule Logo

Capsule

-

by NeCloud

+

by NeCloud

+
- {/* Dashboard Charts */} - - - {/* Recent Transactions */} -
-
-

Recent Transactions

+ {/* Dashboard Settings Panel */} + {showDashboardSettings && ( +
+

Arrange Dashboard Sections

+

Drag sections to reorder, or toggle visibility with the checkboxes

+
+ {dashboardSections.map((section, index) => ( +
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' + }`} + > +
+ ⋮⋮ + +
+
+ + +
+
+ ))} +
-
    - {transactions.slice(-5).reverse().map((transaction) => ( -
  • -
    -
    -
    -

    {transaction.description}

    -

    - {transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()} -

    + )} + + {/* Dynamic Dashboard Sections */} + {dashboardSections.filter(s => s.visible).map((section) => { + switch (section.id) { + case 'summaryCards': + return ( +
    + {(() => { + 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 ( +
    + {/* Total Assets */} + + + {/* Total Debt */} + + + {/* Total Balance */} + + + {/* Envelopes */} + + + {/* Transactions */} + +
    + ); + })()} +
    + ); + + case 'charts': + return ( +
    + +
    + ); + + case 'budgetProgress': + return ( +
    + {(() => { + 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 ( +
    +

    Budget Progress This Month

    +
    +
    +
    + Total Spending + ${totalSpent.toFixed(2)} of ${totalBudget.toFixed(2)} +
    +
    +
    = 100 ? 'bg-red-500' : budgetProgress >= 80 ? 'bg-yellow-500' : 'bg-green-500' + }`} + style={{ width: `${Math.min(budgetProgress, 100)}%` }} + /> +
    +

    {budgetProgress}% of monthly budget used

    +
    +
    +
    + ); + })()} +
    + ); + + case 'quickActions': + return ( +
    +
    +

    Quick Actions

    +
    + +
    -
  • - ))} -
-
+ ); + + case 'recentTransactions': + return ( +
+
+
+

Recent Transactions

+
+
    + {transactions.slice(-5).reverse().map((transaction) => ( +
  • +
    +
    +
    +

    {transaction.description}

    +

    + {transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()} +

    +
    +
    +

    = 0 ? 'text-green-600' : 'text-red-600'}`}> + ${Math.abs(transaction.amount).toFixed(2)} +

    + + +
    +
    +
    +
  • + ))} +
+
+
+ ); + + default: + return null; + } + })}
)} {currentView === 'accounts' && ( diff --git a/src/components/AccountManagement.tsx b/src/components/AccountManagement.tsx index 39ea761..e0ecb70 100644 --- a/src/components/AccountManagement.tsx +++ b/src/components/AccountManagement.tsx @@ -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

Total Balance

-

${totalBalance.toFixed(2)}

+

= 0 ? 'text-green-600' : 'text-red-600'}`}> + ${totalBalance.toFixed(2)} +

Active Accounts

@@ -167,7 +179,9 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
-

${account.balance.toFixed(2)}

+

+ ${account.balance.toFixed(2)} +

{account.isActive ? 'Active' : 'Inactive'}

diff --git a/src/components/DashboardCharts.tsx b/src/components/DashboardCharts.tsx index 6fa6083..19ddc61 100644 --- a/src/components/DashboardCharts.tsx +++ b/src/components/DashboardCharts.tsx @@ -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 ( -
+
+ {/* Income vs Expenses Card */} + {visibleCharts.incomeExpenses && ( +
+

Income vs Expenses (This Month)

+
+
+ Income + ${totalIncome.toFixed(2)} +
+
+ Expenses + ${totalExpenses.toFixed(2)} +
+
+
+ Net Income + = 0 ? 'text-green-600' : 'text-red-600'}`}> + ${netIncome.toFixed(2)} + +
+
+
+
+ )} + + {/* Budget Alerts Card */} + {visibleCharts.budgetAlerts && ( +
+

Budget Alerts

+ {envelopeAlerts.length > 0 ? ( +
+ {envelopeAlerts.slice(0, 5).map((env) => { + const percentage = env.allocated > 0 ? (Math.abs(env.spent) / env.allocated) * 100 : 0; + const isOverspent = percentage > 100; + return ( +
+
+
+

+ {env.name} +

+

+ {isOverspent + ? `Over budget by $${(Math.abs(env.spent) - env.allocated).toFixed(2)}` + : `${percentage.toFixed(0)}% of budget used`} +

+
+ + {percentage.toFixed(0)}% + +
+
+ ); + })} +
+ ) : ( +

✓ All envelopes are within budget

+ )} +
+ )} + + {/* Assets & Debt Summary */} + {visibleCharts.assetsDebt && ( +
+

Assets vs Debt

+
+
+
+ Total Assets + ${totalAssets.toFixed(2)} +
+
+ {assetAccounts.map(acc => acc.name).join(', ') || 'None'} +
+
+
+
+ Total Debt + ${totalDebt.toFixed(2)} +
+
+ {debtAccounts.map(acc => acc.name).join(', ') || 'None'} +
+
+
+
+ Net Worth + = 0 ? 'text-green-600' : 'text-red-600'}`}> + ${(totalAssets - totalDebt).toFixed(2)} + +
+
+
+
+ )} + {/* Envelope Spending Chart */} + {visibleCharts.envelopeSpending && (

Envelope Spending

{envelopeSpending.length > 0 ? ( @@ -104,8 +262,10 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D

No envelope spending data yet.

)}
+ )} {/* Account Balances Chart */} + {visibleCharts.accountBalances && (

Account Balances

{accountBalances.length > 0 ? ( @@ -139,8 +299,10 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D

No account data yet.

)}
+ )} {/* Daily Spending Chart (Last 7 Days) */} + {visibleCharts.dailySpending && (

Daily Spending (Last 7 Days)

{spendingData.length > 0 ? ( @@ -172,6 +334,7 @@ export default function DashboardCharts({ accounts, envelopes, transactions }: D

No spending data for the last 7 days.

)}
+ )}
); } diff --git a/src/components/GetPaid.tsx b/src/components/GetPaid.tsx index d9583a1..d3dd2ae 100644 --- a/src/components/GetPaid.tsx +++ b/src/components/GetPaid.tsx @@ -364,18 +364,11 @@ export default function GetPaid({ required > - {accounts - .filter( - (account) => - account.type !== 'credit_card' && - account.type !== 'mortgage' && - account.type !== 'loan', - ) - .map((account) => ( - - ))} + {accounts.map((account) => ( + + ))}
@@ -502,6 +495,12 @@ export default function GetPaid({ )} + {selectedAccountId && accountEnvelopes.length === 0 && ( +
+ No envelopes are linked to this account. Create envelopes for this account to distribute income here. +
+ )} +
- +