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' ;
45import { AuthContext } from '../context/AuthContext' ;
5- import { getGroupMembers , createExpense } from '../api/groups' ;
66
77const 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