Skip to content

Commit 38f65ba

Browse files
I will fix the UI issues and implement the missing functionality.
1 parent ccf234a commit 38f65ba

File tree

2 files changed

+230
-26
lines changed

2 files changed

+230
-26
lines changed

frontend/screens/AddExpenseScreen.js

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ const CustomCheckbox = ({ label, status, onPress }) => (
3333
</TouchableOpacity>
3434
);
3535

36+
const SplitInputRow = ({
37+
label,
38+
value,
39+
onChangeText,
40+
keyboardType = "numeric",
41+
disabled = false,
42+
isPercentage = false,
43+
}) => (
44+
<View style={styles.splitRow}>
45+
<Text style={styles.splitLabel}>{label}</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>
57+
</View>
58+
);
59+
3660
const AddExpenseScreen = ({ route, navigation }) => {
3761
const { groupId } = route.params;
3862
const { user } = useContext(AuthContext);
@@ -143,8 +167,37 @@ const AddExpenseScreen = ({ route, navigation }) => {
143167
amount: parseFloat(value),
144168
type: "unequal",
145169
}));
170+
} else if (splitMethod === "percentage") {
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) {
176+
throw new Error("Percentages must add up to 100.");
177+
}
178+
splits = Object.entries(percentages)
179+
.filter(([, value]) => parseFloat(value || "0") > 0)
180+
.map(([userId, value]) => ({
181+
userId,
182+
amount: (numericAmount * parseFloat(value)) / 100,
183+
type: "percentage",
184+
}));
185+
} else if (splitMethod === "shares") {
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+
}
193+
splits = Object.entries(shares)
194+
.filter(([, value]) => parseFloat(value || "0") > 0)
195+
.map(([userId, value]) => ({
196+
userId,
197+
amount: (numericAmount * parseFloat(value)) / totalShares,
198+
type: "shares",
199+
}));
146200
}
147-
// ... other split methods logic
148201
const expenseData = {
149202
description,
150203
amount: numericAmount,
@@ -177,7 +230,40 @@ const AddExpenseScreen = ({ route, navigation }) => {
177230
onPress={() => handleMemberSelect(member.userId)}
178231
/>
179232
));
180-
// ... other cases
233+
case "exact":
234+
return members.map((member) => (
235+
<SplitInputRow
236+
key={member.userId}
237+
label={member.user.name}
238+
value={exactAmounts[member.userId]}
239+
onChangeText={(text) =>
240+
setExactAmounts({ ...exactAmounts, [member.userId]: text })
241+
}
242+
/>
243+
));
244+
case "percentage":
245+
return members.map((member) => (
246+
<SplitInputRow
247+
key={member.userId}
248+
label={member.user.name}
249+
value={percentages[member.userId]}
250+
onChangeText={(text) =>
251+
setPercentages({ ...percentages, [member.userId]: text })
252+
}
253+
isPercentage
254+
/>
255+
));
256+
case "shares":
257+
return members.map((member) => (
258+
<SplitInputRow
259+
key={member.userId}
260+
label={member.user.name}
261+
value={shares[member.userId]}
262+
onChangeText={(text) =>
263+
setShares({ ...shares, [member.userId]: text })
264+
}
265+
/>
266+
));
181267
default:
182268
return null;
183269
}
@@ -227,25 +313,25 @@ const AddExpenseScreen = ({ route, navigation }) => {
227313
theme={{ colors: { primary: colors.accent } }}
228314
/>
229315

230-
<Menu
231-
visible={menuVisible}
232-
onDismiss={() => setMenuVisible(false)}
233-
anchor={
234-
<TouchableOpacity
235-
style={styles.menuAnchor}
236-
onPress={() => setMenuVisible(true)}
237-
>
238-
<Text style={styles.menuAnchorText}>
239-
Paid by: {selectedPayerName}
240-
</Text>
241-
<Ionicons
242-
name="chevron-down-outline"
243-
size={24}
244-
color={colors.textSecondary}
245-
/>
246-
</TouchableOpacity>
247-
}
248-
>
316+
<View>
317+
<Text style={styles.label}>Paid by</Text>
318+
<Menu
319+
visible={menuVisible}
320+
onDismiss={() => setMenuVisible(false)}
321+
anchor={
322+
<TouchableOpacity
323+
style={styles.menuAnchor}
324+
onPress={() => setMenuVisible(true)}
325+
>
326+
<Text style={styles.menuAnchorText}>{selectedPayerName}</Text>
327+
<Ionicons
328+
name="chevron-down-outline"
329+
size={24}
330+
color={colors.textSecondary}
331+
/>
332+
</TouchableOpacity>
333+
}
334+
>
249335
{members.map((member) => (
250336
<Menu.Item
251337
key={member.userId}
@@ -257,6 +343,7 @@ const AddExpenseScreen = ({ route, navigation }) => {
257343
/>
258344
))}
259345
</Menu>
346+
</View>
260347

261348
<Text style={styles.splitTitle}>Split Method</Text>
262349
<SegmentedButtons
@@ -341,6 +428,11 @@ const styles = StyleSheet.create({
341428
...typography.body,
342429
color: colors.text,
343430
},
431+
label: {
432+
...typography.body,
433+
color: colors.textSecondary,
434+
marginBottom: spacing.sm,
435+
},
344436
checkboxContainer: {
345437
flexDirection: "row",
346438
alignItems: "center",
@@ -351,6 +443,27 @@ const styles = StyleSheet.create({
351443
color: colors.text,
352444
marginLeft: spacing.md,
353445
},
446+
splitRow: {
447+
flexDirection: "row",
448+
justifyContent: "space-between",
449+
alignItems: "center",
450+
paddingVertical: spacing.sm,
451+
},
452+
splitLabel: {
453+
...typography.body,
454+
color: colors.text,
455+
flex: 1,
456+
},
457+
splitInput: {
458+
width: 100,
459+
textAlign: "right",
460+
backgroundColor: colors.white,
461+
},
462+
percentageSymbol: {
463+
...typography.body,
464+
color: colors.textSecondary,
465+
marginLeft: spacing.sm,
466+
},
354467
});
355468

356469
export default AddExpenseScreen;

frontend/screens/GroupSettingsScreen.js

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,98 @@ const GroupSettingsScreen = ({ route, navigation }) => {
9595
}
9696
};
9797

98-
// ... (onKick, onLeave, onDeleteGroup methods remain the same)
98+
const onKick = (memberId, memberName) => {
99+
Alert.alert(
100+
"Kick Member",
101+
`Are you sure you want to kick ${memberName} from the group?`,
102+
[
103+
{ text: "Cancel", style: "cancel" },
104+
{
105+
text: "Kick",
106+
style: "destructive",
107+
onPress: async () => {
108+
try {
109+
await apiRemoveMember(groupId, memberId);
110+
setMembers(members.filter((m) => m.userId !== memberId));
111+
Alert.alert("Success", `${memberName} has been kicked.`);
112+
} catch (error) {
113+
Alert.alert("Error", "Failed to kick member.");
114+
}
115+
},
116+
},
117+
]
118+
);
119+
};
120+
121+
const onLeave = () => {
122+
Alert.alert(
123+
"Leave Group",
124+
"Are you sure you want to leave this group?",
125+
[
126+
{ text: "Cancel", style: "cancel" },
127+
{
128+
text: "Leave",
129+
style: "destructive",
130+
onPress: async () => {
131+
try {
132+
await apiLeaveGroup(groupId);
133+
navigation.popToTop();
134+
} catch (error) {
135+
Alert.alert("Error", "Failed to leave group.");
136+
}
137+
},
138+
},
139+
]
140+
);
141+
};
142+
143+
const onShare = async () => {
144+
try {
145+
await Share.share({
146+
message: `Join my group on MySplitApp! Use this code: ${group?.joinCode}`,
147+
});
148+
} catch (error) {
149+
Alert.alert("Error", "Failed to share invite code.");
150+
}
151+
};
152+
153+
const pickImage = async () => {
154+
if (!isAdmin) return;
155+
let result = await ImagePicker.launchImageLibraryAsync({
156+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
157+
allowsEditing: true,
158+
aspect: [1, 1],
159+
quality: 1,
160+
base64: true,
161+
});
162+
163+
if (!result.canceled) {
164+
setPickedImage(result.assets[0]);
165+
setIcon(""); // Clear emoji icon when image is picked
166+
}
167+
};
168+
169+
const onDeleteGroup = () => {
170+
Alert.alert(
171+
"Delete Group",
172+
"Are you sure you want to delete this group? This action is irreversible.",
173+
[
174+
{ text: "Cancel", style: "cancel" },
175+
{
176+
text: "Delete",
177+
style: "destructive",
178+
onPress: async () => {
179+
try {
180+
await apiDeleteGroup(groupId);
181+
navigation.popToTop();
182+
} catch (error) {
183+
Alert.alert("Error", "Failed to delete group.");
184+
}
185+
},
186+
},
187+
]
188+
);
189+
};
99190

100191
const renderMemberItem = (m) => {
101192
const isSelf = m.userId === user?._id;
@@ -176,7 +267,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
176267
</View>
177268
<Button
178269
mode="outlined"
179-
onPress={() => {}}
270+
onPress={pickImage}
180271
disabled={!isAdmin}
181272
icon="image"
182273
style={styles.imageButton}
@@ -209,7 +300,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
209300
<Text style={styles.joinCode}>Join Code: {group?.joinCode}</Text>
210301
<Button
211302
mode="outlined"
212-
onPress={() => {}}
303+
onPress={onShare}
213304
icon="share-variant"
214305
labelStyle={{ color: colors.primary }}
215306
>
@@ -225,7 +316,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
225316
<Button
226317
mode="outlined"
227318
textColor={colors.error}
228-
onPress={() => {}}
319+
onPress={onLeave}
229320
icon="logout"
230321
style={{ borderColor: colors.error, marginBottom: spacing.sm }}
231322
>
@@ -235,7 +326,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
235326
<Button
236327
mode="contained"
237328
buttonColor={colors.error}
238-
onPress={() => {}}
329+
onPress={onDeleteGroup}
239330
icon="delete"
240331
>
241332
Delete Group

0 commit comments

Comments
 (0)