Add Cashflow feature to frontend-web
- Introduced CashflowPage component for managing income and expenses, including detailed summaries and transaction tracking. - Updated App component to include routing for the new CashflowPage. - Enhanced Layout component with navigation for Cashflow. - Created Redux slice for cashflow management, including actions for income sources, expenses, and transactions. - Integrated mock data for initial cashflow setup to facilitate development and testing.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import {BrowserRouter, Routes, Route} from 'react-router-dom';
|
import {BrowserRouter, Routes, Route} from 'react-router-dom';
|
||||||
import Layout from '@/components/Layout';
|
import Layout from '@/components/Layout';
|
||||||
import NetWorthPage from '@/pages/NetWorthPage';
|
import NetWorthPage from '@/pages/NetWorthPage';
|
||||||
|
import CashflowPage from '@/pages/CashflowPage';
|
||||||
import DebtsPage from '@/pages/DebtsPage';
|
import DebtsPage from '@/pages/DebtsPage';
|
||||||
import InvoicesPage from '@/pages/InvoicesPage';
|
import InvoicesPage from '@/pages/InvoicesPage';
|
||||||
import ClientsPage from '@/pages/ClientsPage';
|
import ClientsPage from '@/pages/ClientsPage';
|
||||||
@@ -11,6 +12,7 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route index element={<NetWorthPage />} />
|
<Route index element={<NetWorthPage />} />
|
||||||
|
<Route path="cashflow" element={<CashflowPage />} />
|
||||||
<Route path="debts" element={<DebtsPage />} />
|
<Route path="debts" element={<DebtsPage />} />
|
||||||
<Route path="invoices" element={<InvoicesPage />} />
|
<Route path="invoices" element={<InvoicesPage />} />
|
||||||
<Route path="clients" element={<ClientsPage />} />
|
<Route path="clients" element={<ClientsPage />} />
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {NavLink, Outlet} from 'react-router-dom';
|
import {NavLink, Outlet} from 'react-router-dom';
|
||||||
import {TrendingUp, CreditCard, FileText, Users} from 'lucide-react';
|
import {TrendingUp, CreditCard, FileText, Users, ArrowLeftRight} from 'lucide-react';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{to: '/', label: 'Net Worth', icon: TrendingUp},
|
{to: '/', label: 'Net Worth', icon: TrendingUp},
|
||||||
|
{to: '/cashflow', label: 'Cashflow', icon: ArrowLeftRight},
|
||||||
{to: '/debts', label: 'Debts', icon: CreditCard},
|
{to: '/debts', label: 'Debts', icon: CreditCard},
|
||||||
{to: '/invoices', label: 'Invoices', icon: FileText},
|
{to: '/invoices', label: 'Invoices', icon: FileText},
|
||||||
{to: '/clients', label: 'Clients', icon: Users},
|
{to: '/clients', label: 'Clients', icon: Users},
|
||||||
|
|||||||
214
frontend-web/src/pages/CashflowPage.tsx
Normal file
214
frontend-web/src/pages/CashflowPage.tsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||||
|
import {Button} from '@/components/ui/button';
|
||||||
|
import {useAppSelector} from '@/store';
|
||||||
|
|
||||||
|
export default function CashflowPage() {
|
||||||
|
const {incomeSources, expenses, transactions} = useAppSelector(state => state.cashflow);
|
||||||
|
|
||||||
|
// Calculate monthly totals
|
||||||
|
const getMonthlyAmount = (amount: number, frequency: string) => {
|
||||||
|
switch (frequency) {
|
||||||
|
case 'weekly': return amount * 4.33;
|
||||||
|
case 'biweekly': return amount * 2.17;
|
||||||
|
case 'monthly': return amount;
|
||||||
|
case 'quarterly': return amount / 3;
|
||||||
|
case 'yearly': return amount / 12;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthlyIncome = incomeSources
|
||||||
|
.filter(i => i.isActive)
|
||||||
|
.reduce((sum, i) => sum + getMonthlyAmount(i.amount, i.frequency), 0);
|
||||||
|
|
||||||
|
const monthlyExpenses = expenses
|
||||||
|
.filter(e => e.isActive)
|
||||||
|
.reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0);
|
||||||
|
|
||||||
|
const monthlySavings = monthlyIncome - monthlyExpenses;
|
||||||
|
const savingsRate = monthlyIncome > 0 ? (monthlySavings / monthlyIncome) * 100 : 0;
|
||||||
|
|
||||||
|
const essentialExpenses = expenses.filter(e => e.isActive && e.isEssential);
|
||||||
|
const discretionaryExpenses = expenses.filter(e => e.isActive && !e.isEssential);
|
||||||
|
|
||||||
|
const essentialTotal = essentialExpenses.reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0);
|
||||||
|
const discretionaryTotal = discretionaryExpenses.reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0);
|
||||||
|
|
||||||
|
const fmt = (value: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
|
||||||
|
|
||||||
|
// Group expenses by category
|
||||||
|
const expensesByCategory = expenses.filter(e => e.isActive).reduce((acc, e) => {
|
||||||
|
const monthly = getMonthlyAmount(e.amount, e.frequency);
|
||||||
|
acc[e.category] = (acc[e.category] || 0) + monthly;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
const sortedCategories = Object.entries(expensesByCategory).sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-5 flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-medium">Cashflow</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" size="sm">Add Income</Button>
|
||||||
|
<Button size="sm">Add Expense</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="mb-5 grid grid-cols-4 gap-4">
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-1 pt-3 px-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Monthly Income</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3 px-4">
|
||||||
|
<p className="text-xl font-semibold text-green-400">{fmt(monthlyIncome)}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-1 pt-3 px-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Monthly Expenses</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3 px-4">
|
||||||
|
<p className="text-xl font-semibold">{fmt(monthlyExpenses)}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-1 pt-3 px-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Monthly Savings</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3 px-4">
|
||||||
|
<p className={`text-xl font-semibold ${monthlySavings >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{fmt(monthlySavings)}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-1 pt-3 px-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Savings Rate</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3 px-4">
|
||||||
|
<p className={`text-xl font-semibold ${savingsRate >= 20 ? 'text-green-400' : savingsRate >= 10 ? 'text-yellow-400' : 'text-red-400'}`}>
|
||||||
|
{savingsRate.toFixed(0)}%
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-5 lg:grid-cols-2">
|
||||||
|
{/* Income Sources */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4">
|
||||||
|
<CardTitle className="text-sm font-medium">Income Sources</CardTitle>
|
||||||
|
<span className="text-sm font-semibold text-green-400">{fmt(monthlyIncome)}/mo</span>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-4 pb-3">
|
||||||
|
<div className="divide-y divide-border rounded-md border border-border">
|
||||||
|
{incomeSources.filter(i => i.isActive).map(income => (
|
||||||
|
<div key={income.id} className="flex items-center justify-between px-3 py-2.5">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{income.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{income.category} · {income.frequency}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-medium">{fmt(income.amount)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{fmt(getMonthlyAmount(income.amount, income.frequency))}/mo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Expenses by Category */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4">
|
||||||
|
<CardTitle className="text-sm font-medium">Expenses by Category</CardTitle>
|
||||||
|
<span className="text-sm font-semibold">{fmt(monthlyExpenses)}/mo</span>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-4 pb-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{sortedCategories.map(([category, amount]) => {
|
||||||
|
const pct = (amount / monthlyExpenses) * 100;
|
||||||
|
return (
|
||||||
|
<div key={category} className="flex items-center gap-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between text-sm mb-1">
|
||||||
|
<span>{category}</span>
|
||||||
|
<span className="font-medium">{fmt(amount)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 overflow-hidden rounded-full bg-secondary">
|
||||||
|
<div className="h-full bg-foreground/50" style={{width: `${pct}%`}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground w-8">{pct.toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Essential vs Discretionary */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2 pt-3 px-4">
|
||||||
|
<CardTitle className="text-sm font-medium">Essential vs Discretionary</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-4 pb-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className="rounded-md border border-border p-3 text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">Essential</p>
|
||||||
|
<p className="text-lg font-semibold">{fmt(essentialTotal)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{((essentialTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border p-3 text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">Discretionary</p>
|
||||||
|
<p className="text-lg font-semibold">{fmt(discretionaryTotal)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{((discretionaryTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border rounded-md border border-border max-h-48 overflow-y-auto">
|
||||||
|
{expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).map(expense => (
|
||||||
|
<div key={expense.id} className="flex items-center justify-between px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">{expense.name}</span>
|
||||||
|
{expense.isEssential && (
|
||||||
|
<span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] text-muted-foreground">Essential</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">{fmt(getMonthlyAmount(expense.amount, expense.frequency))}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Transactions */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4">
|
||||||
|
<CardTitle className="text-sm font-medium">Recent Transactions</CardTitle>
|
||||||
|
<Button variant="secondary" size="sm">Add</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-4 pb-3">
|
||||||
|
<div className="divide-y divide-border rounded-md border border-border">
|
||||||
|
{transactions.slice(0, 8).map(tx => (
|
||||||
|
<div key={tx.id} className="flex items-center justify-between px-3 py-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm">{tx.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{tx.category} · {tx.date}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm font-medium ${tx.type === 'income' ? 'text-green-400' : ''}`}>
|
||||||
|
{tx.type === 'income' ? '+' : '-'}{fmt(tx.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,3 +57,18 @@ export {
|
|||||||
updateInvoiceStatus,
|
updateInvoiceStatus,
|
||||||
} from './slices/invoicesSlice';
|
} from './slices/invoicesSlice';
|
||||||
export type {Client, Invoice, InvoiceLineItem, InvoicesState} from './slices/invoicesSlice';
|
export type {Client, Invoice, InvoiceLineItem, InvoicesState} from './slices/invoicesSlice';
|
||||||
|
|
||||||
|
// Cashflow slice
|
||||||
|
export {
|
||||||
|
setLoading as setCashflowLoading,
|
||||||
|
setError as setCashflowError,
|
||||||
|
addIncomeSource,
|
||||||
|
updateIncomeSource,
|
||||||
|
removeIncomeSource,
|
||||||
|
addExpense,
|
||||||
|
updateExpense,
|
||||||
|
removeExpense,
|
||||||
|
addTransaction,
|
||||||
|
removeTransaction,
|
||||||
|
} from './slices/cashflowSlice';
|
||||||
|
export type {IncomeSource, Expense, Transaction, CashflowState} from './slices/cashflowSlice';
|
||||||
|
|||||||
144
frontend-web/src/store/slices/cashflowSlice.ts
Normal file
144
frontend-web/src/store/slices/cashflowSlice.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {createSlice, type PayloadAction} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export interface IncomeSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
frequency: 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'yearly' | 'once';
|
||||||
|
category: string;
|
||||||
|
nextDate: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Expense {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
frequency: 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'yearly' | 'once';
|
||||||
|
category: string;
|
||||||
|
nextDate: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isEssential: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: string;
|
||||||
|
type: 'income' | 'expense';
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
category: string;
|
||||||
|
date: string;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CashflowState {
|
||||||
|
incomeSources: IncomeSource[];
|
||||||
|
expenses: Expense[];
|
||||||
|
transactions: Transaction[];
|
||||||
|
categories: {income: string[]; expense: string[]};
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCategories = {
|
||||||
|
income: ['Salary', 'Freelance', 'Investments', 'Rental', 'Side Business', 'Other'],
|
||||||
|
expense: ['Housing', 'Utilities', 'Transportation', 'Food', 'Insurance', 'Healthcare', 'Subscriptions', 'Entertainment', 'Shopping', 'Savings', 'Other'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockIncomeSources: IncomeSource[] = [
|
||||||
|
{id: 'i1', name: 'Software Engineer Salary', amount: 8500, frequency: 'monthly', category: 'Salary', nextDate: '2024-12-15', isActive: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'i2', name: 'Consulting', amount: 2000, frequency: 'monthly', category: 'Freelance', nextDate: '2024-12-20', isActive: true, createdAt: '2024-03-01'},
|
||||||
|
{id: 'i3', name: 'Dividend Income', amount: 450, frequency: 'quarterly', category: 'Investments', nextDate: '2024-12-31', isActive: true, createdAt: '2024-01-01'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockExpenses: Expense[] = [
|
||||||
|
{id: 'e1', name: 'Mortgage', amount: 2200, frequency: 'monthly', category: 'Housing', nextDate: '2024-12-01', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e2', name: 'Car Payment', amount: 450, frequency: 'monthly', category: 'Transportation', nextDate: '2024-12-05', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e3', name: 'Car Insurance', amount: 180, frequency: 'monthly', category: 'Insurance', nextDate: '2024-12-10', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e4', name: 'Utilities', amount: 250, frequency: 'monthly', category: 'Utilities', nextDate: '2024-12-15', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e5', name: 'Groceries', amount: 600, frequency: 'monthly', category: 'Food', nextDate: '2024-12-01', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e6', name: 'Gym Membership', amount: 50, frequency: 'monthly', category: 'Healthcare', nextDate: '2024-12-01', isActive: true, isEssential: false, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e7', name: 'Netflix', amount: 15, frequency: 'monthly', category: 'Subscriptions', nextDate: '2024-12-08', isActive: true, isEssential: false, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e8', name: 'Spotify', amount: 12, frequency: 'monthly', category: 'Subscriptions', nextDate: '2024-12-12', isActive: true, isEssential: false, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e9', name: 'Health Insurance', amount: 350, frequency: 'monthly', category: 'Insurance', nextDate: '2024-12-01', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
{id: 'e10', name: '401k Contribution', amount: 1500, frequency: 'monthly', category: 'Savings', nextDate: '2024-12-15', isActive: true, isEssential: true, createdAt: '2024-01-01'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockTransactions: Transaction[] = [
|
||||||
|
{id: 't1', type: 'income', name: 'Salary', amount: 8500, category: 'Salary', date: '2024-11-15'},
|
||||||
|
{id: 't2', type: 'expense', name: 'Mortgage', amount: 2200, category: 'Housing', date: '2024-11-01'},
|
||||||
|
{id: 't3', type: 'expense', name: 'Groceries', amount: 145, category: 'Food', date: '2024-11-28'},
|
||||||
|
{id: 't4', type: 'expense', name: 'Gas', amount: 55, category: 'Transportation', date: '2024-11-25'},
|
||||||
|
{id: 't5', type: 'income', name: 'Consulting Payment', amount: 2000, category: 'Freelance', date: '2024-11-20'},
|
||||||
|
{id: 't6', type: 'expense', name: 'Restaurant', amount: 85, category: 'Food', date: '2024-11-22'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialState: CashflowState = {
|
||||||
|
incomeSources: mockIncomeSources,
|
||||||
|
expenses: mockExpenses,
|
||||||
|
transactions: mockTransactions,
|
||||||
|
categories: defaultCategories,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cashflowSlice = createSlice({
|
||||||
|
name: 'cashflow',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isLoading = action.payload;
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
},
|
||||||
|
// Income actions
|
||||||
|
addIncomeSource: (state, action: PayloadAction<IncomeSource>) => {
|
||||||
|
state.incomeSources.push(action.payload);
|
||||||
|
},
|
||||||
|
updateIncomeSource: (state, action: PayloadAction<IncomeSource>) => {
|
||||||
|
const idx = state.incomeSources.findIndex(i => i.id === action.payload.id);
|
||||||
|
if (idx !== -1) state.incomeSources[idx] = action.payload;
|
||||||
|
},
|
||||||
|
removeIncomeSource: (state, action: PayloadAction<string>) => {
|
||||||
|
state.incomeSources = state.incomeSources.filter(i => i.id !== action.payload);
|
||||||
|
},
|
||||||
|
// Expense actions
|
||||||
|
addExpense: (state, action: PayloadAction<Expense>) => {
|
||||||
|
state.expenses.push(action.payload);
|
||||||
|
},
|
||||||
|
updateExpense: (state, action: PayloadAction<Expense>) => {
|
||||||
|
const idx = state.expenses.findIndex(e => e.id === action.payload.id);
|
||||||
|
if (idx !== -1) state.expenses[idx] = action.payload;
|
||||||
|
},
|
||||||
|
removeExpense: (state, action: PayloadAction<string>) => {
|
||||||
|
state.expenses = state.expenses.filter(e => e.id !== action.payload);
|
||||||
|
},
|
||||||
|
// Transaction actions
|
||||||
|
addTransaction: (state, action: PayloadAction<Transaction>) => {
|
||||||
|
state.transactions.push(action.payload);
|
||||||
|
},
|
||||||
|
removeTransaction: (state, action: PayloadAction<string>) => {
|
||||||
|
state.transactions = state.transactions.filter(t => t.id !== action.payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setLoading,
|
||||||
|
setError,
|
||||||
|
addIncomeSource,
|
||||||
|
updateIncomeSource,
|
||||||
|
removeIncomeSource,
|
||||||
|
addExpense,
|
||||||
|
updateExpense,
|
||||||
|
removeExpense,
|
||||||
|
addTransaction,
|
||||||
|
removeTransaction,
|
||||||
|
} = cashflowSlice.actions;
|
||||||
|
|
||||||
|
export default cashflowSlice.reducer;
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ import userReducer from './slices/userSlice';
|
|||||||
import netWorthReducer from './slices/netWorthSlice';
|
import netWorthReducer from './slices/netWorthSlice';
|
||||||
import debtsReducer from './slices/debtsSlice';
|
import debtsReducer from './slices/debtsSlice';
|
||||||
import invoicesReducer from './slices/invoicesSlice';
|
import invoicesReducer from './slices/invoicesSlice';
|
||||||
|
import cashflowReducer from './slices/cashflowSlice';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@@ -10,6 +11,7 @@ export const store = configureStore({
|
|||||||
netWorth: netWorthReducer,
|
netWorth: netWorthReducer,
|
||||||
debts: debtsReducer,
|
debts: debtsReducer,
|
||||||
invoices: invoicesReducer,
|
invoices: invoicesReducer,
|
||||||
|
cashflow: cashflowReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user