@@ -33,137 +33,183 @@ const CustomCheckbox = ({ label, status, onPress }) => (
3333 </ TouchableOpacity >
3434) ;
3535
36- const SplitInputRow = ( { label, value, onChangeText, isPercentage } ) => (
36+ const SplitInputRow = ( {
37+ label,
38+ value,
39+ onChangeText,
40+ keyboardType = "numeric" ,
41+ disabled = false ,
42+ isPercentage = false ,
43+ } ) => (
3744 < View style = { styles . splitRow } >
3845 < Text style = { styles . splitLabel } > { label } </ Text >
39- < TextInput
40- value = { value }
41- onChangeText = { onChangeText }
42- keyboardType = "numeric"
43- style = { styles . splitInput }
44- />
45- { isPercentage && < Text style = { styles . percentageSymbol } > %</ Text > }
46+ < View style = { { flexDirection : "row" , alignItems : "center" } } >
47+ < TextInput
48+ style = { styles . splitInput }
49+ value = { value }
50+ onChangeText = { onChangeText }
51+ keyboardType = { keyboardType }
52+ disabled = { disabled }
53+ theme = { { colors : { primary : colors . accent } } }
54+ />
55+ { isPercentage && < Text style = { styles . percentageSymbol } > %</ Text > }
56+ </ View >
4657 </ View >
4758) ;
4859
4960const AddExpenseScreen = ( { route, navigation } ) => {
5061 const { groupId } = route . params ;
51- const { token, user } = useContext ( AuthContext ) ;
52- const [ members , setMembers ] = useState ( [ ] ) ;
53- const [ isLoading , setIsLoading ] = useState ( true ) ;
62+ const { user } = useContext ( AuthContext ) ;
5463 const [ description , setDescription ] = useState ( "" ) ;
5564 const [ amount , setAmount ] = useState ( "" ) ;
56- const [ payerId , setPayerId ] = useState ( null ) ;
65+ const [ members , setMembers ] = useState ( [ ] ) ;
66+ const [ isLoading , setIsLoading ] = useState ( true ) ;
67+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
5768 const [ splitMethod , setSplitMethod ] = useState ( "equal" ) ;
58- const [ exactAmounts , setExactAmounts ] = useState ( { } ) ;
69+ const [ payerId , setPayerId ] = useState ( null ) ;
70+ const [ menuVisible , setMenuVisible ] = useState ( false ) ;
71+
5972 const [ percentages , setPercentages ] = useState ( { } ) ;
6073 const [ shares , setShares ] = useState ( { } ) ;
74+ const [ exactAmounts , setExactAmounts ] = useState ( { } ) ;
6175 const [ selectedMembers , setSelectedMembers ] = useState ( { } ) ;
62- const [ menuVisible , setMenuVisible ] = useState ( false ) ;
63- const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
6476
6577 useEffect ( ( ) => {
66- const loadMembers = async ( ) => {
78+ const fetchMembers = async ( ) => {
6779 try {
68- const res = await getGroupMembers ( groupId ) ;
69- setMembers ( res . data || [ ] ) ;
70- const initial = { } ;
71- ( res . data || [ ] ) . forEach ( ( m ) => {
72- initial [ m . userId ] = true ; // include by default for equal split
80+ const response = await getGroupMembers ( groupId ) ;
81+ setMembers ( response . data ) ;
82+ const initialShares = { } ;
83+ const initialPercentages = { } ;
84+ const initialExactAmounts = { } ;
85+ const initialSelectedMembers = { } ;
86+ const numMembers = response . data . length ;
87+ const basePercentage = Math . floor ( 100 / numMembers ) ;
88+ const remainder = 100 - basePercentage * numMembers ;
89+
90+ response . data . forEach ( ( member , index ) => {
91+ initialShares [ member . userId ] = "1" ;
92+ let memberPercentage = basePercentage ;
93+ if ( index < remainder ) {
94+ memberPercentage += 1 ;
95+ }
96+ initialPercentages [ member . userId ] = memberPercentage . toString ( ) ;
97+ initialExactAmounts [ member . userId ] = "0.00" ;
98+ initialSelectedMembers [ member . userId ] = true ;
7399 } ) ;
74- setSelectedMembers ( initial ) ;
75- } catch ( e ) {
76- Alert . alert ( "Error" , "Failed to load members" ) ;
100+ setShares ( initialShares ) ;
101+ setPercentages ( initialPercentages ) ;
102+ setExactAmounts ( initialExactAmounts ) ;
103+ setSelectedMembers ( initialSelectedMembers ) ;
104+
105+ const currentUserMember = response . data . find (
106+ ( member ) => member . userId === user . _id
107+ ) ;
108+ if ( currentUserMember ) {
109+ setPayerId ( user . _id ) ;
110+ } else if ( response . data . length > 0 ) {
111+ setPayerId ( response . data [ 0 ] . userId ) ;
112+ }
113+ } catch ( error ) {
114+ console . error ( "Failed to fetch members:" , error ) ;
115+ Alert . alert ( "Error" , "Failed to fetch group members." ) ;
77116 } finally {
78117 setIsLoading ( false ) ;
79118 }
80119 } ;
81- loadMembers ( ) ;
120+ if ( groupId ) {
121+ fetchMembers ( ) ;
122+ }
82123 } , [ groupId ] ) ;
83124
84- const toNumber = ( v ) => {
85- if ( v === null || v === undefined ) return 0 ;
86- const cleaned = String ( v ) . replace ( / [ ^ 0 - 9 . + - ] / g, '' ) . trim ( ) ;
87- if ( cleaned === '' || cleaned === '.' || cleaned === '-' || cleaned === '+' ) return 0 ;
88- const n = parseFloat ( cleaned ) ;
89- return Number . isFinite ( n ) ? n : 0 ;
90- } ;
91-
92125 const handleAddExpense = async ( ) => {
93126 if ( ! description || ! amount || ! payerId ) {
94127 Alert . alert ( "Error" , "Please fill in all required fields." ) ;
95128 return ;
96129 }
97- const numericAmount = toNumber ( amount ) ;
98- if ( numericAmount <= 0 ) {
99- Alert . alert ( "Error" , "Please enter a valid amount greater than 0 ." ) ;
130+ const numericAmount = parseFloat ( amount ) ;
131+ if ( isNaN ( numericAmount ) || numericAmount <= 0 ) {
132+ Alert . alert ( "Error" , "Please enter a valid amount." ) ;
100133 return ;
101134 }
135+
102136 setIsSubmitting ( true ) ;
103137 try {
104138 let splits = [ ] ;
105139 if ( splitMethod === "equal" ) {
106- const includedMembers = Object . keys ( selectedMembers ) . filter ( ( id ) => selectedMembers [ id ] ) ;
140+ const includedMembers = Object . keys ( selectedMembers ) . filter (
141+ ( userId ) => selectedMembers [ userId ]
142+ ) ;
107143 if ( includedMembers . length === 0 )
108144 throw new Error ( "Select at least one member for the split." ) ;
109- const base = Math . floor ( ( numericAmount * 100 ) / includedMembers . length ) ;
110- const remainder = ( numericAmount * 100 ) - base * includedMembers . length ;
111- splits = includedMembers . map ( ( userId , idx ) => ( {
145+ const splitAmount =
146+ Math . round ( ( numericAmount / includedMembers . length ) * 100 ) / 100 ;
147+ const remainder =
148+ Math . round (
149+ ( numericAmount - splitAmount * includedMembers . length ) * 100
150+ ) / 100 ;
151+ splits = includedMembers . map ( ( userId , index ) => ( {
112152 userId,
113- amount : ( base + ( idx === 0 ? remainder : 0 ) ) / 100 ,
153+ amount : index === 0 ? splitAmount + remainder : splitAmount ,
114154 type : "equal" ,
115155 } ) ) ;
116156 } else if ( splitMethod === "exact" ) {
117- const total = Object . values ( exactAmounts ) . reduce ( ( sum , val ) => sum + toNumber ( val ) , 0 ) ;
118- if ( ! Number . isFinite ( total ) || Math . abs ( total - numericAmount ) > 0.01 )
157+ const total = Object . values ( exactAmounts ) . reduce (
158+ ( sum , val ) => sum + parseFloat ( val || "0" ) ,
159+ 0
160+ ) ;
161+ if ( Math . abs ( total - numericAmount ) > 0.01 )
119162 throw new Error ( "Exact amounts must add up to the total." ) ;
120163 splits = Object . entries ( exactAmounts )
121- . map ( ( [ userId , value ] ) => ( { userId, amount : toNumber ( value ) } ) )
122- . filter ( ( s ) => s . amount > 0 )
123- . map ( ( s ) => ( { ...s , type : "unequal" } ) ) ;
124- const diff = Math . round ( ( numericAmount - splits . reduce ( ( a , b ) => a + b . amount , 0 ) ) * 100 ) / 100 ;
125- if ( Math . abs ( diff ) > 0 && splits . length > 0 ) splits [ 0 ] . amount += diff ;
164+ . filter ( ( [ , value ] ) => parseFloat ( value || "0" ) > 0 )
165+ . map ( ( [ userId , value ] ) => ( {
166+ userId,
167+ amount : parseFloat ( value ) ,
168+ type : "unequal" ,
169+ } ) ) ;
126170 } else if ( splitMethod === "percentage" ) {
127- const totalPercentage = Object . values ( percentages ) . reduce ( ( sum , val ) => sum + toNumber ( val ) , 0 ) ;
128- if ( ! Number . isFinite ( totalPercentage ) || Math . abs ( totalPercentage - 100 ) > 0.01 )
171+ const totalPercentage = Object . values ( percentages ) . reduce (
172+ ( sum , val ) => sum + parseFloat ( val || "0" ) ,
173+ 0
174+ ) ;
175+ if ( Math . abs ( totalPercentage - 100 ) > 0.01 ) {
129176 throw new Error ( "Percentages must add up to 100." ) ;
177+ }
130178 splits = Object . entries ( percentages )
131- . map ( ( [ userId , value ] ) => ( { userId, pct : toNumber ( value ) } ) )
132- . filter ( ( s ) => s . pct > 0 )
133- . map ( ( s ) => ( {
134- userId : s . userId ,
135- amount : Math . round ( ( numericAmount * ( s . pct / 100 ) ) * 100 ) / 100 ,
179+ . filter ( ( [ , value ] ) => parseFloat ( value || "0" ) > 0 )
180+ . map ( ( [ userId , value ] ) => ( {
181+ userId,
182+ amount : ( numericAmount * parseFloat ( value ) ) / 100 ,
136183 type : "percentage" ,
137184 } ) ) ;
138- const diff = Math . round ( ( numericAmount - splits . reduce ( ( a , b ) => a + b . amount , 0 ) ) * 100 ) / 100 ;
139- if ( Math . abs ( diff ) > 0 && splits . length > 0 ) splits [ 0 ] . amount += diff ;
140185 } else if ( splitMethod === "shares" ) {
141- const totalShares = Object . values ( shares ) . reduce ( ( sum , val ) => sum + toNumber ( val ) , 0 ) ;
142- if ( ! Number . isFinite ( totalShares ) || totalShares <= 0 ) throw new Error ( "Total shares must be positive." ) ;
186+ const totalShares = Object . values ( shares ) . reduce (
187+ ( sum , val ) => sum + parseFloat ( val || "0" ) ,
188+ 0
189+ ) ;
190+ if ( totalShares === 0 ) {
191+ throw new Error ( "Total shares cannot be zero." ) ;
192+ }
143193 splits = Object . entries ( shares )
144- . map ( ( [ userId , value ] ) => ( { userId, shares : toNumber ( value ) } ) )
145- . filter ( ( s ) => s . shares > 0 )
146- . map ( ( s ) => ( {
147- userId : s . userId ,
148- amount : Math . round ( ( numericAmount * ( s . shares / totalShares ) ) * 100 ) / 100 ,
149- type : "unequal" ,
194+ . filter ( ( [ , value ] ) => parseFloat ( value || "0" ) > 0 )
195+ . map ( ( [ userId , value ] ) => ( {
196+ userId,
197+ amount : ( numericAmount * parseFloat ( value ) ) / totalShares ,
198+ type : "shares" ,
150199 } ) ) ;
151- const diff = Math . round ( ( numericAmount - splits . reduce ( ( a , b ) => a + b . amount , 0 ) ) * 100 ) / 100 ;
152- if ( Math . abs ( diff ) > 0 && splits . length > 0 ) splits [ 0 ] . amount += diff ;
153200 }
154- const splitTypeMap = { exact : "unequal" , shares : "unequal" } ;
155201 const expenseData = {
156202 description,
157203 amount : numericAmount ,
158204 paidBy : payerId ,
159- splitType : splitTypeMap [ splitMethod ] || splitMethod ,
205+ splitType : splitMethod ,
160206 splits,
161207 } ;
162208 await createExpense ( groupId , expenseData ) ;
163209 Alert . alert ( "Success" , "Expense added successfully." ) ;
164210 navigation . goBack ( ) ;
165- } catch ( e ) {
166- Alert . alert ( "Error" , e . message || "Failed to add expense." ) ;
211+ } catch ( error ) {
212+ Alert . alert ( "Error" , error . message || "Failed to create expense." ) ;
167213 } finally {
168214 setIsSubmitting ( false ) ;
169215 }
@@ -190,7 +236,9 @@ const AddExpenseScreen = ({ route, navigation }) => {
190236 key = { member . userId }
191237 label = { member . user . name }
192238 value = { exactAmounts [ member . userId ] }
193- onChangeText = { ( text ) => setExactAmounts ( { ...exactAmounts , [ member . userId ] : text } ) }
239+ onChangeText = { ( text ) =>
240+ setExactAmounts ( { ...exactAmounts , [ member . userId ] : text } )
241+ }
194242 />
195243 ) ) ;
196244 case "percentage" :
@@ -199,7 +247,9 @@ const AddExpenseScreen = ({ route, navigation }) => {
199247 key = { member . userId }
200248 label = { member . user . name }
201249 value = { percentages [ member . userId ] }
202- onChangeText = { ( text ) => setPercentages ( { ...percentages , [ member . userId ] : text } ) }
250+ onChangeText = { ( text ) =>
251+ setPercentages ( { ...percentages , [ member . userId ] : text } )
252+ }
203253 isPercentage
204254 />
205255 ) ) ;
@@ -209,7 +259,9 @@ const AddExpenseScreen = ({ route, navigation }) => {
209259 key = { member . userId }
210260 label = { member . user . name }
211261 value = { shares [ member . userId ] }
212- onChangeText = { ( text ) => setShares ( { ...shares , [ member . userId ] : text } ) }
262+ onChangeText = { ( text ) =>
263+ setShares ( { ...shares , [ member . userId ] : text } )
264+ }
213265 />
214266 ) ) ;
215267 default :
@@ -225,16 +277,24 @@ const AddExpenseScreen = ({ route, navigation }) => {
225277 ) ;
226278 }
227279
228- const selectedPayerName = members . find ( ( m ) => m . userId === payerId ) ?. user . name || "Select Payer" ;
280+ const selectedPayerName =
281+ members . find ( ( m ) => m . userId === payerId ) ?. user . name || "Select Payer" ;
229282
230283 return (
231284 < KeyboardAvoidingView
232285 behavior = { Platform . OS === "ios" ? "padding" : "height" }
233286 style = { styles . container }
234287 >
235288 < Appbar . Header style = { { backgroundColor : colors . primary } } >
236- < Appbar . BackAction onPress = { ( ) => navigation . goBack ( ) } color = { colors . white } />
237- < Appbar . Content title = "Add Expense" color = { colors . white } titleStyle = { { ...typography . h2 } } />
289+ < Appbar . BackAction
290+ onPress = { ( ) => navigation . goBack ( ) }
291+ color = { colors . white }
292+ />
293+ < Appbar . Content
294+ title = "Add Expense"
295+ color = { colors . white }
296+ titleStyle = { { ...typography . h2 } }
297+ />
238298 </ Appbar . Header >
239299 < ScrollView contentContainerStyle = { styles . content } >
240300 < TextInput
@@ -252,30 +312,39 @@ const AddExpenseScreen = ({ route, navigation }) => {
252312 keyboardType = "numeric"
253313 theme = { { colors : { primary : colors . accent } } }
254314 />
315+
255316 < View >
256317 < Text style = { styles . label } > Paid by</ Text >
257318 < Menu
258319 visible = { menuVisible }
259320 onDismiss = { ( ) => setMenuVisible ( false ) }
260321 anchor = {
261- < TouchableOpacity style = { styles . menuAnchor } onPress = { ( ) => setMenuVisible ( true ) } >
322+ < TouchableOpacity
323+ style = { styles . menuAnchor }
324+ onPress = { ( ) => setMenuVisible ( true ) }
325+ >
262326 < Text style = { styles . menuAnchorText } > { selectedPayerName } </ Text >
263- < Ionicons name = "chevron-down-outline" size = { 24 } color = { colors . textSecondary } />
327+ < Ionicons
328+ name = "chevron-down-outline"
329+ size = { 24 }
330+ color = { colors . textSecondary }
331+ />
264332 </ TouchableOpacity >
265333 }
266334 >
267- { members . map ( ( member ) => (
268- < Menu . Item
269- key = { member . userId }
270- onPress = { ( ) => {
271- setPayerId ( member . userId ) ;
272- setMenuVisible ( false ) ;
273- } }
274- title = { member . user . name }
275- />
276- ) ) }
277- </ Menu >
335+ { members . map ( ( member ) => (
336+ < Menu . Item
337+ key = { member . userId }
338+ onPress = { ( ) => {
339+ setPayerId ( member . userId ) ;
340+ setMenuVisible ( false ) ;
341+ } }
342+ title = { member . user . name }
343+ />
344+ ) ) }
345+ </ Menu >
278346 </ View >
347+
279348 < Text style = { styles . splitTitle } > Split Method</ Text >
280349 < SegmentedButtons
281350 value = { splitMethod }
@@ -289,7 +358,9 @@ const AddExpenseScreen = ({ route, navigation }) => {
289358 style = { styles . input }
290359 theme = { { colors : { primary : colors . primary } } }
291360 />
361+
292362 < View style = { styles . splitInputsContainer } > { renderSplitInputs ( ) } </ View >
363+
293364 < Button
294365 mode = "contained"
295366 onPress = { handleAddExpense }
0 commit comments