From ff3e6765744a417de206e0381376b27dd6719475 Mon Sep 17 00:00:00 2001 From: Cedric Poon Date: Wed, 23 Oct 2024 21:23:15 +0800 Subject: [PATCH] feat(editable): transactions + participant --- src/pages/Balance.js | 27 +-- src/pages/Participants.js | 104 ++++++++--- src/pages/Transactions.js | 246 ++++++++++++++++++++------ src/redux/slices/transactionsSlice.js | 68 ++++++- src/redux/store.js | 2 +- 5 files changed, 349 insertions(+), 98 deletions(-) diff --git a/src/pages/Balance.js b/src/pages/Balance.js index 3a92b67..8649c76 100644 --- a/src/pages/Balance.js +++ b/src/pages/Balance.js @@ -6,17 +6,22 @@ function Balance() { const participants = useSelector((state) => state.transactions.participants); const transactions = useSelector((state) => state.transactions.transactions); + const participantMap = participants.reduce((acc, p) => { + acc[p.id] = p.name; + return acc; + }, {}); + const calculateBalances = () => { const balances = {}; participants.forEach((p) => { - balances[p.name] = 0; + balances[p.id] = 0; }); transactions.forEach((t) => { const splitAmount = t.amount / t.participants.length; - t.participants.forEach((p) => { - if (p !== t.paidBy) { - balances[p] -= splitAmount; + t.participants.forEach((pId) => { + if (pId !== t.paidBy) { + balances[pId] -= splitAmount; balances[t.paidBy] += splitAmount; } }); @@ -26,11 +31,11 @@ function Balance() { const debtors = []; const creditors = []; - Object.keys(balances).forEach((person) => { - if (balances[person] < 0) { - debtors.push({ name: person, amount: -balances[person] }); - } else if (balances[person] > 0) { - creditors.push({ name: person, amount: balances[person] }); + Object.keys(balances).forEach((pId) => { + if (balances[pId] < -0.01) { + debtors.push({ id: pId, amount: -balances[pId] }); + } else if (balances[pId] > 0.01) { + creditors.push({ id: pId, amount: balances[pId] }); } }); @@ -44,8 +49,8 @@ function Balance() { const settledAmount = Math.min(remaining, creditor.amount); settlements.push({ - from: debtor.name, - to: creditor.name, + from: participantMap[debtor.id], + to: participantMap[creditor.id], amount: settledAmount.toFixed(2), }); creditor.amount -= settledAmount; diff --git a/src/pages/Participants.js b/src/pages/Participants.js index 8bada42..43698d4 100644 --- a/src/pages/Participants.js +++ b/src/pages/Participants.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { addParticipant } from '../redux/slices/transactionsSlice'; +import { addParticipant, editParticipant, deleteParticipant } from '../redux/slices/transactionsSlice'; import { Container, Typography, @@ -11,48 +11,58 @@ import { List, ListItem, ListItemText, - Snackbar, - Alert, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, } from '@mui/material'; -import Slide from '@mui/material/Slide'; - -// Optional: Slide Transition for Snackbar -function SlideTransition(props) { - return ; -} +import { Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; function Participants() { const participants = useSelector((state) => state.transactions.participants); const dispatch = useDispatch(); const [name, setName] = useState(''); - const [open, setOpen] = useState(false); + + // State for editing + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [currentParticipant, setCurrentParticipant] = useState(null); + const [editName, setEditName] = useState(''); const handleAdd = () => { if (name.trim()) { - dispatch(addParticipant({ id: Date.now(), name })); + dispatch(addParticipant({ id: Date.now(), name: name.trim() })); setName(''); - setOpen(true); } }; + const handleEditOpen = (participant) => { + setCurrentParticipant(participant); + setEditName(participant.name); + setEditDialogOpen(true); + }; + + const handleEditClose = () => { + setEditDialogOpen(false); + setCurrentParticipant(null); + }; + + const handleEditSave = () => { + if (editName.trim()) { + dispatch(editParticipant({ + id: currentParticipant.id, + updatedParticipant: { name: editName.trim() }, + })); + handleEditClose(); + } + }; + + const handleDelete = (id) => { + dispatch(deleteParticipant(id)); + }; + return ( - setOpen(false)} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} // Positioning at top-center - TransitionComponent={SlideTransition} // Adding slide transition - > - setOpen(false)} - severity="success" - sx={{ width: '100%', fontSize: '1.2rem', fontWeight: 'bold' }} // Enhanced styling - variant="filled" // Filled variant for higher visibility - > - Participant added successfully! - - Participants @@ -76,11 +86,47 @@ function Participants() { {participants.map((p) => ( - + + handleEditOpen(p)}> + + + handleDelete(p.id)}> + + + + } + > ))} + + {/* Edit Participant Dialog */} + + Edit Participant + + setEditName(e.target.value)} + sx={{ mt: 2 }} + /> + + + + + + ); } diff --git a/src/pages/Transactions.js b/src/pages/Transactions.js index e9e5332..bf3aea5 100644 --- a/src/pages/Transactions.js +++ b/src/pages/Transactions.js @@ -1,41 +1,59 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { addTransaction } from '../redux/slices/transactionsSlice'; +import { addTransaction, editTransaction, deleteTransaction } from '../redux/slices/transactionsSlice'; import { Container, Typography, TextField, Button, + FormControl, + InputLabel, + Select, + MenuItem, Checkbox, - FormControlLabel, - FormGroup, + ListItemText, Grid, List, ListItem, - ListItemText, Paper, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, } from '@mui/material'; +import { Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; function Transactions() { const participants = useSelector((state) => state.transactions.participants); const transactions = useSelector((state) => state.transactions.transactions); const dispatch = useDispatch(); - + const [description, setDescription] = useState(''); const [amount, setAmount] = useState(''); const [paidBy, setPaidBy] = useState(''); const [participantsInvolved, setParticipantsInvolved] = useState([]); + // State for editing + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [currentTransaction, setCurrentTransaction] = useState(null); + const [editDescription, setEditDescription] = useState(''); + const [editAmount, setEditAmount] = useState(''); + const [editPaidBy, setEditPaidBy] = useState(''); + const [editParticipantsInvolved, setEditParticipantsInvolved] = useState([]); + const handleAddTransaction = () => { if (description && amount && paidBy && participantsInvolved.length > 0) { - dispatch(addTransaction({ - id: Date.now(), - description, - amount: parseFloat(amount), - paidBy, - participants: participantsInvolved, - date: new Date().toISOString(), - })); + dispatch( + addTransaction({ + id: Date.now(), + description, + amount: parseFloat(amount), + paidBy, + participants: participantsInvolved, + date: new Date().toISOString(), + }) + ); // Reset fields setDescription(''); setAmount(''); @@ -44,15 +62,46 @@ function Transactions() { } }; - const handleParticipantSelect = (e) => { - const { value, checked } = e.target; - if (checked) { - setParticipantsInvolved([...participantsInvolved, value]); - } else { - setParticipantsInvolved(participantsInvolved.filter((p) => p !== value)); + const handleDelete = (id) => { + dispatch(deleteTransaction(id)); + }; + + const handleEditOpen = (transaction) => { + setCurrentTransaction(transaction); + setEditDescription(transaction.description); + setEditAmount(transaction.amount.toString()); + setEditPaidBy(transaction.paidBy); + setEditParticipantsInvolved(transaction.participants); + setEditDialogOpen(true); + }; + + const handleEditClose = () => { + setEditDialogOpen(false); + setCurrentTransaction(null); + }; + + const handleEditSave = () => { + if (editDescription && editAmount && editPaidBy && editParticipantsInvolved.length > 0) { + dispatch( + editTransaction({ + id: currentTransaction.id, + updatedTransaction: { + description: editDescription, + amount: parseFloat(editAmount), + paidBy: editPaidBy, + participants: editParticipantsInvolved, + }, + }) + ); + handleEditClose(); } }; + const getParticipantName = (id) => { + const participant = participants.find((p) => p.id === id); + return participant ? participant.name : 'Unknown'; + }; + return ( @@ -80,40 +129,42 @@ function Transactions() { /> - setPaidBy(e.target.value)} - > - - {participants.map((p) => ( - - ))} - + + Paid By + + - Participants Involved - - {participants.map((p) => ( - - } - label={p.name} - /> - ))} - + + Participants Involved + + + + + ); } diff --git a/src/redux/slices/transactionsSlice.js b/src/redux/slices/transactionsSlice.js index 2f811c2..c532175 100644 --- a/src/redux/slices/transactionsSlice.js +++ b/src/redux/slices/transactionsSlice.js @@ -12,12 +12,74 @@ const transactionsSlice = createSlice({ addParticipant(state, action) { state.participants.push(action.payload); }, + editParticipant(state, action) { + const { id, updatedParticipant } = action.payload; + const participantIndex = state.participants.findIndex((p) => p.id === id); + if (participantIndex !== -1) { + const oldName = state.participants[participantIndex].name; + state.participants[participantIndex] = { ...state.participants[participantIndex], ...updatedParticipant }; + + // Update participant name in transactions + state.transactions = state.transactions.map((transaction) => { + const updatedPaidBy = transaction.paidBy === oldName ? updatedParticipant.name : transaction.paidBy; + + const updatedParticipants = transaction.participants.map((participantName) => + participantName === oldName ? updatedParticipant.name : participantName + ); + + return { + ...transaction, + paidBy: updatedPaidBy, + participants: updatedParticipants, + }; + }); + } + }, + deleteParticipant(state, action) { + const participantId = action.payload; + const participant = state.participants.find((p) => p.id === participantId); + if (participant) { + state.participants = state.participants.filter((p) => p.id !== participantId); + + // Remove participant from transactions + state.transactions = state.transactions.map((transaction) => { + const updatedParticipants = transaction.participants.filter((name) => name !== participant.name); + return { + ...transaction, + participants: updatedParticipants, + }; + }); + + // Remove transactions where the participant was the payer or no participants are left + state.transactions = state.transactions.filter( + (transaction) => + transaction.paidBy !== participant.name && transaction.participants.length > 0 + ); + } + }, addTransaction(state, action) { state.transactions.push(action.payload); }, - // Additional reducers for editing and deleting + editTransaction(state, action) { + const { id, updatedTransaction } = action.payload; + const index = state.transactions.findIndex((t) => t.id === id); + if (index !== -1) { + state.transactions[index] = { ...state.transactions[index], ...updatedTransaction }; + } + }, + deleteTransaction(state, action) { + state.transactions = state.transactions.filter((t) => t.id !== action.payload); + }, }, }); -export const { addParticipant, addTransaction } = transactionsSlice.actions; -export default transactionsSlice.reducer; \ No newline at end of file +export const { + addParticipant, + editParticipant, + deleteParticipant, + addTransaction, + editTransaction, + deleteTransaction, +} = transactionsSlice.actions; + +export default transactionsSlice.reducer; diff --git a/src/redux/store.js b/src/redux/store.js index 58b9de9..2435b30 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -17,4 +17,4 @@ store.subscribe(() => { }); }); -export default store; \ No newline at end of file +export default store;