Skip to content

Commit 3110bef

Browse files
committed
Refactor AddExpenseScreen for improved structure and functionality; enhance split methods and auto-balance percentages
1 parent 4adf84b commit 3110bef

File tree

1 file changed

+181
-61
lines changed

1 file changed

+181
-61
lines changed

frontend/screens/AddExpenseScreen.js

Lines changed: 181 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { useState, useEffect, useContext } from 'react';
2-
import { View, StyleSheet, Alert, ScrollView } from 'react-native';
3-
import { Button, TextInput, ActivityIndicator, Appbar, Title, SegmentedButtons, Text, Paragraph, Checkbox } from 'react-native-paper';
1+
import { useContext, useEffect, useState } from 'react';
2+
import { Alert, KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native';
3+
import { ActivityIndicator, Appbar, Button, Checkbox, Menu, Paragraph, SegmentedButtons, Text, TextInput, Title } from 'react-native-paper';
4+
import { createExpense, getGroupMembers } from '../api/groups';
45
import { AuthContext } from '../context/AuthContext';
5-
import { getGroupMembers, createExpense } from '../api/groups';
66

77
const AddExpenseScreen = ({ route, navigation }) => {
88
const { groupId } = route.params;
@@ -13,6 +13,8 @@ const AddExpenseScreen = ({ route, navigation }) => {
1313
const [isLoading, setIsLoading] = useState(true);
1414
const [isSubmitting, setIsSubmitting] = useState(false);
1515
const [splitMethod, setSplitMethod] = useState('equal');
16+
const [payerId, setPayerId] = useState(user._id);
17+
const [menuVisible, setMenuVisible] = useState(false);
1618

1719
// State for different split methods
1820
const [percentages, setPercentages] = useState({});
@@ -31,9 +33,17 @@ const AddExpenseScreen = ({ route, navigation }) => {
3133
const initialExactAmounts = {};
3234
const initialSelectedMembers = {};
3335
const numMembers = response.data.length;
34-
response.data.forEach(member => {
36+
response.data.forEach((member, index) => {
3537
initialShares[member.userId] = '1';
36-
initialPercentages[member.userId] = numMembers > 0 ? (100 / numMembers).toFixed(2) : '0';
38+
// Better percentage distribution that sums to 100
39+
if (index < numMembers - 1) {
40+
const percentage = Math.floor((100 / numMembers) * 100) / 100;
41+
initialPercentages[member.userId] = percentage.toString();
42+
} else {
43+
// Give remainder to last member to ensure total is 100
44+
const otherPercentages = Object.values(initialPercentages).reduce((sum, val) => sum + parseFloat(val || '0'), 0);
45+
initialPercentages[member.userId] = (100 - otherPercentages).toString();
46+
}
3747
initialExactAmounts[member.userId] = '0.00';
3848
initialSelectedMembers[member.userId] = true; // Select all by default
3949
});
@@ -76,40 +86,61 @@ const AddExpenseScreen = ({ route, navigation }) => {
7686
if (includedMembers.length === 0) {
7787
throw new Error('You must select at least one member for the split.');
7888
}
79-
const splitAmount = numericAmount / includedMembers.length;
80-
splits = includedMembers.map(userId => ({ userId, amount: splitAmount }));
89+
const splitAmount = Math.round((numericAmount / includedMembers.length) * 100) / 100;
90+
splits = includedMembers.map(userId => ({
91+
userId,
92+
amount: splitAmount,
93+
type: 'equal'
94+
}));
95+
splitType = 'equal';
8196
} else if (splitMethod === 'exact') {
8297
const total = Object.values(exactAmounts).reduce((sum, val) => sum + parseFloat(val || '0'), 0);
8398
if (Math.abs(total - numericAmount) > 0.01) {
84-
throw new Error(`The exact amounts must add up to ${numericAmount}.`);
99+
throw new Error(`The exact amounts must add up to ${numericAmount.toFixed(2)}. Current total: ${total.toFixed(2)}`);
85100
}
86-
splits = Object.entries(exactAmounts).map(([userId, value]) => ({ userId, amount: parseFloat(value) }));
101+
splits = Object.entries(exactAmounts)
102+
.filter(([userId, value]) => parseFloat(value || '0') > 0)
103+
.map(([userId, value]) => ({
104+
userId,
105+
amount: Math.round(parseFloat(value) * 100) / 100,
106+
type: 'unequal'
107+
}));
108+
splitType = 'unequal'; // Backend uses 'unequal' for exact amounts
87109
} else if (splitMethod === 'percentage') {
88110
const total = Object.values(percentages).reduce((sum, val) => sum + parseFloat(val || '0'), 0);
89111
if (Math.abs(total - 100) > 0.01) {
90-
throw new Error('Percentages must add up to 100%.');
112+
throw new Error(`Percentages must add up to 100%. Current total: ${total.toFixed(2)}%`);
91113
}
92-
splits = Object.entries(percentages).map(([userId, value]) => ({
93-
userId,
94-
amount: numericAmount * (parseFloat(value) / 100),
95-
}));
114+
splits = Object.entries(percentages)
115+
.filter(([userId, value]) => parseFloat(value || '0') > 0)
116+
.map(([userId, value]) => ({
117+
userId,
118+
amount: Math.round((numericAmount * (parseFloat(value) / 100)) * 100) / 100,
119+
type: 'percentage'
120+
}));
121+
splitType = 'percentage';
96122
} else if (splitMethod === 'shares') {
97123
const totalShares = Object.values(shares).reduce((sum, val) => sum + parseInt(val || '0', 10), 0);
98124
if (totalShares === 0) {
99125
throw new Error('Total shares cannot be zero.');
100126
}
101-
splits = Object.entries(shares).map(([userId, value]) => ({
102-
userId,
103-
amount: numericAmount * (parseInt(value, 10) / totalShares),
104-
}));
127+
splits = Object.entries(shares)
128+
.filter(([userId, value]) => parseInt(value || '0', 10) > 0)
129+
.map(([userId, value]) => ({
130+
userId,
131+
amount: Math.round((numericAmount * (parseInt(value, 10) / totalShares)) * 100) / 100,
132+
type: 'unequal'
133+
}));
134+
splitType = 'unequal'; // Backend uses 'unequal' for shares
105135
}
106136

107137
expenseData = {
108138
description,
109139
amount: numericAmount,
110-
paidBy: user._id,
140+
paidBy: payerId, // Use the selected payer
111141
splitType,
112142
splits,
143+
tags: []
113144
};
114145

115146
await createExpense(token, groupId, expenseData);
@@ -126,9 +157,35 @@ const AddExpenseScreen = ({ route, navigation }) => {
126157
setSelectedMembers(prev => ({...prev, [userId]: !prev[userId]}));
127158
};
128159

160+
// Helper function to auto-balance percentages
161+
const balancePercentages = (updatedPercentages) => {
162+
const total = Object.values(updatedPercentages).reduce((sum, val) => sum + parseFloat(val || '0'), 0);
163+
const memberIds = Object.keys(updatedPercentages);
164+
165+
if (total !== 100 && memberIds.length > 1) {
166+
// Find the last non-zero percentage to adjust
167+
const lastMemberId = memberIds[memberIds.length - 1];
168+
const otherTotal = Object.entries(updatedPercentages)
169+
.filter(([id]) => id !== lastMemberId)
170+
.reduce((sum, [, val]) => sum + parseFloat(val || '0'), 0);
171+
172+
const newValue = Math.max(0, 100 - otherTotal);
173+
updatedPercentages[lastMemberId] = newValue.toFixed(2);
174+
}
175+
176+
return updatedPercentages;
177+
};
178+
129179
const renderSplitInputs = () => {
130180
const handleSplitChange = (setter, userId, value) => {
131-
setter(prev => ({ ...prev, [userId]: value }));
181+
if (setter === setPercentages) {
182+
// Auto-balance percentages when one changes
183+
const updatedPercentages = { ...percentages, [userId]: value };
184+
const balanced = balancePercentages(updatedPercentages);
185+
setter(balanced);
186+
} else {
187+
setter(prev => ({ ...prev, [userId]: value }));
188+
}
132189
};
133190

134191
switch (splitMethod) {
@@ -187,55 +244,104 @@ const AddExpenseScreen = ({ route, navigation }) => {
187244
);
188245
}
189246

247+
const selectedPayerName = members.find(m => m.userId === payerId)?.user.name || 'Select Payer';
248+
190249
return (
191250
<View style={styles.container}>
192251
<Appbar.Header>
193252
<Appbar.BackAction onPress={() => navigation.goBack()} />
194253
<Appbar.Content title="Add Expense" />
195254
</Appbar.Header>
196-
<ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 32 }}>
197-
<Title>New Expense Details</Title>
198-
<TextInput
199-
label="Description"
200-
value={description}
201-
onChangeText={setDescription}
202-
style={styles.input}
203-
/>
204-
<TextInput
205-
label="Amount"
206-
value={amount}
207-
onChangeText={setAmount}
208-
style={styles.input}
209-
keyboardType="numeric"
210-
/>
255+
<KeyboardAvoidingView
256+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
257+
style={styles.keyboardAvoidingView}
258+
>
259+
<View style={styles.content}>
260+
<Title>New Expense Details</Title>
261+
<TextInput
262+
label="Description"
263+
value={description}
264+
onChangeText={setDescription}
265+
style={styles.input}
266+
/>
267+
<TextInput
268+
label="Amount"
269+
value={amount}
270+
onChangeText={setAmount}
271+
style={styles.input}
272+
keyboardType="numeric"
273+
/>
211274

212-
<Title style={styles.splitTitle}>Split Method</Title>
213-
<SegmentedButtons
214-
value={splitMethod}
215-
onValueChange={setSplitMethod}
216-
buttons={[
217-
{ value: 'equal', label: 'Equally' },
218-
{ value: 'exact', label: 'Exact' },
219-
{ value: 'percentage', label: '%' },
220-
{ value: 'shares', label: 'Shares' },
221-
]}
222-
style={styles.input}
223-
/>
275+
<Menu
276+
visible={menuVisible}
277+
onDismiss={() => setMenuVisible(false)}
278+
anchor={<Button onPress={() => setMenuVisible(true)}>Paid by: {selectedPayerName}</Button>}
279+
>
280+
{members.map(member => (
281+
<Menu.Item
282+
key={member.userId}
283+
onPress={() => {
284+
setPayerId(member.userId);
285+
setMenuVisible(false);
286+
}}
287+
title={member.user.name}
288+
/>
289+
))}
290+
</Menu>
224291

225-
<View style={styles.splitInputsContainer}>
226-
{renderSplitInputs()}
227-
</View>
292+
<Title style={styles.splitTitle}>Split Method</Title>
293+
<SegmentedButtons
294+
value={splitMethod}
295+
onValueChange={setSplitMethod}
296+
buttons={[
297+
{ value: 'equal', label: 'Equally' },
298+
{ value: 'exact', label: 'Exact' },
299+
{ value: 'percentage', label: '%' },
300+
{ value: 'shares', label: 'Shares' },
301+
]}
302+
style={styles.input}
303+
/>
228304

229-
<Button
230-
mode="contained"
231-
onPress={handleAddExpense}
232-
style={styles.button}
233-
loading={isSubmitting}
234-
disabled={isSubmitting}
235-
>
236-
Add Expense
237-
</Button>
238-
</ScrollView>
305+
{splitMethod === 'equal' && (
306+
<Paragraph style={styles.helperText}>Select members to split the expense equally among them.</Paragraph>
307+
)}
308+
{splitMethod === 'exact' && (
309+
<Paragraph style={styles.helperText}>
310+
Enter exact amounts for each member. Total must equal ${amount || '0'}.
311+
{amount && (
312+
<Text style={styles.totalText}>
313+
{' '}Current total: ${Object.values(exactAmounts).reduce((sum, val) => sum + parseFloat(val || '0'), 0).toFixed(2)}
314+
</Text>
315+
)}
316+
</Paragraph>
317+
)}
318+
{splitMethod === 'percentage' && (
319+
<Paragraph style={styles.helperText}>
320+
Enter percentages for each member. Total must equal 100%.
321+
<Text style={styles.totalText}>
322+
{' '}Current total: {Object.values(percentages).reduce((sum, val) => sum + parseFloat(val || '0'), 0).toFixed(2)}%
323+
</Text>
324+
</Paragraph>
325+
)}
326+
{splitMethod === 'shares' && (
327+
<Paragraph style={styles.helperText}>Enter shares for each member. Higher shares = larger portion of the expense.</Paragraph>
328+
)}
329+
330+
<View style={styles.splitInputsContainer}>
331+
{renderSplitInputs()}
332+
</View>
333+
334+
<Button
335+
mode="contained"
336+
onPress={handleAddExpense}
337+
style={styles.button}
338+
loading={isSubmitting}
339+
disabled={isSubmitting}
340+
>
341+
Add Expense
342+
</Button>
343+
</View>
344+
</KeyboardAvoidingView>
239345
</View>
240346
);
241347
};
@@ -244,8 +350,13 @@ const styles = StyleSheet.create({
244350
container: {
245351
flex: 1,
246352
},
353+
keyboardAvoidingView: {
354+
flex: 1,
355+
},
247356
content: {
357+
flex: 1,
248358
padding: 16,
359+
paddingBottom: 32,
249360
},
250361
loaderContainer: {
251362
flex: 1,
@@ -267,6 +378,15 @@ const styles = StyleSheet.create({
267378
},
268379
splitInput: {
269380
marginBottom: 8,
381+
},
382+
helperText: {
383+
fontSize: 12,
384+
marginBottom: 8,
385+
opacity: 0.7,
386+
},
387+
totalText: {
388+
fontWeight: 'bold',
389+
opacity: 1,
270390
}
271391
});
272392

0 commit comments

Comments
 (0)