added custom model for contriution screen

This commit is contained in:
Jaanu423
2025-09-24 14:35:28 +05:30
parent b0b85684ea
commit 7f95ae73a0
11 changed files with 514 additions and 164 deletions
+5 -3
View File
@@ -1,8 +1,9 @@
// src/components/JoinedGroupCard.tsx
import React, { FC } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import colors from './colors';
import { useNavigation } from '@react-navigation/native';
// Update the type to match the full data structure
interface JoinedGroup {
@@ -62,9 +63,10 @@ const getStatusStyle = (status: JoinedGroup['status']) => {
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
const progress = (item.currentParticipants / item.maxParticipants) * 100;
const statusStyle = getStatusStyle(item.status);
const navigation = useNavigation<any>()
return (
<View style={styles.card}>
<TouchableOpacity style={styles.card} onPress={()=> navigation.navigate("GroupDetails", {item, color:colors.Settings})}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.groupName}>{item.groupName}</Text>
@@ -103,7 +105,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
{formatDate(item.startDate)} - {formatDate(item.endDate)}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
+2 -1
View File
@@ -10,12 +10,13 @@ const colors = {
Profile: "#2D9CDB", // Deep sky blue
Group: "#27AE60", // Green for freshness
Settings: "#F39C12", // Amber for warmth
// common text + UI colors
textDark: "#212121",
textLight: "#555555",
white: "#FFFFFF",
black: "#000000",
primary: "#a57ae9ff",
primary: "#711ff7ff",
};
export default colors;
+158
View File
@@ -0,0 +1,158 @@
import React, { useRef, useEffect } from "react";
import {
View,
Text,
StyleSheet,
Modal,
TouchableOpacity,
Animated,
Dimensions,
} from "react-native";
import colors from "../colors";
const { height } = Dimensions.get("window");
type CustomModalProps = {
visible: boolean;
onClose: () => void;
title?: string;
children?: React.ReactNode;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
confirmButton?:string
};
const CustomModal: React.FC<CustomModalProps> = ({
visible,
onClose,
title,
children,
confirmText = "Confirm",
cancelText = "Cancel",
onConfirm,
confirmButton = colors.primary
}) => {
const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(slideAnim, {
toValue: height,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
]).start();
}
}, [visible]);
return (
<Modal transparent visible={visible} animationType="none">
{/* Overlay */}
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
{/* Modal Box */}
<Animated.View
style={[
styles.modalBox,
{ transform: [{ translateY: slideAnim }] },
]}
>
{/* Header */}
{
title &&
<Text style={styles.title}>{title}</Text>
}
{/* Body */}
<View style={styles.content}>{children}</View>
{/* Footer Actions */}
<View style={styles.actions}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={onClose}
>
<Text style={styles.cancelText}>{cancelText}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, {backgroundColor:confirmButton}]}
onPress={onConfirm}
>
<Text style={styles.confirmText}>{confirmText}</Text>
</TouchableOpacity>
</View>
</Animated.View>
</Animated.View>
</Modal>
);
};
export default CustomModal;
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "flex-end",
},
modalBox: {
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
elevation: 10,
},
title: {
fontSize: 18,
fontWeight: "700",
color: "#1f2937",
marginBottom: 12,
},
content: {
marginBottom: 20,
},
actions: {
flexDirection: "row",
justifyContent: "flex-end",
marginTop: 10,
},
button: {
paddingVertical: 10,
paddingHorizontal: 18,
borderRadius: 10,
marginLeft: 10,
},
cancelButton: {
backgroundColor: "#e5e7eb",
},
cancelText: {
color: "#374151",
fontWeight: "500",
},
confirmText: {
color: "#fff",
fontWeight: "600",
},
});
+1
View File
@@ -57,5 +57,6 @@ const styles = StyleSheet.create({
headerTitle: {
fontSize: 18,
fontWeight: "600",
textTransform:'uppercase'
},
});
+1 -1
View File
@@ -27,7 +27,7 @@ export type RootStackParamList = {
GroupCreation: undefined;
GroupReview: undefined;
EditGroup: { groupId: string | number };
GroupDetails: { groupId: string | number };
GroupDetails: { item:object };
BecomeOrg: undefined;
YourContacts: undefined;
Transaction: undefined;
+156
View File
@@ -0,0 +1,156 @@
import React, { useEffect, useRef } from "react";
import {
View,
Text,
StyleSheet,
Animated,
Easing,
} from "react-native";
import {Ionicons} from "@react-native-vector-icons/ionicons";
import colors from "../components/colors";
import CustomModal from "../components/common/CustomModal";
interface ContributionProps {
data: any;
visible: boolean;
onClose: () => void;
onConfirm: () => void;
}
const ContributionDetails: React.FC<ContributionProps> = ({
data,
visible,
onClose,
onConfirm,
}) => {
// Animations
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(50)).current;
const scaleAnim = useRef(new Animated.Value(0.9)).current;
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 600,
easing: Easing.out(Easing.exp),
useNativeDriver: true,
}),
Animated.spring(scaleAnim, {
toValue: 1,
friction: 6,
useNativeDriver: true,
}),
]).start();
} else {
fadeAnim.setValue(0);
slideAnim.setValue(50);
scaleAnim.setValue(0.9);
}
}, [visible]);
return (
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm}>
{/* Header */}
<Animated.View
style={[
styles.header,
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] },
]}
>
<Ionicons name="wallet-outline" size={32} color={colors.primary} />
<Text style={styles.headerTitle}>Contribution Summary</Text>
</Animated.View>
{/* Contribution Info Card */}
<Animated.View
style={[
styles.card,
{ transform: [{ scale: scaleAnim }], opacity: fadeAnim },
]}
>
<Text style={styles.label}>Base Amount</Text>
<Text style={styles.value}>₹{data?.baseAmount}</Text>
<Text style={styles.label}>Total Payable</Text>
<Text style={styles.valueHighlight}>₹{data?.totalPayable}</Text>
<Text style={styles.label}>Due Date</Text>
<Text style={styles.value}>{data?.dueDate}</Text>
<Text style={styles.label}>Cycle End</Text>
<Text style={styles.value}>{data?.cycleEnd}</Text>
</Animated.View>
{/* Extra Details */}
<Animated.View
style={[
styles.card,
{ transform: [{ translateY: slideAnim }], opacity: fadeAnim },
]}
>
<Text style={styles.label}>Cycle Number</Text>
<Text style={styles.value}>{data?.cycleNumber}</Text>
<Text style={styles.label}>Late Fee Mode</Text>
<Text style={styles.value}>{data?.lateFeeMode}</Text>
<Text style={styles.label}>Late Fee Value</Text>
<Text style={styles.value}>₹{data?.lateFeeValue}</Text>
<Text style={styles.label}>Effective Late Fee</Text>
<Text style={styles.value}>₹{data?.effectiveLateFee}</Text>
</Animated.View>
</CustomModal>
);
};
export default ContributionDetails;
const styles = StyleSheet.create({
header: {
flexDirection: "row",
alignItems: "center",
marginBottom: 24,
},
headerTitle: {
fontSize: 22,
fontWeight: "700",
color: colors.primary,
marginLeft: 10,
},
card: {
backgroundColor: "#fff",
padding: 16,
borderRadius: 16,
marginBottom: 16,
shadowColor: "#000",
shadowOpacity: 0.08,
shadowOffset: { width: 0, height: 3 },
shadowRadius: 6,
elevation: 3,
},
label: {
fontSize: 13,
color: "#6b7280",
marginTop: 6,
},
value: {
fontSize: 15,
fontWeight: "500",
color: "#374151",
marginBottom: 6,
},
valueHighlight: {
fontSize: 18,
fontWeight: "700",
color: colors.primary,
marginBottom: 10,
},
});
+2
View File
@@ -50,6 +50,8 @@ const Home = () => {
return;
}
const token = credentials.password;
console.log("token : ", token)
const response = await fetch(
PROFILE_URL,
{
+162 -149
View File
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from "react";
import React, { useRef, useEffect, useState } from "react";
import {
StyleSheet,
Text,
@@ -6,108 +6,161 @@ import {
TouchableOpacity,
FlatList,
Animated,
ToastAndroid,
Alert,
} from "react-native";
import { useRoute } from "@react-navigation/native";
import { useRoute, RouteProp } from "@react-navigation/native";
import AppView from "../../../components/AppView";
import colors from "../../../components/colors";
import Ionicons from "@react-native-vector-icons/ionicons";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import Header from "../../../components/common/Header";
import { apiCall } from "../../../utils/ApiHelper";
import { CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
import ContributionScreen from "../../ContributionScreen";
// Define type for a member
interface Member {
id: string;
name: string;
role: "Organizer" | "Member";
contribution: number;
data: any
}
// Define route params type
const GroupDetails = () => {
const route = useRoute();
const { groupId } = route.params as { groupId: string };
const route = useRoute<any>();
const { id, groupName, description, contributionAmount, currentParticipants } = route.params;
const [memberData, setMemberData] = useState<Member[]>([]);
const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = useState(false)
const [payable, setPayable] = useState()
// Group object
const group = {
id: groupId,
name: "Wedding Fund",
collected: 12000,
goal: 20000,
description:
"This group is created to collect contributions for the wedding celebration. Everyone can contribute as per their convenience.",
members: [
{ id: "1", name: "Alice", contribution: 3000, role: "Organizer" },
{ id: "2", name: "Bob", contribution: 2500, role: "Member" },
{ id: "3", name: "Charlie", contribution: 4000, role: "Member" },
{ id: "4", name: "David", contribution: 2500, role: "Member" },
],
id: route.params.item?.id,
name: route.params.item?.groupName,
collected: route.params.item?.contributionAmount,
goal: route.params.item?.contributionAmount * route.params.item?.currentParticipants,
description: route.params.item?.description,
status: route.params.item?.status,
members: memberData,
};
const progress = Math.min(group.collected / group.goal, 1);
// Animation refs
const animatedValues = useRef(group.members.map(() => new Animated.Value(0))).current;
const animatedValues = useRef<Animated.Value[]>([]);
// useEffect(() => {
// animatedValues.current = group.members.map(() => new Animated.Value(0));
// }, [group.members]);
// useEffect(() => {
// const animations = group.members.map((_, i) =>
// Animated.timing(animatedValues.current[i], {
// toValue: 1,
// duration: 400,
// delay: i * 100,
// useNativeDriver: true,
// })
// );
// Animated.stagger(100, animations).start();
// }, [group.members]);
// Fetch group members
useEffect(() => {
const animations = group.members.map((_, i) =>
Animated.timing(animatedValues[i], {
toValue: 1,
duration: 400,
delay: i * 100,
useNativeDriver: true,
})
);
Animated.stagger(100, animations).start();
}, []);
const handleGroupDetails = async () => {
try {
setLoading(true);
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${route?.params?.item?.id}`, "GET") as any;
console.log("Group details response:", res);
setMemberData(res.data);
ToastAndroid.show("Group details fetched successfully!", ToastAndroid.SHORT);
} catch (error: any) {
console.error("Error fetching group details:", error);
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
} finally {
setLoading(false);
}
};
const renderMember = ({ item, index }: { item: typeof group.members[0]; index: number }) => {
return (
<Animated.View
style={[
styles.memberCard,
{
opacity: animatedValues[index],
transform: [
{
translateY: animatedValues[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0], // slide up effect
}),
},
],
},
]}
>
<View style={{ flexDirection: 'row', gap: 10 }}>
<Ionicons
name="person-circle-outline"
size={40}
color={colors.primary}
/>
<View>
<Text style={styles.memberName}>
{item.name} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
</Text>
<Text style={styles.memberDetail}>Contribution: ₹{item.contribution}</Text>
</View>
handleGroupDetails();
}, [id]);
const handleContribution = async () => {
try {
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any
setPayable(res.data)
console.log("contribution res : ", res)
} catch (error) {
console.log("contribution error : ", error)
}
}
// useEffect(()=>{
// if(group?.id)
// handleContribution()
// },[])
const confirmPayment = async () => {
try {
} catch (error) {
}
}
const renderMember = ({ item, index }: { item: any; index: number }) => (
<View
style={[
styles.memberCard,
{
// opacity: animatedValues.current[index],
// transform: [
// {
// translateY: animatedValues.current[index].interpolate({
// inputRange: [0, 1],
// outputRange: [20, 0],
// }),
// },
// ],
borderColor: route.params?.color ? route.params?.color : colors.Home
},
]}
>
<View style={{ flexDirection: "row", gap: 10 }}>
<Ionicons name="person-circle-outline" size={40} color={route.params?.color ? route.params?.color : colors.primary} />
<View>
<Text style={styles.memberName}>
{item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
</Text>
<Text style={styles.memberDetail}>Mobile: {item.mobile}</Text>
</View>
</Animated.View>
);
};
</View>
</View>
);
return (
<AppView>
{/* Group Title */}
<Header title={group.name}/>
{/* <Text style={styles.groupName}>{group.name}</Text> */}
<Header title={group.name} arrowColor={route.params?.color ? route.params?.color : colors.Home} titleColor={route.params?.color ? route.params?.color : colors.Home} />
<Text style={styles.description}>{group.description}</Text>
{/* Progress Bar */}
<View style={styles.progressContainer}>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { flex: progress }]} />
<View style={{ flex: progress, backgroundColor: route.params?.color ? route.params?.color : colors.Home }} />
<View style={{ flex: 1 - progress }} />
</View>
<Text style={styles.amount}>
₹{group.collected} / ₹{group.goal}
</Text>
<Text style={styles.amount}>₹{group.collected} / ₹{group.goal}</Text>
</View>
{/* Stats */}
<View style={styles.statsRow}>
<View style={styles.statBox}>
<Text style={styles.statValue}>{group.members.length}</Text>
<Text style={styles.statValue}>{group.members.length ?? []}</Text>
<Text style={styles.statLabel}>Members</Text>
</View>
<View style={styles.statBox}>
@@ -121,42 +174,53 @@ const GroupDetails = () => {
</View>
{/* Members List */}
<Text style={styles.sectionTitle}>Members</Text>
<View style={styles.statsRow}>
<Text style={styles.sectionTitle}>Members</Text>
<Text style={[styles.sectionTitle, { backgroundColor: route.params?.color ? colors.pastelOrange : colors.pastelPink, color: route.params?.color ? colors.Settings : colors.Home, paddingHorizontal: 5 }]}>{group.status}</Text>
</View>
<FlatList
data={group.members}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => renderMember({ item, index })}
renderItem={renderMember}
contentContainerStyle={styles.membersListContainer}
/>
{/* Actions */}
<View style={styles.actions}>
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity>
</View>
{
group.status === 'ACTIVE' &&
<View style={styles.actions}>
<TouchableOpacity onPress={() => {
setOpenModal(true)
handleContribution()
}}
style={[styles.button, { backgroundColor: route.params?.color ? route.params?.color : colors.Home }]}>
<Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity>
</View>
}
{
openModal && <ContributionScreen data={payable} visible={openModal} onClose={() => setOpenModal(!openModal)} onConfirm={() => {
confirmPayment()
setOpenModal(false)
}} />
}
</AppView>
);
};
export default GroupDetails;
// Styles remain mostly unchanged
const styles = StyleSheet.create({
groupName: {
fontSize: 22,
fontWeight: "700",
color: "#0f172a",
marginBottom: 6,
},
description: {
fontSize: 14,
color: "#475569",
marginBottom: 20,
lineHeight: 20,
},
progressContainer: {
marginBottom: 24,
},
progressContainer: { marginBottom: 24 },
progressBar: {
flexDirection: "row",
height: 8,
@@ -165,18 +229,8 @@ const styles = StyleSheet.create({
overflow: "hidden",
marginBottom: 6,
},
progressFill: {
backgroundColor: colors.Home,
},
amount: {
fontSize: 13,
color: "#64748b",
},
statsRow: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 20,
},
amount: { fontSize: 13, color: "#64748b" },
statsRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 },
statBox: {
flex: 1,
backgroundColor: "#fff",
@@ -190,61 +244,20 @@ const styles = StyleSheet.create({
shadowRadius: 6,
elevation: 2,
},
statValue: {
fontSize: 18,
fontWeight: "700",
color: "#1e293b",
},
statLabel: {
fontSize: 12,
color: "#64748b",
marginTop: 4,
},
sectionTitle: {
fontSize: 16,
fontWeight: "600",
color: "#0f172a",
marginBottom: 12,
},
membersListContainer: {
paddingBottom: 20,
},
statValue: { fontSize: 18, fontWeight: "700", color: "#1e293b" },
statLabel: { fontSize: 12, color: "#64748b", marginTop: 4 },
sectionTitle: { fontSize: 16, fontWeight: "600", color: "#0f172a", marginBottom: 12 },
membersListContainer: { paddingBottom: 20 },
memberCard: {
backgroundColor: "#fff",
padding: 16,
borderRadius: 12,
marginBottom: 10,
// shadowColor: "#000",
// shadowOpacity: 0.05,
// shadowOffset: { width: 0, height: 2 },
// shadowRadius: 4,
// elevation: 2,
borderLeftWidth:5,
borderColor:colors.primary
},
memberName: {
fontSize: 15,
fontWeight: "600",
color: "#1e293b",
marginBottom: 4,
},
memberDetail: {
fontSize: 13,
color: "#475569",
},
actions: {
marginTop: 16,
},
button: {
backgroundColor: colors.Home,
paddingVertical: 14,
borderRadius: 10,
alignItems: "center",
marginBottom: 12,
},
buttonText: {
fontSize: 15,
fontWeight: "600",
color: "#fff",
borderLeftWidth: 5,
},
memberName: { fontSize: 15, fontWeight: "600", color: "#1e293b", marginBottom: 4 },
memberDetail: { fontSize: 13, color: "#475569" },
actions: { marginTop: 16 },
button: { backgroundColor: colors.Home, paddingVertical: 14, borderRadius: 10, alignItems: "center", marginBottom: 12 },
buttonText: { fontSize: 15, fontWeight: "600", color: "#fff" },
});
+24 -8
View File
@@ -6,6 +6,7 @@ import {
FlatList,
TouchableOpacity,
Alert,
RefreshControl,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
@@ -34,13 +35,25 @@ const UserHome = () => {
const navigation = useNavigation<NavigationProp>();
const [groups, setGroups] = useState<Group[]>([]);
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const totalGroups = groups.length;
const totalContribution = groups.reduce(
const activeGroups = groups.filter((g) => g.status === "ACTIVE");
const totalGroups = activeGroups.length;
const totalContribution = activeGroups.reduce(
(sum, g) => sum + g.contributionAmount,
0
);
const onRefresh = async () => {
setRefreshing(true)
await handleGroup()
setRefreshing(false)
}
const handleGroup = async () => {
try {
setLoading(true)
@@ -70,7 +83,7 @@ const UserHome = () => {
} catch (error) {
console.error("❌ Error fetching groups:", error);
Alert.alert("Error", "Could not connect to server");
}finally{
} finally {
setLoading(false)
}
};
@@ -83,11 +96,10 @@ const UserHome = () => {
return (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate("GroupDetails", { groupId: item.id })}
onPress={() => navigation.navigate("GroupDetails", { item })}
>
<Text style={styles.groupName}>{item.groupName}</Text>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.description} numberOfLines={1}>{item.description}</Text>
<View style={styles.statsRow}>
<Ionicons
name="person-circle-outline"
@@ -117,17 +129,19 @@ const UserHome = () => {
</View>
<View style={styles.statBox}>
<Text style={styles.statValue}>₹{totalContribution}</Text>
<Text style={styles.statLabel}>Total Contribution</Text>
<Text style={styles.statLabel}>Cycle Contribution</Text>
</View>
</View>
{/* Groups List */}
<Text style={styles.sectionTitle}>Your Groups</Text>
<FlatList
data={groups}
data={groups.filter((status) => status.status === "ACTIVE")}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={{ paddingBottom: 20 }}
onRefresh={onRefresh}
refreshing={refreshing}
/>
</View>
);
@@ -178,6 +192,8 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 2 },
shadowRadius: 6,
elevation: 2,
borderLeftWidth: 5,
borderColor: colors.Home
},
groupName: {
fontSize: 16,
+2 -2
View File
@@ -13,11 +13,11 @@ export async function apiCall<T>(
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
data?: any,
config?: AxiosRequestConfig
): Promise<T | null> {
): Promise<T | null > {
try {
// Get saved credentials from Keychain
const creds = await Keychain.getGenericPassword();
console.log("hihihi", creds);
// console.log("hihihi", creds);
const headers = {
"Content-Type": "application/json",
...(creds ? { Authorization: `Bearer ${creds.password}` } : {}),
+1
View File
@@ -18,3 +18,4 @@ export const PUT_USER_STATUS = `${BASE_URL}user/profile/status`
export const USER_REQUEST = `${BASE_URL}fund/my-requests`
export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member`
export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`;
export const CONTRIBUTE = `${BASE_URL}payments/groups/`;