Files
circle-collect/src/screens/home/user-home/GroupDetails.tsx
T
2025-10-01 18:52:51 +05:30

374 lines
12 KiB
TypeScript

import React, { useEffect, useState, useCallback } from "react";
import {
StyleSheet,
Text,
View,
TouchableOpacity,
ToastAndroid,
RefreshControl,
ScrollView,
Modal,
} from "react-native";
import { useRoute } from "@react-navigation/native";
import AppView from "../../../components/AppView";
import colors from "../../../components/colors";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import Header from "../../../components/common/Header";
import { apiCall } from "../../../utils/ApiHelper";
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
import ContributionScreen from "../../ContributionScreen";
import CycleDashboard from "./CycleDashboard";
const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }:any) => {
const [open, setOpen] = useState(false);
const selectedLabel = items.find((item:any) => item.value === selectedValue)?.label || placeholder;
return (
<View style={styles.dropdownWrapper}>
<TouchableOpacity onPress={() => setOpen(true)} style={styles.dropdownHeader}>
<Text style={styles.headerText}>{selectedLabel}</Text>
<Ionicons name='chevron-down' size={22} color={colors.black} />
</TouchableOpacity>
<Modal
transparent
animationType="fade"
visible={open}
onRequestClose={() => setOpen(false)}
>
<TouchableOpacity style={styles.modalOverlay} onPress={() => setOpen(false)}>
<View style={styles.dropdownListContainer}>
<ScrollView>
{items.map(item => (
<TouchableOpacity
key={item.value}
onPress={() => {
onValueChange(item.value);
setOpen(false);
}}
style={[
styles.dropdownItem,
item.value === selectedValue && styles.selectedItem
]}
>
<Text style={styles.itemText}>{item.label}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
</TouchableOpacity>
</Modal>
</View>
);
};
const GroupDetails = () => {
const route = useRoute<any>();
const [memberData, setMemberData] = useState([]);
const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = useState(false);
const [payable, setPayable] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const [cycleDashboardData, setCycleDashboardData] = useState<any>();
const [selectedCycleNumber, setSelectedCycleNumber] = useState(null);
const [cycleItems, setCycleItems] = useState([]);
const group = {
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 getCycleDashboard = async () => {
try {
const response = await apiCall(`${BASE_URL}payments/groups/${group.id}/cycle-dashboard`, "GET") as any;
const responseData = response.data;
setCycleDashboardData(responseData);
if (responseData?.cycles) {
const items = responseData.cycles.map(cycle => ({
label: `Cycle ${cycle.cycleNumber}`,
value: cycle.cycleNumber
}));
setCycleItems(items);
if (responseData.currentCycle) {
setSelectedCycleNumber(responseData.currentCycle);
}
}
} catch (error) {
ToastAndroid.show("Failed to fetch cycle data", ToastAndroid.SHORT);
}
};
const fetchGroupDetails = async () => {
try {
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id}`, "GET") as any;
console.log("Group Members Response: ", res);
setMemberData(res.data);
} catch (error: any) {
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
}
};
const handleContribution = async () => {
try {
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any;
setPayable(res.data);
} catch (error) {
console.log("Error in handleContribution: ", error);
}
};
const fetchAllData = useCallback(async () => {
setLoading(true);
await Promise.all([
fetchGroupDetails(),
handleContribution(),
getCycleDashboard()
]);
setLoading(false);
}, [group.id]);
useEffect(() => {
if (group.id) {
fetchAllData();
}
}, [group.id, fetchAllData]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchAllData();
setRefreshing(false);
}, [fetchAllData]);
const confirmPayment = async () => {
try {
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any;
if (res.status === 200 || res.status === 201) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
onRefresh();
} else {
ToastAndroid.show(res?.data?.message || 'Payment failed!', ToastAndroid.SHORT);
}
} catch (error) {
ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT);
}
};
const selectedCycleData = cycleDashboardData?.cycles.find(c => c.cycleNumber === selectedCycleNumber);
return (
<AppView paddingHorizontal={0} style={{ flex: 1 }}>
<ScrollView
refreshControl={
<RefreshControl refreshing={refreshing || loading} onRefresh={onRefresh} colors={[colors.black]} />
}
contentContainerStyle={styles.scrollContent}
>
<Header title={group.name} />
<Text style={styles.description}>{group.description}</Text>
<View style={styles.statsRow}>
<View style={styles.statBox}>
<Text style={styles.statValue}>{group.members.length ?? 0}</Text>
<Text style={styles.statLabel}>Members</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.statValue}>₹{group.collected}</Text>
<Text style={styles.statLabel}>Contribution</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.statValue}>₹{group.goal}</Text>
<Text style={styles.statLabel}>Total Amount</Text>
</View>
</View>
{cycleItems.length > 0 && (
<CustomDropdown
items={cycleItems}
selectedValue={selectedCycleNumber}
onValueChange={setSelectedCycleNumber}
placeholder="Select a Cycle"
/>
)}
{selectedCycleData && cycleDashboardData && (
<CycleDashboard data={{ ...cycleDashboardData, cycles: [selectedCycleData] }} />
)}
<View style={styles.membersHeader}>
<Text style={styles.sectionTitle}>Members</Text>
<View style={styles.statusBadge}>
<Text style={styles.statusText}>{group.status}</Text>
</View>
</View>
{memberData.map((member:any) => (
<View key={member.userId} style={styles.memberCard}>
<Ionicons name="person" size={40} color={colors.black} />
<View style={styles.memberInfo}>
<Text style={styles.memberName}>
{member.firstName} {member.role === "Organizer" ? "(Organizer)" : ""}
</Text>
</View>
</View>
))}
{/* Button at the bottom inside scroll (no absolute positioning) */}
{group.status === 'ACTIVE' && !payable?.hasPaid && (
<TouchableOpacity onPress={() => setOpenModal(true)} style={styles.button}>
<Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity>
)}
</ScrollView>
{openModal && (
<ContributionScreen
data={payable}
visible={openModal}
onClose={() => setOpenModal(false)}
onConfirm={() => {
confirmPayment();
setOpenModal(false);
}}
/>
)}
</AppView>
);
};
export default GroupDetails;
const styles = StyleSheet.create({
dropdownWrapper: {
marginHorizontal: 15,
marginBottom: 20,
},
dropdownHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 12,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
backgroundColor: '#fff',
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.4)',
padding: 20,
},
dropdownListContainer: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 10,
maxHeight: 300,
},
dropdownItem: {
paddingVertical: 12,
paddingHorizontal: 10,
},
selectedItem: {
backgroundColor: colors.pastelOrange,
borderRadius: 6,
},
itemText: {
fontSize: 15,
color: colors.black,
},
description: {
fontSize: 14,
color: "#475569",
marginBottom: 20,
lineHeight: 20,
paddingHorizontal: 15,
},
statsRow: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 20,
paddingHorizontal: 15,
},
statBox: {
alignItems: 'center',
flex: 1,
},
statValue: {
fontSize: 18,
fontWeight: '700',
color: '#1e293b',
},
statLabel: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
},
membersHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: 'center',
paddingHorizontal: 15,
marginTop: 20,
marginBottom: 10,
},
sectionTitle: {
fontSize: 18,
fontWeight: "600",
color: "#0f172a",
},
statusBadge: {
backgroundColor: colors.pastelOrange,
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 6,
elevation: 2,
},
statusText: {
fontSize: 14,
fontWeight: '600',
color: colors.Home,
},
memberCard: {
flexDirection: "row",
alignItems: 'center',
backgroundColor: '#fff',
padding: 12,
borderRadius: 12,
marginBottom: 10,
elevation: 2,
marginHorizontal: 15,
},
memberInfo: {
marginLeft: 10,
},
memberName: {
fontSize: 15,
fontWeight: "600",
color: "#1e293b",
},
button: {
backgroundColor: colors.pastelOrange,
marginHorizontal: 15,
marginTop: 20,
marginBottom: 30,
elevation: 5,
paddingVertical: 14,
borderRadius: 10,
alignItems: "center",
},
buttonText: {
fontSize: 16,
fontWeight: "600",
color: "#000",
},
scrollContent: {
paddingBottom: 50,
},
});